package rag

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/mlogclub/simple/sqls"

	"agent-desk/internal/ai"
	"agent-desk/internal/models"
	"agent-desk/internal/pkg/dto"
	"agent-desk/internal/pkg/dto/request"
	"agent-desk/internal/pkg/dto/response"
	"agent-desk/internal/pkg/enums"
	"agent-desk/internal/pkg/errorsx"
	"agent-desk/internal/repositories"
)

type answer struct {
}

var Answer = &answer{}

func (s *answer) DebugSearch(ctx context.Context, req request.KnowledgeSearchRequest) (*response.KnowledgeSearchResponse, error) {
	if strings.TrimSpace(req.Question) == "" {
		return nil, errorsx.InvalidParamI18n("error.e0340")
	}
	startedAt := time.Now()
	results, err := s.retrieve(req, ctx)
	if err != nil {
		return nil, err
	}

	respResults := make([]response.KnowledgeSearchResult, 0, len(results))
	for _, item := range results {
		respResults = append(respResults, response.KnowledgeSearchResult{
			KnowledgeBaseID: item.KnowledgeBaseID,
			ChunkID:         item.ChunkID,
			DocumentID:      item.DocumentID,
			DocumentTitle:   item.DocumentTitle,
			FaqID:           item.FaqID,
			FaqQuestion:     item.FaqQuestion,
			ChunkNo:         item.ChunkNo,
			Title:           item.Title,
			SectionPath:     item.SectionPath,
			Content:         item.Content,
			Score:           float64(item.Score),
		})
	}

	return &response.KnowledgeSearchResponse{
		Question:  req.Question,
		Results:   respResults,
		HitCount:  len(respResults),
		LatencyMs: time.Since(startedAt).Milliseconds(),
	}, nil
}

func (s *answer) DebugAnswer(ctx context.Context, req request.KnowledgeAnswerRequest, operator *dto.AuthPrincipal) (*response.KnowledgeAnswerResponse, error) {
	if strings.TrimSpace(req.Question) == "" {
		return nil, errorsx.InvalidParamI18n("error.e0340")
	}
	startedAt := time.Now()

	retrieveStartedAt := time.Now()
	results, err := s.retrieve(request.KnowledgeSearchRequest{
		KnowledgeBaseIDs: req.KnowledgeBaseIDs,
		Question:         req.Question,
		TopK:             req.TopK,
		ScoreThreshold:   req.ScoreThreshold,
		RerankLimit:      req.RerankLimit,
	}, ctx)
	if err != nil {
		return nil, err
	}
	retrieveMs := time.Since(retrieveStartedAt).Milliseconds()
	knowledgeBase := s.resolveAnswerKnowledgeBase(req.KnowledgeBaseIDs, results)
	contextResults := buildContextHits(Retrieve.SelectContextResults(results, 4000))

	hits := make([]response.KnowledgeSearchResult, 0, len(results))
	topScore := 0.0
	for i, item := range results {
		score := float64(item.Score)
		if i == 0 {
			topScore = score
		}
		hits = append(hits, response.KnowledgeSearchResult{
			KnowledgeBaseID: item.KnowledgeBaseID,
			ChunkID:         item.ChunkID,
			DocumentID:      item.DocumentID,
			DocumentTitle:   item.DocumentTitle,
			FaqID:           item.FaqID,
			FaqQuestion:     item.FaqQuestion,
			ChunkNo:         item.ChunkNo,
			Title:           item.Title,
			SectionPath:     item.SectionPath,
			Content:         item.Content,
			Score:           score,
		})
	}
	citations := buildKnowledgeCitations(hits, 3)

	answerMode := enums.KnowledgeAnswerMode(req.AnswerMode)
	if answerMode == 0 {
		if knowledgeBase != nil {
			answerMode = enums.KnowledgeAnswerMode(knowledgeBase.AnswerMode)
		}
		if answerMode == 0 {
			answerMode = enums.KnowledgeAnswerModeStrict
		}
	}

	fallbackMode := enums.AIAgentFallbackModeNoAnswer

	answerStatus := enums.KnowledgeAnswerStatusNormal
	answer := ""
	modelName := ""
	promptTokens := 0
	completionTokens := 0
	generateStartedAt := time.Now()

	if len(hits) == 0 {
		answerStatus = enums.KnowledgeAnswerStatusNoAnswer
		answer = buildFallbackAnswer(fallbackMode)
	} else {
		contextText := Retrieve.BuildContext(ctx, results, 4000)
		systemPrompt := buildAnswerSystemPrompt(answerMode)
		userPrompt := fmt.Sprintf("用户问题：%s\n\n参考资料：\n%s", req.Question, contextText)
		llmResult, llmErr := ai.LLM.Chat(ctx, systemPrompt, userPrompt)
		if llmErr != nil {
			answerStatus = enums.KnowledgeAnswerStatusFallback
			answer = buildFallbackAnswer(fallbackMode)
		} else {
			answer = llmResult.Content
			modelName = llmResult.ModelName
			promptTokens = llmResult.PromptTokens
			completionTokens = llmResult.CompletionTokens
			if strings.TrimSpace(answer) == "" {
				answerStatus = enums.KnowledgeAnswerStatusFallback
				answer = buildFallbackAnswer(fallbackMode)
			}
		}
	}
	generateMs := time.Since(generateStartedAt).Milliseconds()
	rerankLimit := 0
	chunkProvider := ""
	chunkTargetTokens := 0
	chunkMaxTokens := 0
	chunkOverlapTokens := 0
	if knowledgeBase != nil {
		rerankLimit = resolveRerankLimit(req.RerankLimit, knowledgeBase.DefaultRerankLimit)
		chunkProvider = knowledgeBase.ChunkProvider
		chunkTargetTokens = knowledgeBase.ChunkTargetTokens
		chunkMaxTokens = knowledgeBase.ChunkMaxTokens
		chunkOverlapTokens = knowledgeBase.ChunkOverlapTokens
	}

	logItem, err := RetrieveLog.CreateRetrieveLog(&CreateRetrieveLogRequest{
		KnowledgeBaseID:    firstKnowledgeBaseID(req.KnowledgeBaseIDs),
		Channel:            defaultRetrieveChannel(req.Channel),
		Scene:              defaultRetrieveScene(req.Scene),
		SessionID:          req.SessionID,
		ConversationID:     req.ConversationID,
		Question:           req.Question,
		RewriteQuestion:    "",
		Answer:             answer,
		AnswerStatus:       int(answerStatus),
		ChunkProvider:      chunkProvider,
		ChunkTargetTokens:  chunkTargetTokens,
		ChunkMaxTokens:     chunkMaxTokens,
		ChunkOverlapTokens: chunkOverlapTokens,
		RerankEnabled:      rerankLimit > 0,
		RerankLimit:        rerankLimit,
		Hits:               hits,
		UsedHits:           contextResults,
		Citations:          citations,
		LatencyMs:          time.Since(startedAt).Milliseconds(),
		RetrieveMs:         retrieveMs,
		GenerateMs:         generateMs,
		PromptTokens:       promptTokens,
		CompletionTokens:   completionTokens,
		ModelName:          modelName,
	}, operator)
	if err != nil {
		return nil, err
	}

	return &response.KnowledgeAnswerResponse{
		Question:         req.Question,
		Answer:           answer,
		AnswerStatus:     int(answerStatus),
		AnswerStatusName: getAnswerStatusName(answerStatus),
		Citations:        citations,
		Hits:             hits,
		HitCount:         len(hits),
		TopScore:         topScore,
		LatencyMs:        time.Since(startedAt).Milliseconds(),
		RetrieveMs:       retrieveMs,
		GenerateMs:       generateMs,
		PromptTokens:     promptTokens,
		CompletionTokens: completionTokens,
		ModelName:        modelName,
		RetrieveLogID:    logItem.ID,
	}, nil
}

