package viral

import (
	"context"
	"fmt"
	"strings"

	"haixun-backend/internal/library/placement"
)

const (
	defaultLimitPerKeyword = 15
	missionLimitPerKeyword = 10
	maxKeywords            = 6
	maxMergedPosts         = 60
	missionMaxMergedPosts  = 40
	missionQualityTarget   = 12 // stop scanning extra keywords once enough quality posts
)

type DiscoverInput struct {
	Keywords    []string
	Exclusions  []string
	Member      placement.MemberContext
	Crawler     placement.CrawlerSearchFn
	Limit       int  // per keyword; 0 = default
	MaxMerged   int  // total cap; 0 = default
	MissionScan bool // leaner defaults to save search API quota
}

type ProgressFn func(message string, pct int)

// RunDiscover searches Threads for viral candidates across keywords, ranked by engagement.
func RunDiscover(ctx context.Context, input DiscoverInput, progress ProgressFn) ([]placement.ScanCandidate, error) {
	keywords := normalizeKeywords(input.Keywords)
	if len(keywords) == 0 {
		return nil, fmt.Errorf("請提供至少一個爆款掃描關鍵字")
	}
	perKeyword := input.Limit
	if perKeyword <= 0 {
		if input.MissionScan {
			perKeyword = missionLimitPerKeyword
		} else {
			perKeyword = defaultLimitPerKeyword
		}
	}
	maxMerged := input.MaxMerged
	if maxMerged <= 0 {
		if input.MissionScan {
			maxMerged = missionMaxMergedPosts
		} else {
			maxMerged = maxMergedPosts
		}
	}

	merged := map[string]placement.ScanCandidate{}
	relaxed := map[string]placement.ScanCandidate{}
	total := len(keywords)
	pathLabel := input.Member.DiscoverPathLabel()
	var lastErr error
	keywordsAttempted := 0

	for i, keyword := range keywords {
		if input.MissionScan && countMissionQuality(merged) >= missionQualityTarget {
			if progress != nil {
				progress(fmt.Sprintf("已收足 %d 篇品質候選，略過剩餘標籤以節省搜尋次數", missionQualityTarget), 10+(i*70)/max(total, 1))
			}
			break
		}
		if progress != nil {
			pct := 10 + (i*70)/total
			progress(fmt.Sprintf("掃描「%s」（%s）…", keyword, pathLabel), pct)
		}
		limit := perKeyword
		if input.MissionScan && len(merged) > 0 {
			limit = min(perKeyword, 8)
		}
		posts, _, err := placement.Discover(ctx, placement.DiscoverRequest{
			Query:   keyword,
			Keyword: keyword,
			Limit:   limit,
			Member:  input.Member,
			Crawler: input.Crawler,
		})
		if err != nil {
			lastErr = err
			if progress != nil {
				progress(fmt.Sprintf("「%s」搜尋略過：%s", keyword, shortenDiscoverErr(err)), 10+(i*70)/max(total, 1))
			}
			continue
		}
		keywordsAttempted++
		for _, post := range posts {
			key := strings.TrimSpace(post.Permalink)
			if key == "" {
				key = strings.TrimSpace(post.ExternalID)
			}
			if key == "" {
				continue
			}
			score := ScorePost(post.LikeCount, post.ReplyCount)
			candidate := placement.ScanCandidate{
				Permalink:       post.Permalink,
				ExternalID:      post.ExternalID,
				Author:          post.Author,
				AuthorVerified:  post.AuthorVerified,
				FollowerCount:   post.FollowerCount,
				Text:            post.Text,
				SearchTag:       keyword,
				Source:          post.Source,
				LikeCount:       post.LikeCount,
				ReplyCount:      post.ReplyCount,
				EngagementScore: score,
				PlacementScore:  score,
				Priority:        PriorityLabel(score),
			}
			if input.MissionScan {
				if PassesMissionQualityCandidate(
					post.Text, post.LikeCount, post.ReplyCount, score,
					post.AuthorVerified, post.FollowerCount, input.Exclusions,
				) {
					mergeCandidate(merged, key, candidate)
					continue
				}
				if PassesViralCandidate(post.Text, post.LikeCount, post.ReplyCount, score, input.Exclusions) {
					mergeCandidate(relaxed, key, candidate)
				}
				continue
			}
			if !PassesViralCandidate(post.Text, post.LikeCount, post.ReplyCount, score, input.Exclusions) {
				continue
			}
			mergeCandidate(merged, key, candidate)
		}
	}

	if input.MissionScan && len(merged) == 0 && len(relaxed) > 0 {
		merged = relaxed
		if progress != nil {
			progress("未取得藍勾等延伸資料，改以互動門檻收斂爆款候選", 82)
		}
	}

	out := candidatesFromMap(merged)
	sortByEngagement(out)
	if len(out) > maxMerged {
		out = out[:maxMerged]
	}
	if len(out) == 0 {
		if keywordsAttempted == 0 && lastErr != nil {
			return nil, fmt.Errorf("所有標籤搜尋均失敗：%w", lastErr)
		}
	}
	if progress != nil {
		progress(fmt.Sprintf("合併 %d 篇爆款候選", len(out)), 85)
	}
	return out, nil
}

func mergeCandidate(merged map[string]placement.ScanCandidate, key string, candidate placement.ScanCandidate) {
	if prev, ok := merged[key]; !ok {
		merged[key] = candidate
	} else if candidate.EngagementScore > prev.EngagementScore {
		merged[key] = MergeAuthorSignals(candidate, prev)
	} else {
		merged[key] = MergeAuthorSignals(prev, candidate)
	}
}

func candidatesFromMap(merged map[string]placement.ScanCandidate) []placement.ScanCandidate {
	out := make([]placement.ScanCandidate, 0, len(merged))
	for _, item := range merged {
		out = append(out, item)
	}
	return out
}

func shortenDiscoverErr(err error) string {
	msg := strings.TrimSpace(err.Error())
	if len(msg) > 80 {
		return msg[:80] + "…"
	}
	return msg
}

func normalizeKeywords(raw []string) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0, len(raw))
	for _, item := range raw {
		kw := DiscoverKeywordFromTag(item)
		if kw == "" {
			continue
		}
		if _, ok := seen[kw]; ok {
			continue
		}
		seen[kw] = struct{}{}
		out = append(out, kw)
		if len(out) >= maxKeywords {
			break
		}
	}
	return out
}

func countMissionQuality(merged map[string]placement.ScanCandidate) int {
	n := 0
	for _, item := range merged {
		if PassesMissionQualityCandidate(
			item.Text, item.LikeCount, item.ReplyCount, item.EngagementScore,
			item.AuthorVerified, item.FollowerCount, nil,
		) {
			n++
		}
	}
	return n
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func sortByEngagement(items []placement.ScanCandidate) {
	for i := 0; i < len(items); i++ {
		for j := i + 1; j < len(items); j++ {
			if items[j].EngagementScore > items[i].EngagementScore {
				items[i], items[j] = items[j], items[i]
			}
		}
	}
}
