package usecase

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

	"apps/backend/internal/module/radar/domain"
	usageDomain "apps/backend/internal/module/usage/domain"
)

// JudgeResult is the five-question output for one candidate.
type JudgeResult struct {
	Status         string
	IntentScore    int
	IntentBand     string
	Reasons        []domain.OpportunityReason
	RegionDetected string
	RegionMatch    string
	FreshnessHours int
	MatchedService string
	RejectReason   string
	ProductMatch   *domain.ProductMatch
	// Cacheable is internal provenance: only a structured provider result or a
	// deterministic hard reject may be reused as a judgment cache entry.
	Cacheable bool
}

/*
JudgeCandidate produces intent score / band / five reasons / region_match.
Hard rejects: non-authentic classification, provider_offer/announcement/noise, age > 14d.
Meter: ai_research / radar.judge when AI path is used; heuristic path still bills once if Usage set.
*/
func (s *Service) judgeCandidateUncached(ctx context.Context, ownerUID int64, profile *domain.ServiceProfile, watch *domain.RadarWatch, cand *domain.CandidatePost) (res *JudgeResult, credits int, err error) {
	if watch != nil && watch.ContextMode == domain.WatchContextProduct {
		product, perr := s.LoadProductContext(ctx, ownerUID, watch.BrandID, watch.ProductID)
		if perr != nil {
			return nil, 0, perr
		}
		return s.judgeProductCandidate(ctx, ownerUID, profile, watch, cand, product)
	}
	return s.judgeCandidateGeneric(ctx, ownerUID, profile, watch, cand)
}

func (s *Service) judgeCandidateGeneric(ctx context.Context, ownerUID int64, profile *domain.ServiceProfile, watch *domain.RadarWatch, cand *domain.CandidatePost) (res *JudgeResult, credits int, err error) {
	if cand == nil {
		return nil, 0, fmt.Errorf("%w: candidate required", domain.ErrValidation)
	}
	now := domain.NowNano()
	hours := domain.FreshnessHoursSince(cand.PostedAt, now)

	// Hard reject: stale
	if domain.IsStaleHardReject(cand.PostedAt, now) {
		return hardReject("貼文發布已超過 14 天", hours, profile, watch, cand), 0, nil
	}
	// Hard reject: classification
	switch cand.Classification {
	case "provider_offer", "announcement", "noise":
		return hardReject("分類為"+cand.Classification+"，非真實需求", hours, profile, watch, cand), 0, nil
	}

	charge, berr := s.bill(ctx, ownerUID, usageDomain.MeterAIResearch, "雷達五問判定", "radar.judge")
	if berr != nil {
		return nil, 0, berr
	}
	defer charge.Settle(ctx, &err)
	credits = usageDomain.MeterCost(usageDomain.MeterAIResearch)

	// Prefer structured AI; fall back to deterministic heuristic for tests / offline.
	if raw, aerr := s.completeAI(ctx, ownerUID, judgePrompt(profile, watch, cand)); aerr == nil {
		if parsed, perr := parseJudgeJSON(raw); perr == nil && parsed != nil {
			if err := domain.ValidateReasons(parsed.Reasons); err == nil {
				parsed.FreshnessHours = hours
				if parsed.Status == "" {
					if parsed.IntentScore >= domain.BandMidMinScore {
						parsed.Status = domain.OppQualified
					} else {
						parsed.Status = domain.OppQualified // low still qualified listing
					}
				}
				if parsed.IntentBand == "" {
					parsed.IntentBand = domain.BandFromScore(parsed.IntentScore)
				}
				parsed.Cacheable = true
				return parsed, credits, nil
			}
		}
	}

	// Heuristic judge (also used when AI output is incomplete).
	res = heuristicJudge(profile, watch, cand, hours)
	if err := domain.ValidateReasons(res.Reasons); err != nil {
		// Treat incomplete as judge failure (caller skips persist).
		return nil, credits, err
	}
	return res, credits, nil
}

