package usecase

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"apps/backend/internal/module/radar/domain"
	usageDomain "apps/backend/internal/module/usage/domain"
	"github.com/zeromicro/go-zero/core/logx"
)

type productFitAIResult struct {
	ProductFitScore int                       `json:"product_fit_score"`
	ProductFitBand  string                    `json:"product_fit_band"`
	Excluded        bool                      `json:"excluded"`
	ExcludeReason   string                    `json:"exclude_reason"`
	Reasons         []domain.ProductFitReason `json:"reasons"`
	Risks           []string                  `json:"risks"`
}

func productFitPrompt(product *ProductContextSnapshot, cand *domain.CandidatePost) string {
	return fmt.Sprintf(`你是產品適配判定器，只輸出 JSON，不要猜測產品承諾。
四維權重固定為 pain=35、scenario=25、audience=20、capability=20。
產品上下文：品牌=%q；產品=%q；受眾=%q；痛點=%q；情境=%q；標籤=%q；能力=%q；排除詞=%q。
	候選公開貼文（excerpt 必須逐字取自這段文字）：%s
每一維都必須輸出一筆 reason。正分時 candidate_excerpt 必須是候選文字原文短片段，product_basis 必須逐字來自對應產品上下文；找不到證據就 score=0。單純產品／品牌名稱命中不得給 pain 或 scenario 分。受眾不確定給 0。命中排除詞、同業供給、公告或雜訊時 excluded=true 並填 exclude_reason。
	格式：{"product_fit_score":0,"product_fit_band":"strong|possible|weak","excluded":false,"exclude_reason":"","reasons":[{"dimension":"pain|scenario|audience|capability","score":0,"reason":"人話","candidate_excerpt":"","product_basis":""}],"risks":[]}`, product.BrandName, product.ProductLabel, product.TargetAudience, strings.Join(product.PainPoints, "、"), product.ProductContext, strings.Join(product.MatchTags, "、"), strings.Join(product.ProviderCapabilityTerms, "、"), strings.Join(product.ProviderExcludeTerms, "、"), cand.Text+" "+cand.Title)
}

func (s *Service) judgeProductCandidate(ctx context.Context, ownerUID int64, profile *domain.ServiceProfile, watch *domain.RadarWatch, cand *domain.CandidatePost, product *ProductContextSnapshot) (*JudgeResult, int, error) {
	base, credits, err := s.judgeCandidateGeneric(ctx, ownerUID, profile, watch, cand)
	if err != nil || base == nil {
		return base, credits, err
	}
	match, fitCredits, ferr := s.scoreProductFit(ctx, ownerUID, product, cand)
	credits += fitCredits
	if ferr != nil {
		return nil, credits, ferr
	}
	if watch != nil && watch.ID != "" {
		match.WatchIDs = []string{watch.ID}
		if err := match.ValidateForWrite(); err != nil {
			return nil, credits, err
		}
	}
	base.ProductMatch = match
	return base, credits, nil
}

func (s *Service) scoreProductFit(ctx context.Context, ownerUID int64, product *ProductContextSnapshot, cand *domain.CandidatePost) (_ *domain.ProductMatch, credits int, err error) {
	charge, err := s.bill(ctx, ownerUID, usageDomain.MeterAIResearch, "雷達產品適配判定", "radar.product_fit")
	if err != nil {
		return nil, 0, err
	}
	defer charge.Settle(ctx, &err)
	credits = usageDomain.MeterCost(usageDomain.MeterAIResearch)

	if raw, aiErr := s.completeAI(ctx, ownerUID, productFitPrompt(product, cand)); aiErr == nil {
		if parsed, perr := parseProductFitJSON(raw); perr == nil {
			if match, verr := validateProductFitAI(parsed, product, cand); verr == nil {
				return match, credits, nil
			}
			logx.Errorf("radar product fit: invalid AI result uid=%d; using deterministic fallback", ownerUID)
		} else {
			logx.Errorf("radar product fit: AI JSON parse failed uid=%d: %v; using deterministic fallback", ownerUID, perr)
		}
	} else {
		logx.Errorf("radar product fit: provider unavailable uid=%d: %v; using deterministic fallback", ownerUID, aiErr)
	}

	match, ferr := ScoreProductFit(cand, product)
	if ferr != nil {
		return nil, credits, ferr
	}
	return match, credits, nil
}