func buildContextHits(results []RetrieveResult) []response.KnowledgeSearchResult {
	if len(results) == 0 {
		return nil
	}
	hits := make([]response.KnowledgeSearchResult, 0, len(results))
	for _, item := range results {
		hits = append(hits, response.KnowledgeSearchResult{
			KnowledgeBaseID: item.KnowledgeBaseID,
			ChunkID:         item.ChunkID,
			DocumentID:      item.DocumentID,
			DocumentTitle:   item.DocumentTitle,
			FaqID:           item.FaqID,
			FaqQuestion:     item.FaqQuestion,
			ChunkNo:         item.ChunkNo,
			Title:           item.Title,
			SectionPath:     item.SectionPath,
			Content:         item.Content,
			Score:           float64(item.Score),
		})
	}
	return hits
}

func buildKnowledgeCitations(hits []response.KnowledgeSearchResult, limit int) []response.KnowledgeCitation {
	if len(hits) == 0 || limit <= 0 {
		return nil
	}
	citations := make([]response.KnowledgeCitation, 0, limit)
	seen := make(map[string]struct{})
	for _, item := range hits {
		key := fmt.Sprintf("%d|%d|%s|%d", item.DocumentID, item.FaqID, item.SectionPath, item.ChunkNo)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		citations = append(citations, response.KnowledgeCitation{
			DocumentID:    item.DocumentID,
			DocumentTitle: item.DocumentTitle,
			FaqID:         item.FaqID,
			FaqQuestion:   item.FaqQuestion,
			ChunkNo:       item.ChunkNo,
			Title:         item.Title,
			SectionPath:   item.SectionPath,
			Snippet:       truncateCitationSnippet(item.Content, 120),
			Score:         item.Score,
		})
		if len(citations) >= limit {
			break
		}
	}
	return citations
}

func truncateCitationSnippet(text string, limit int) string {
	if limit <= 0 {
		return ""
	}
	runes := []rune(strings.TrimSpace(text))
	if len(runes) <= limit {
		return string(runes)
	}
	return string(runes[:limit]) + "..."
}

func (s *answer) BuildDocumentIndex(ctx context.Context, documentID int64) error {
	return Index.IndexDocumentByID(ctx, documentID)
}