func hardReject(reason string, hours int, profile *domain.ServiceProfile, watch *domain.RadarWatch, cand *domain.CandidatePost) *JudgeResult {
	areas := serviceAreasFor(profile, watch)
	detected := domain.DetectRegionCodes(cand.Text + " " + cand.Title)
	remote := profile != nil && profile.RemoteOk
	match, regionScore := domain.MatchRegion(detected, areas, remote)
	freshScore := domain.FreshnessScore(hours)
	reasons := []domain.OpportunityReason{
		{Dimension: domain.DimAuthenticity, Score: 0, Reason: reason},
		{Dimension: domain.DimIntent, Score: 0, Reason: "硬否決後不計購買意圖"},
		{Dimension: domain.DimRegion, Score: regionScore, Reason: regionReason(match, detected)},
		{Dimension: domain.DimFreshness, Score: freshScore, Reason: freshnessReason(hours)},
		{Dimension: domain.DimFit, Score: 0, Reason: "硬否決後不計服務匹配"},
	}
	score := domain.SumReasonScores(reasons)
	return &JudgeResult{
		Status:         domain.OppRejected,
		IntentScore:    score,
		IntentBand:     domain.BandFromScore(score),
		Reasons:        reasons,
		RegionDetected: firstOrEmpty(detected),
		RegionMatch:    match,
		FreshnessHours: hours,
		RejectReason:   reason,
		Cacheable:      true,
	}
}

func heuristicJudge(profile *domain.ServiceProfile, watch *domain.RadarWatch, cand *domain.CandidatePost, hours int) *JudgeResult {
	text := strings.ToLower(cand.Text + " " + cand.Title)
	areas := serviceAreasFor(profile, watch)
	detected := domain.DetectRegionCodes(cand.Text + " " + cand.Title)
	remote := profile != nil && profile.RemoteOk
	match, regionScore := domain.MatchRegion(detected, areas, remote)

	// authenticity
	authScore := 10
	authReason := "貼文語氣偏討論，真實需求訊號中等"
	if hasAnySub(text, "求推薦", "有人推薦", "推薦嗎", "徵", "找", "需要", "請問") {
		authScore = domain.WeightAuthenticity
		authReason = "貼文明確在找服務或求推薦，像真實需求"
	}
	if hasAnySub(text, "接案", "檔期", "價格表") {
		authScore = 5
		authReason = "語氣像同業供給，真實需求較弱"
	}

	// intent
	intentScore := 12
	intentReason := "有興趣但購買意圖不明顯"
	if hasAnySub(text, "推薦", "預算", "報價", "價格", "多少錢", "档期", "檔期", "什麼時候") {
		intentScore = domain.WeightIntent
		intentReason = "提到價格／檔期／求推薦，購買意圖高"
	}

	// freshness
	freshScore := domain.FreshnessScore(hours)
	// fit
	fitScore := 4
	fitReason := "與服務項目關聯有限"
	matchedService := ""
	if profile != nil {
		for _, svc := range profile.Services {
			name := strings.ToLower(svc.Name)
			if name != "" && strings.Contains(text, name) {
				fitScore = domain.WeightFit
				fitReason = "貼文提到服務項目「" + svc.Name + "」"
				matchedService = svc.Name
				break
			}
		}
		if matchedService == "" && len(profile.Services) > 0 {
			// soft match via watch terms
			if cand.MatchedTerm != "" {
				fitScore = 7
				fitReason = "觸發關鍵字與服務檔案相關"
				matchedService = profile.Services[0].Name
			}
		}
	}

	// hard reject authenticity if clearly not demand
	if authScore <= 5 && hasAnySub(text, "接案中", "歡迎洽詢我") {
		return hardReject("非真實需求（同業供給語氣）", hours, profile, watch, cand)
	}
	// region mismatch hard reject when not remote
	if match == domain.RegionMismatch && !remote {
		return hardReject("服務地區明確不符且不可遠端", hours, profile, watch, cand)
	}

	reasons := []domain.OpportunityReason{
		{Dimension: domain.DimAuthenticity, Score: authScore, Reason: authReason},
		{Dimension: domain.DimIntent, Score: intentScore, Reason: intentReason},
		{Dimension: domain.DimRegion, Score: regionScore, Reason: regionReason(match, detected)},
		{Dimension: domain.DimFreshness, Score: freshScore, Reason: freshnessReason(hours)},
		{Dimension: domain.DimFit, Score: fitScore, Reason: fitReason},
	}
	score := domain.SumReasonScores(reasons)
	return &JudgeResult{
		Status:         domain.OppQualified,
		IntentScore:    score,
		IntentBand:     domain.BandFromScore(score),
		Reasons:        reasons,
		RegionDetected: firstOrEmpty(detected),
		RegionMatch:    match,
		FreshnessHours: hours,
		MatchedService: matchedService,
	}
}