func parseProductFitJSON(raw string) (*productFitAIResult, error) {
	start, end := strings.Index(raw, "{"), strings.LastIndex(raw, "}")
	if start < 0 || end <= start {
		return nil, fmt.Errorf("product fit JSON object missing")
	}
	var parsed productFitAIResult
	if err := json.Unmarshal([]byte(raw[start:end+1]), &parsed); err != nil {
		return nil, err
	}
	return &parsed, nil
}

func validateProductFitAI(in *productFitAIResult, product *ProductContextSnapshot, cand *domain.CandidatePost) (*domain.ProductMatch, error) {
	if in == nil || len(in.Reasons) != len(domain.ProductFitDimensionOrder) {
		return nil, fmt.Errorf("product fit requires four reasons")
	}
	for _, reason := range in.Reasons {
		if reason.Score > 0 {
			if strings.TrimSpace(reason.CandidateExcerpt) == "" || findProductExcerpt(cand.Text+" "+cand.Title, reason.CandidateExcerpt) == "" {
				return nil, fmt.Errorf("candidate excerpt is not from source")
			}
			if !productBasisExists(reason.Dimension, reason.ProductBasis, product) {
				return nil, fmt.Errorf("product basis is not in snapshot")
			}
		}
		if strings.TrimSpace(reason.Reason) == "" {
			return nil, fmt.Errorf("empty product fit reason")
		}
	}
	match := &domain.ProductMatch{
		BrandID: product.BrandID, ProductID: product.ProductID,
		BrandNameSnapshot: product.BrandName, ProductLabelSnapshot: product.ProductLabel,
		BrandUpdatedAt: product.BrandUpdatedAt, ProductUpdatedAt: product.ProductUpdatedAt,
		ProductFitScore: in.ProductFitScore, ProductFitBand: in.ProductFitBand,
		Excluded: in.Excluded, ExcludeReason: strings.TrimSpace(in.ExcludeReason),
		Reasons: append([]domain.ProductFitReason(nil), in.Reasons...), Risks: append([]string(nil), in.Risks...), MatchedAt: domain.NowNano(),
	}
	for _, reason := range match.Reasons {
		if reason.Score > 0 && strings.TrimSpace(reason.ProductBasis) != "" {
			match.MatchedTerms = append(match.MatchedTerms, strings.TrimSpace(reason.ProductBasis))
		}
	}
	if match.Risks == nil {
		match.Risks = []string{}
	}
	if err := match.ValidateForWrite(); err != nil {
		return nil, err
	}
	// Hard exclusions are deterministic even when a model forgets to set the flag.
	if excluded, reason := productCandidateExclusion(cand.Text+" "+cand.Title, cand, product); excluded {
		match.Excluded, match.ExcludeReason = true, reason
		if err := match.ValidateForWrite(); err != nil {
			return nil, err
		}
	}
	return match, nil
}

func productBasisExists(dimension, basis string, product *ProductContextSnapshot) bool {
	basis = strings.TrimSpace(basis)
	if basis == "" {
		return false
	}
	var values []string
	switch dimension {
	case domain.ProductFitPain:
		values = product.PainPoints
	case domain.ProductFitScenario:
		values = append(append([]string(nil), product.MatchTags...), product.ProductContext)
	case domain.ProductFitAudience:
		values = []string{product.TargetAudience}
	case domain.ProductFitCapability:
		values = product.ProviderCapabilityTerms
	default:
		return false
	}
	for _, value := range values {
		if strings.EqualFold(strings.TrimSpace(value), basis) {
			return true
		}
	}
	return false
}
