package viral

import (
	"fmt"
	"math"
	"sort"
	"strings"

	"haixun-backend/internal/library/clock"
	"haixun-backend/internal/library/placement"
	libthreads "haixun-backend/internal/library/threadsapi"
	missionentity "haixun-backend/internal/model/copy_mission/domain/entity"
)

const (
	RefAccountMinBestEngagement  = 50
	RefAccountMinBestLikes       = 18
	RefAccountMinTotalEngagement = 80
	RefVerifiedMinBestEngagement = 35
	RefVerifiedMinBestLikes      = 10
)

// ReferenceRankWeights controls the weighted sort key applied to ranked
// reference authors. Defaults preserve historical precedence (verified first,
// then follower count, then total engagement, then best single-post
// engagement) and introduce topic relevance as a tie-breaker multiplier.
type ReferenceRankWeights struct {
	VerifiedW        int
	FollowerW        int
	TotalEngagementW int
	BestEngagementW  int
	TopicRelevanceW  int
}

// DefaultReferenceRankWeights returns the canonical weights used by
// BuildReferenceAccountsFromScan. Callers may pass a customised copy via
// ReferenceAccountInput in a future iteration; Phase 1 keeps it internal.
func DefaultReferenceRankWeights() ReferenceRankWeights {
	return ReferenceRankWeights{
		VerifiedW:        4,
		FollowerW:        2,
		TotalEngagementW: 1,
		BestEngagementW:  1,
		TopicRelevanceW:  2,
	}
}

// rankScore converts aggregated author signals into a weighted integer sort
// key. Follower count is log-scaled so 10M-follower mega accounts do not
// dominate niche candidates with high topic relevance.
func (w ReferenceRankWeights) rankScore(item referenceAuthorAgg) int {
	score := 0
	if item.verified {
		score += w.VerifiedW * 1000
	}
	followerBucket := 0
	switch {
	case item.followerCount >= 1_000_000:
		followerBucket = 4
	case item.followerCount >= 100_000:
		followerBucket = 3
	case item.followerCount >= 10_000:
		followerBucket = 2
	case item.followerCount >= 1_000:
		followerBucket = 1
	}
	score += w.FollowerW * followerBucket * 100
	score += w.TotalEngagementW * item.totalEngagement
	score += w.BestEngagementW * item.bestEngagement
	score += w.TopicRelevanceW * item.topicHits * 50
	return score
}

type ReferenceAccountInput struct {
	SeedQuery         string
	Label             string
	Posts             []placement.ScanCandidate
	Limit             int
	ExcludedUsernames []string
}

type referenceAuthorAgg struct {
	username        string
	verified        bool
	followerCount   int
	totalEngagement int
	bestEngagement  int
	bestLikes       int
	bestReplies     int
	postCount       int
	topicHits       int
	sampleText      string
	sampleSearchTag string
	samplePermalink string
}

// BuildReferenceAccountsFromScan lists authors from patrol posts that match the
// mission topic and pass quality gates. Verified/follower are optional bonuses;
// if strict gates yield nothing, falls back to baseline viral engagement.
func BuildReferenceAccountsFromScan(in ReferenceAccountInput) []missionentity.SimilarAccount {
	out := buildReferenceAccountsFromScan(in, true)
	if len(out) > 0 {
		return out
	}
	return buildReferenceAccountsFromScan(in, false)
}