func serviceAreasFor(profile *domain.ServiceProfile, watch *domain.RadarWatch) []string {
	if watch != nil && len(watch.Regions) > 0 {
		return watch.Regions
	}
	if profile != nil {
		return profile.ServiceAreas
	}
	return nil
}

func regionReason(match string, detected []string) string {
	switch match {
	case domain.RegionMatch:
		return "貼文地區與服務範圍相符（" + strings.Join(detected, ",") + "）"
	case domain.RegionMismatch:
		return "貼文地區不在服務範圍（" + strings.Join(detected, ",") + "）"
	default:
		return "貼文未提地區，不做縣市猜測"
	}
}

func freshnessReason(hours int) string {
	switch {
	case hours <= 24:
		return "24 小時內發布，時效佳"
	case hours <= 72:
		return "2–3 天內，時效尚可"
	case hours <= domain.MaxFreshnessDays*24:
		return "已超過三天，時效偏低"
	default:
		return "超過 14 天"
	}
}

func firstOrEmpty(ss []string) string {
	if len(ss) == 0 {
		return ""
	}
	return ss[0]
}

func judgePrompt(profile *domain.ServiceProfile, watch *domain.RadarWatch, cand *domain.CandidatePost) string {
	var b strings.Builder
	b.WriteString("你是台灣在地服務業的商機判定助理。依五問為貼文打分，只輸出 JSON。\n")
	b.WriteString("五維度權重：authenticity 30、intent 30、region 15、freshness 15、fit 10。\n")
	b.WriteString("region_match 只能是 match|mismatch|unknown，未提地區必須 unknown，禁止猜縣市。\n")
	if profile != nil {
		b.WriteString("服務項目：")
		for i, s := range profile.Services {
			if i > 0 {
				b.WriteString("、")
			}
			b.WriteString(s.Name)
		}
		b.WriteString("\n地區：" + strings.Join(profile.ServiceAreas, ",") + "\n")
		if profile.RemoteOk {
			b.WriteString("可遠端。\n")
		}
	}
	if watch != nil {
		b.WriteString("觸發關鍵字：" + strings.Join(watch.Terms, "、") + "\n")
	}
	b.WriteString("貼文：\n" + cand.Text + "\n")
	b.WriteString(`輸出：{"status":"qualified|rejected","intent_score":0-100,"reasons":[{"dimension":"authenticity|intent|region|freshness|fit","score":0,"reason":"人話"}],"region_detected":"","region_match":"unknown","matched_service":"","reject_reason":""}`)
	return b.String()
}

func parseJudgeJSON(raw string) (*JudgeResult, error) {
	start := strings.Index(raw, "{")
	end := strings.LastIndex(raw, "}")
	if start < 0 || end <= start {
		return nil, fmt.Errorf("no json object")
	}
	var tmp struct {
		Status         string                     `json:"status"`
		IntentScore    int                        `json:"intent_score"`
		Reasons        []domain.OpportunityReason `json:"reasons"`
		RegionDetected string                     `json:"region_detected"`
		RegionMatch    string                     `json:"region_match"`
		MatchedService string                     `json:"matched_service"`
		RejectReason   string                     `json:"reject_reason"`
	}
	if err := json.Unmarshal([]byte(raw[start:end+1]), &tmp); err != nil {
		return nil, err
	}
	score := tmp.IntentScore
	if score == 0 && len(tmp.Reasons) > 0 {
		score = domain.SumReasonScores(tmp.Reasons)
	}
	status := tmp.Status
	if status == "" {
		status = domain.OppQualified
	}
	return &JudgeResult{
		Status:         status,
		IntentScore:    score,
		IntentBand:     domain.BandFromScore(score),
		Reasons:        tmp.Reasons,
		RegionDetected: tmp.RegionDetected,
		RegionMatch:    tmp.RegionMatch,
		MatchedService: tmp.MatchedService,
		RejectReason:   tmp.RejectReason,
	}, nil
}
