package viral

import (
	"sort"
	"strings"
	"unicode/utf8"

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

const MaxAudienceSamples = 15

type AudienceOpts struct {
	Max            int
	ExcludeAuthors []string
	Now            int64
}

type audienceAgg struct {
	username       string
	samplePostID   string
	sampleText     string
	replyLikeCount int
	appearances    int
	lastPostedAt   string
}

// BuildAudienceSamplesFromReplies aggregates reply authors into potential
// audience samples. Low-quality replies and known similar-account authors are
// filtered out.
func BuildAudienceSamplesFromReplies(replies []placement.ReplyCandidate, opts AudienceOpts) []missionentity.AudienceSample {
	if len(replies) == 0 {
		return nil
	}
	max := opts.Max
	if max <= 0 {
		max = MaxAudienceSamples
	}
	exclude := map[string]struct{}{}
	for _, author := range opts.ExcludeAuthors {
		key := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(author, "@")))
		if key != "" {
			exclude[key] = struct{}{}
		}
	}

	byUser := map[string]audienceAgg{}
	for _, reply := range replies {
		user := strings.TrimSpace(reply.Author)
		if user == "" || !isValidUsername(user) {
			continue
		}
		if _, ok := exclude[strings.ToLower(user)]; ok {
			continue
		}
		text := strings.TrimSpace(reply.Text)
		if isLowQualityReply(text) {
			continue
		}
		key := strings.ToLower(user)
		prev := byUser[key]
		prev.username = user
		prev.appearances++
		if reply.LikeCount > prev.replyLikeCount {
			prev.replyLikeCount = reply.LikeCount
			prev.sampleText = text
			prev.samplePostID = strings.TrimSpace(reply.ExternalID)
			prev.lastPostedAt = strings.TrimSpace(reply.PostedAt)
		} else if prev.sampleText == "" {
			prev.sampleText = text
			prev.samplePostID = strings.TrimSpace(reply.ExternalID)
			prev.lastPostedAt = strings.TrimSpace(reply.PostedAt)
		}
		byUser[key] = prev
	}

	ranked := make([]audienceAgg, 0, len(byUser))
	for _, item := range byUser {
		ranked = append(ranked, item)
	}
	sort.Slice(ranked, func(i, j int) bool {
		if ranked[i].appearances != ranked[j].appearances {
			return ranked[i].appearances > ranked[j].appearances
		}
		if ranked[i].replyLikeCount != ranked[j].replyLikeCount {
			return ranked[i].replyLikeCount > ranked[j].replyLikeCount
		}
		return ranked[i].lastPostedAt > ranked[j].lastPostedAt
	})
	if len(ranked) > max {
		ranked = ranked[:max]
	}

	now := opts.Now
	out := make([]missionentity.AudienceSample, 0, len(ranked))
	for _, item := range ranked {
		out = append(out, missionentity.AudienceSample{
			Username:       item.username,
			SamplePostID:   item.samplePostID,
			SampleText:     item.sampleText,
			ReplyLikeCount: item.replyLikeCount,
			Appearances:    item.appearances,
			FirstSeenAt:    now,
			LastSeenAt:     now,
		})
	}
	return out
}

// MergeAudienceSamples merges previous and newly built audience samples keyed
// by lowercase username. New samples refresh statistics; pinned samples are
// preserved even when not seen in the latest scan.
func MergeAudienceSamples(prev, next []missionentity.AudienceSample) []missionentity.AudienceSample {
	if len(prev) == 0 {
		return append([]missionentity.AudienceSample(nil), next...)
	}
	if len(next) == 0 {
		return prioritizePinnedAudienceSamples(append([]missionentity.AudienceSample(nil), prev...))
	}

	byUser := map[string]missionentity.AudienceSample{}
	newOrder := make([]string, 0, len(next))
	for _, item := range next {
		key := strings.ToLower(strings.TrimSpace(item.Username))
		if key == "" {
			continue
		}
		if _, ok := byUser[key]; !ok {
			newOrder = append(newOrder, key)
		}
		byUser[key] = mergeAudienceRecord(byUser[key], item)
	}

	prevOrder := make([]string, 0, len(prev))
	for _, item := range prev {
		key := strings.ToLower(strings.TrimSpace(item.Username))
		if key == "" {
			continue
		}
		if _, ok := byUser[key]; ok {
			merged := mergeAudienceRecord(item, byUser[key])
			byUser[key] = merged
			continue
		}
		byUser[key] = item
		prevOrder = append(prevOrder, key)
	}

	out := make([]missionentity.AudienceSample, 0, len(newOrder)+len(prevOrder))
	for _, key := range newOrder {
		if sample, ok := byUser[key]; ok {
			out = append(out, sample)
		}
	}
	for _, key := range prevOrder {
		if sample, ok := byUser[key]; ok {
			out = append(out, sample)
		}
	}
	return prioritizePinnedAudienceSamples(out)
}

func mergeAudienceRecord(prev, next missionentity.AudienceSample) missionentity.AudienceSample {
	out := next
	if prev.FirstSeenAt > 0 {
		out.FirstSeenAt = prev.FirstSeenAt
	}
	if prev.Status == missionentity.AudienceSampleStatusPinned || prev.Status == missionentity.AudienceSampleStatusExcluded {
		out.Status = prev.Status
	}
	if out.LastSeenAt == 0 {
		out.LastSeenAt = prev.LastSeenAt
	}
	return out
}

func prioritizePinnedAudienceSamples(samples []missionentity.AudienceSample) []missionentity.AudienceSample {
	if len(samples) == 0 {
		return nil
	}
	pinned := make([]missionentity.AudienceSample, 0)
	rest := make([]missionentity.AudienceSample, 0, len(samples))
	for _, item := range samples {
		if item.Status == missionentity.AudienceSampleStatusPinned {
			pinned = append(pinned, item)
		} else {
			rest = append(rest, item)
		}
	}
	out := append(pinned, rest...)
	if len(out) > MaxAudienceSamples {
		remaining := MaxAudienceSamples - len(pinned)
		if remaining < 0 {
			return pinned[:MaxAudienceSamples]
		}
		out = append(pinned, rest[:remaining]...)
	}
	return out
}

func isLowQualityReply(text string) bool {
	trimmed := strings.TrimSpace(text)
	if trimmed == "" {
		return true
	}
	if utf8.RuneCountInString(trimmed) < 5 {
		return true
	}
	lower := strings.ToLower(trimmed)
	if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
		fields := strings.Fields(trimmed)
		if len(fields) <= 1 {
			return true
		}
	}
	return false
}