func buildReferenceAccountsFromScan(in ReferenceAccountInput, strictQuality bool) []missionentity.SimilarAccount {
	limit := in.Limit
	if limit <= 0 {
		limit = MaxSimilarAccounts
	}
	byUser := map[string]referenceAuthorAgg{}
	terms := normalisedTopicTerms(in.SeedQuery, in.Label)
	weights := DefaultReferenceRankWeights()
	now := clock.NowUnixNano()
	topicDenom := math.Max(1, float64(len(terms)))
	for _, post := range in.Posts {
		user := strings.TrimSpace(post.Author)
		if user == "" || !isValidUsername(user) {
			continue
		}
		if isExcluded(in.ExcludedUsernames, user) {
			continue
		}
		hits := topicTopicHits(post, terms)
		if hits == 0 {
			continue
		}
		if strictQuality {
			if !PassesMissionQualityCandidate(
				post.Text, post.LikeCount, post.ReplyCount, post.EngagementScore,
				post.AuthorVerified, post.FollowerCount, nil,
			) {
				continue
			}
		} else if !PassesViralCandidate(
			post.Text, post.LikeCount, post.ReplyCount, post.EngagementScore, nil,
		) {
			continue
		}
		key := strings.ToLower(user)
		prev := byUser[key]
		prev.username = user
		if post.AuthorVerified {
			prev.verified = true
		}
		if post.FollowerCount > prev.followerCount {
			prev.followerCount = post.FollowerCount
		}
		prev.postCount++
		prev.totalEngagement += post.EngagementScore
		if hits > prev.topicHits {
			prev.topicHits = hits
		}
		if post.EngagementScore > prev.bestEngagement {
			prev.bestEngagement = post.EngagementScore
			prev.bestLikes = post.LikeCount
			prev.bestReplies = post.ReplyCount
			text := strings.TrimSpace(post.Text)
			if len([]rune(text)) > 80 {
				text = string([]rune(text)[:80])
			}
			prev.sampleText = text
			prev.sampleSearchTag = strings.TrimSpace(post.SearchTag)
			prev.samplePermalink = strings.TrimSpace(post.Permalink)
		}
		byUser[key] = prev
	}

	ranked := make([]referenceAuthorAgg, 0, len(byUser))
	for _, item := range byUser {
		if qualifiesReferenceAuthor(item, strictQuality) {
			ranked = append(ranked, item)
		}
	}
	sort.Slice(ranked, func(i, j int) bool {
		si := weights.rankScore(ranked[i])
		sj := weights.rankScore(ranked[j])
		if si != sj {
			return si > sj
		}
		// stable historical tie-breakers (verified first, follower,
		// total engagement, best engagement) preserved for determinism.
		if ranked[i].verified != ranked[j].verified {
			return ranked[i].verified
		}
		if ranked[i].followerCount != ranked[j].followerCount {
			return ranked[i].followerCount > ranked[j].followerCount
		}
		if ranked[i].totalEngagement != ranked[j].totalEngagement {
			return ranked[i].totalEngagement > ranked[j].totalEngagement
		}
		return ranked[i].bestEngagement > ranked[j].bestEngagement
	})
	if len(ranked) > limit {
		ranked = ranked[:limit]
	}

	out := make([]missionentity.SimilarAccount, 0, len(ranked))
	for _, item := range ranked {
		conf := "medium"
		if item.verified {
			conf = "high"
		} else if item.bestEngagement >= HotEngagementScore || item.totalEngagement >= 120 {
			conf = "high"
		}
		out = append(out, missionentity.SimilarAccount{
			Username:        item.username,
			Reason:          formatReferenceReason(item),
			Source:          "scan",
			MatchedSource:   []string{"scan"},
			Confidence:      conf,
			Status:          missionentity.SimilarAccountStatusRecommended,
			TopicRelevance:  float64(item.topicHits) / topicDenom,
			LastSeenAt:      now,
			ProfileURL:      libthreads.ProfileURLFromPermalink(item.samplePermalink, item.username),
			AuthorVerified:  item.verified,
			FollowerCount:   item.followerCount,
			EngagementScore: item.bestEngagement,
			LikeCount:       item.bestLikes,
			ReplyCount:      item.bestReplies,
			PostCount:       item.postCount,
		})
	}
	return out
}

func qualifiesReferenceAuthor(item referenceAuthorAgg, strictQuality bool) bool {
	if item.postCount == 0 {
		return false
	}
	if item.verified {
		return item.bestLikes >= RefVerifiedMinBestLikes &&
			(item.bestEngagement >= RefVerifiedMinBestEngagement || item.totalEngagement >= 60)
	}
	if strictQuality {
		if item.bestLikes < RefAccountMinBestLikes {
			return false
		}
		return item.bestEngagement >= RefAccountMinBestEngagement || item.totalEngagement >= RefAccountMinTotalEngagement
	}
	return item.bestLikes >= 8 && item.bestEngagement >= MinEngagementScore
}

func postTopicRelevant(post placement.ScanCandidate, seed, label string) bool {
	return topicTopicHits(post, normalisedTopicTerms(seed, label)) > 0
}

// topicTopicHits counts how many normalised topic terms appear (case-folded
// substring match against the post's text and search tag). Returning a hit
// count (instead of a boolean) lets the ranking weight reward posts that are
// relevant across multiple seed/label tokens — a coarse but dependency-free
// CJK-friendly proxy for topic similarity.
func topicTopicHits(post placement.ScanCandidate, terms []string) int {
	if len(terms) == 0 {
		text := strings.TrimSpace(post.Text)
		tag := strings.TrimSpace(post.SearchTag)
		if text == "" && tag == "" {
			return 0
		}
		return 1
	}
	text := strings.ToLower(strings.TrimSpace(post.Text))
	tag := strings.ToLower(strings.TrimSpace(post.SearchTag))
	hits := 0
	for _, term := range terms {
		if term == "" {
			continue
		}
		if strings.Contains(text, term) || strings.Contains(tag, term) {
			hits++
		}
	}
	return hits
}

// normalisedTopicTerms lowercases and de-spaces the seed-query and label while
// also exposing coarse tokens split on whitespace and punctuation. It remains
// dependency-free and CJK-friendly, but avoids matching only a long exact phrase.
func normalisedTopicTerms(seed, label string) []string {
	out := []string{}
	terms := missionTopicMatchTerms(seed, label, nil)
	seen := map[string]struct{}{}
	for _, term := range append(append([]string{}, terms.Anchors...), terms.Terms...) {
		if term == "" {
			continue
		}
		if _, ok := seen[term]; ok {
			continue
		}
		seen[term] = struct{}{}
		out = append(out, term)
	}
	return out
}

func formatReferenceReason(item referenceAuthorAgg) string {
	if item.sampleText != "" {
		return item.sampleText
	}
	if item.sampleSearchTag != "" {
		return fmt.Sprintf("標籤「%s」高互動作者", item.sampleSearchTag)
	}
	return "本次海巡高互動作者"
}