func (s *answer) retrieve(req request.KnowledgeSearchRequest, ctx context.Context) ([]RetrieveResult, error) {
	if len(normalizeKnowledgeBaseIDs(req.KnowledgeBaseIDs)) == 0 {
		return nil, errorsx.InvalidParamI18n("error.e0284")
	}
	knowledgeBases := s.loadKnowledgeBases(req.KnowledgeBaseIDs)

	results, err := Retrieve.Retrieve(ctx, RetrieveRequest{
		KnowledgeBaseIDs: req.KnowledgeBaseIDs,
		Query:            req.Question,
		TopK:             req.TopK,
		ScoreThreshold:   req.ScoreThreshold,
	})
	if err != nil {
		return nil, err
	}

	defaultRerankLimit := resolveDefaultRerankLimit(knowledgeBases)
	rerankLimit := resolveRerankLimit(req.RerankLimit, defaultRerankLimit)
	if rerankLimit > 0 && len(results) > rerankLimit {
		return Retrieve.RetrieveWithRerank(ctx, RetrieveRequest{
			KnowledgeBaseIDs: req.KnowledgeBaseIDs,
			Query:            req.Question,
			TopK:             req.TopK,
			ScoreThreshold:   req.ScoreThreshold,
		}, rerankLimit)
	}
	return results, nil
}

func (s *answer) loadKnowledgeBases(knowledgeBaseIDs []int64) []models.KnowledgeBase {
	normalized := normalizeKnowledgeBaseIDs(knowledgeBaseIDs)
	if len(normalized) == 0 {
		return nil
	}
	items := repositories.KnowledgeBaseRepository.Find(sqls.DB(), sqls.NewCnd().In("id", normalized))
	if len(items) == 0 {
		return nil
	}
	itemMap := make(map[int64]models.KnowledgeBase, len(items))
	for _, item := range items {
		itemMap[item.ID] = item
	}
	results := make([]models.KnowledgeBase, 0, len(normalized))
	for _, id := range normalized {
		if item, ok := itemMap[id]; ok {
			results = append(results, item)
		}
	}
	return results
}

func (s *answer) resolvePrimaryKnowledgeBase(knowledgeBaseIDs []int64) *models.KnowledgeBase {
	items := s.loadKnowledgeBases(knowledgeBaseIDs)
	for _, item := range items {
		return &item
	}
	return nil
}

func (s *answer) resolveAnswerKnowledgeBase(knowledgeBaseIDs []int64, results []RetrieveResult) *models.KnowledgeBase {
	items := s.loadKnowledgeBases(knowledgeBaseIDs)
	if len(items) == 0 {
		return nil
	}
	if len(results) > 0 {
		for _, item := range items {
			if item.ID == results[0].KnowledgeBaseID {
				return &item
			}
		}
	}
	return &items[0]
}

func firstKnowledgeBaseID(ids []int64) int64 {
	normalized := normalizeKnowledgeBaseIDs(ids)
	if len(normalized) == 0 {
		return 0
	}
	return normalized[0]
}

func resolveRerankLimit(requestLimit int, defaultLimit int) int {
	if requestLimit > 0 {
		return requestLimit
	}
	if defaultLimit > 0 {
		return defaultLimit
	}
	return 0
}

func resolveDefaultRerankLimit(items []models.KnowledgeBase) int {
	limit := 0
	for _, item := range items {
		if item.DefaultRerankLimit > limit {
			limit = item.DefaultRerankLimit
		}
	}
	return limit
}

func buildAnswerSystemPrompt(answerMode enums.KnowledgeAnswerMode) string {
	if answerMode == enums.KnowledgeAnswerModeAssist {
		return "你是客服知识库助手。请优先依据提供的知识片段回答，可以做轻度归纳，但不要编造未提供的事实。"
	}
	return "你是严格的客服知识库助手。只能依据提供的知识片段回答；如果资料不足，请明确说明知识库暂无明确信息。"
}

func buildFallbackAnswer(fallbackMode enums.AIAgentFallbackMode) string {
	switch fallbackMode {
	case enums.AIAgentFallbackModeSuggestRetry:
		return "当前知识库里没有找到足够明确的信息，你可以换个更具体的问法再试一次。"
	default:
		return "当前知识库暂无明确信息。"
	}
}

func defaultRetrieveChannel(channel string) string {
	if strings.TrimSpace(channel) == "" {
		return string(enums.KnowledgeRetrieveChannelDebug)
	}
	return channel
}

func defaultRetrieveScene(scene string) string {
	if strings.TrimSpace(scene) == "" {
		return string(enums.KnowledgeRetrieveSceneQA)
	}
	return scene
}

func getAnswerStatusName(status enums.KnowledgeAnswerStatus) string {
	switch status {
	case enums.KnowledgeAnswerStatusNormal:
		return "正常"
	case enums.KnowledgeAnswerStatusNoAnswer:
		return "无答案"
	case enums.KnowledgeAnswerStatusFallback:
		return "兜底"
	case enums.KnowledgeAnswerStatusBlocked:
		return "风控拦截"
	default:
		return "未知"
	}
}
