package plugin

import (
	"database/sql"
	"fmt"
	"log/slog"
	"math"
	"sort"
	"strings"

	"gogobee/internal/db"
)

// ─── Threshold Constants ────��──────────────────────────────────────

const (
	// Communication
	thNovelistAvgWords      = 7
	thNovelistMinMsgs       = 100
	thMinimalistMaxAvgWords = 4
	thMinimalistMinMsgs     = 50
	thInquisitorPct         = 8
	thInquisitorMinMsgs     = 50
	thEnthusiastPct         = 8
	thEnthusiastMinMsgs     = 50
	thChatterboxMinMsgs     = 500
	thChatterboxMinAvgWords = 5
	thLinkmasterPct         = 5
	thLinkmasterMinMsgs     = 50

	// Temporal
	thNightOwlPct      = 40
	thNightOwlMinMsgs  = 100
	thEarlyBirdPct     = 40
	thEarlyBirdMinMsgs = 100

	// Emotional (LLM-gated)
	thEmotionalMinClassified = 100
	thCheerleaderPosPct      = 50
	thPhilosopherNeutPct     = 40
	thPhilosopherQPct        = 10
	thPhilosopherAvgWords    = 8
	thAgitatorNegPct         = 30
	thAgitatorMinMsgs        = 200
	thWildcardStdDev         = 0.5
	thWildcardMinClassified  = 150
	thHypeMachineExclPct     = 20
	thHypeMachinePosPct      = 60

	// Economy
	thBrokeSpiritedMaxBalance = 100.0
	thDegenerateMinLosses     = 10

	// Games
	thSharkWinRate          = 55
	thSharkMinGames         = 15
	thWordleMinPuzzles      = 10
	thArenaChampMinTier     = 4
	thArenaChampWinRate     = 50
	thArenaCowardMinRuns    = 3
	thArenaCowardMaxAvgTier = 2
	thTriviaNerdMinCorrect  = 10

	// Adventure
	thAdvMinDays            = 10
	thAdvDiverseMinDays     = 15
	thAdvDiverseMinTypes    = 3
	thAdvDiverseMaxShare    = 40
	thGearheadMinMasterwork = 3

	// Communication (vocabulary)
	thWordsmithMinFancyWords = 10

	// Social
	thPatronMinRepGiven     = 5
	thPatronRatioMultiplier = 2

	// Display
	maxDisplayArchetypes = 6
)

// ─── Flavor Text ─────���─────────────────────────────────────────────

var archetypeFlavors = map[string]string{
	// Communication
	"Novelist":   "Writes in paragraphs. Has opinions. Probably re-reads their own messages.",
	"Minimalist": "Says a lot with very little. You're never sure if they're fine or not.",
	"Inquisitor": "Always asking. Never satisfied with the first answer. Probably has follow-ups.",
	"Enthusiast": "Genuinely excited about things. All the things. Possibly all at once.",
	"Chatterbox": "Has thoughts. Many thoughts. Shares them all. You wouldn't have it any other way.",
	"Linkmaster": "The community's unofficial curator. Their tab count is not your business.",
	"Wordsmith":  "Uses words most people have to look up. The thesaurus fears them.",

	// Temporal
	"Night Owl":  "Awake when they probably shouldn't be. Thriving despite all evidence.",
	"Early Bird": "Up before anyone else. Has already formed opinions about the day.",

	// Emotional
	"Cheerleader":  "Lifts the room. Probably the reason someone didn't log off that one time.",
	"Philosopher":  "Thinks out loud. Asks questions that don't have comfortable answers.",
	"Agitator":     "Keeps things interesting. You don't always agree but you're never bored.",
	"Wildcard":     "You never quite know what you're getting. That's the charm. Mostly.",
	"Hype Machine": "Arrived and immediately made everything louder. The room is better for it.",

	// Economy
	"Whale":              "Has money. Spends money. Has more money somehow. The math is unclear.",
	"Degenerate":         "Knows exactly what they're doing. Does it anyway. Respects it.",
	"Broke But Spirited": "Down but not out. The pot fears them anyway.",

	// Games
	"Shark":          "Wins more than they should. GogoBee has opinions about this.",
	"Wordle Devotee": "Shows up every day. Rain or shine. Five letters at a time.",
	"Arena Champion": "Walks into the arena and the monsters check their insurance.",
	"Arena Coward":   "Shows up. Wins a little. Leaves immediately. Smart, actually.",
	"Trivia Nerd":    "Has retained an unreasonable amount of information.",

	// Adventure
	"Dungeon Crawler": "Always in the dungeon. The dungeon knows their name by now.",
	"The Miner":       "Steady. Patient. Has more ore than anyone needs. Still mining.",
	"The Forager":     "Out in the field while everyone else is underground. Knows where the good stuff is.",
	"The Angler":      "Patient beyond reason. Has caught things nobody else has caught.",
	"The Merchant":    "Buys. Sells. Optimizes. The economy is a puzzle and they're solving it.",
	"Resting Face":    "Technically participating. The numbers are still going up. Good enough.",
	"The Adventurer":  "Does a bit of everything. Refuses to specialize. Somehow works.",
	"Gearhead":        "Has the good stuff. Spent the time getting it. You can tell.",

	// Social
	"Patron":  "Gives credit generously. Asks for nothing in return. Suspicious, honestly.",
	"Reactor": "Expresses everything through emoji. An entire emotional life, no words required.",

	// Fallback
	"Regular": "Shows up. Participates. The backbone of any community, honestly.",
}

// ─��─ Mutual Exclusions ────��────────────────────────────────────────

var mutualExclusions = [][2]string{
	{"Novelist", "Minimalist"},
	{"Early Bird", "Night Owl"},
	{"Arena Champion", "Arena Coward"},
	{"Whale", "Broke But Spirited"},
}

// ─── Core Types ────────────���───────────────────────────────────────

type archetypeResult struct {
	Name        string
	Category    string
	SignalScore float64
	Flavor      string
}

// ─── Community Percentiles ──────────────────────���──────────────────

type communityPercentiles struct {
	whaleBalanceThreshold float64 // top 10%
	repGivenP75           int     // top 25% rep given
	reactionsGivenP75     int     // top 25% reactions given
	medianMsgCount        int     // median total_messages
}

func computePercentiles(d *sql.DB) communityPercentiles {
	var p communityPercentiles

	// Whale: top 10% balance
	var userCount int
	d.QueryRow(`SELECT COUNT(*) FROM euro_balances WHERE balance > 0`).Scan(&userCount)
	offset := userCount / 10
	if offset < 1 {
		offset = 1
	}
	d.QueryRow(`SELECT balance FROM euro_balances ORDER BY balance DESC LIMIT 1 OFFSET ?`, offset).Scan(&p.whaleBalanceThreshold)

	// Rep given: top 25%
	repGiven := queryIntList(d, `SELECT COUNT(*) FROM rep_cooldowns GROUP BY giver ORDER BY COUNT(*) DESC`)
	p.repGivenP75 = percentile75(repGiven)

	// Reactions given: top 25%
	reactionsGiven := queryIntList(d, `SELECT COUNT(*) FROM reaction_log GROUP BY sender ORDER BY COUNT(*) DESC`)
	p.reactionsGivenP75 = percentile75(reactionsGiven)

	// Median message count
	msgCounts := queryIntList(d, `SELECT total_messages FROM user_stats ORDER BY total_messages`)
	if len(msgCounts) > 0 {
		p.medianMsgCount = msgCounts[len(msgCounts)/2]
	}

	return p
}

func queryIntList(d *sql.DB, query string) []int {
	rows, err := d.Query(query)
	if err != nil {
		return nil
	}
	defer rows.Close()
	var result []int
	for rows.Next() {
		var v int
		if rows.Scan(&v) == nil {
			result = append(result, v)
		}
	}
	return result
}

func percentile75(sorted []int) int {
	if len(sorted) == 0 {
		return 0
	}
	idx := len(sorted) / 4 // top 25% = first quarter of desc-sorted list
	if idx >= len(sorted) {
		idx = len(sorted) - 1
	}
	return sorted[idx]
}

// ─── Per-User Data Loaders ──────────────────────��──────────────────

type userData struct {
	userID string

	// user_stats
	totalMsgs      int
	totalWords     int
	totalLinks     int
	totalImages    int
	totalQuestions int
	totalExcl      int
	totalEmojis    int
	nightMsgs      int
	morningMsgs    int
	fancyWords     int

	// sentiment_stats
	sentPositive int
	sentNegative int
	sentNeutral  int

	// llm_classifications
	classifiedCount int
	sentVariance    float64

	// economy
	balance        float64
	gamblingLosses int
	recentGaming   bool

	// games
	bjPlayed, bjWon             int
	hmPlayed, hmWon             int
	holdemPlayed                int
	holdemNetPositive           bool
	unoSoloPlayed, unoSoloWon   int
	unoMultiPlayed, unoMultiWon int
	wordlePlayed                int
	triviaCorrect               int

	// arena
	arenaHighestTier int
	arenaRuns        int
	arenaWins        int
	arenaAvgCashTier float64

	// adventure
	advDays         int
	advActivities   map[string]int // activity_type -> count
	masterworkCount int

	// social
	repGiven       int
	repReceived    int
	reactionsGiven int
}

func loadUserData(d *sql.DB, userID string) userData {
	u := userData{userID: userID, advActivities: make(map[string]int)}

	// user_stats
	d.QueryRow(`SELECT total_messages, total_words, total_links, total_images,
		total_questions, total_exclamations, total_emojis, night_messages, morning_messages,
		COALESCE(fancy_words, 0)
		FROM user_stats WHERE user_id = ?`, userID).Scan(
		&u.totalMsgs, &u.totalWords, &u.totalLinks, &u.totalImages,
		&u.totalQuestions, &u.totalExcl, &u.totalEmojis, &u.nightMsgs, &u.morningMsgs,
		&u.fancyWords)

	// sentiment_stats
	d.QueryRow(`SELECT COALESCE(positive,0), COALESCE(negative,0), COALESCE(neutral,0)
		FROM sentiment_stats WHERE user_id = ?`, userID).Scan(
		&u.sentPositive, &u.sentNegative, &u.sentNeutral)

	// llm_classifications count + variance
	d.QueryRow(`SELECT COUNT(*) FROM llm_classifications WHERE user_id = ?`, userID).Scan(&u.classifiedCount)
	if u.classifiedCount > 1 {
		d.QueryRow(`SELECT AVG(sentiment_score * sentiment_score) - AVG(sentiment_score) * AVG(sentiment_score)
			FROM llm_classifications WHERE user_id = ?`, userID).Scan(&u.sentVariance)
		if u.sentVariance < 0 {
			u.sentVariance = 0
		}
	}

	// economy
	d.QueryRow(`SELECT COALESCE(balance, 0) FROM euro_balances WHERE user_id = ?`, userID).Scan(&u.balance)

	// Count actual gambling losses (not bets/antes which are always negative)
	d.QueryRow(`SELECT COUNT(*) FROM euro_transactions WHERE user_id = ? AND amount < 0
		AND reason IN ('holdem_loss')`,
		userID).Scan(&u.gamblingLosses)
	// Add blackjack losses (games played minus games won)
	var bjLosses int
	d.QueryRow(`SELECT MAX(COALESCE(games_played,0) - COALESCE(games_won,0), 0) FROM blackjack_scores WHERE user_id = ?`,
		userID).Scan(&bjLosses)
	u.gamblingLosses += bjLosses

	var recentCount int
	d.QueryRow(`SELECT COUNT(*) FROM euro_transactions WHERE user_id = ?
		AND created_at > datetime('now', '-30 days')
		AND reason IN ('blackjack_bet','blackjack_win','holdem_loss','holdem_win',
		'uno_wager','uno_win','uno_multi_ante','uno_multi_win')`,
		userID).Scan(&recentCount)
	u.recentGaming = recentCount > 0

	// blackjack
	d.QueryRow(`SELECT COALESCE(games_played,0), COALESCE(games_won,0) FROM blackjack_scores WHERE user_id = ?`,
		userID).Scan(&u.bjPlayed, &u.bjWon)

	// hangman
	d.QueryRow(`SELECT COALESCE(games_played,0), COALESCE(games_won,0) FROM hangman_scores WHERE user_id = ?`,
		userID).Scan(&u.hmPlayed, &u.hmWon)

	// holdem
	var totalWon, totalLost int
	d.QueryRow(`SELECT COALESCE(hands_played,0), COALESCE(total_won,0), COALESCE(total_lost,0) FROM holdem_scores WHERE user_id = ?`,
		userID).Scan(&u.holdemPlayed, &totalWon, &totalLost)
	u.holdemNetPositive = totalWon > totalLost

	// uno solo
	d.QueryRow(`SELECT COUNT(*), COALESCE(SUM(CASE WHEN result='player_win' THEN 1 ELSE 0 END), 0) FROM uno_games WHERE player_id = ?`,
		userID).Scan(&u.unoSoloPlayed, &u.unoSoloWon)

	// uno multi
	d.QueryRow(`SELECT COUNT(*) FROM uno_multi_games WHERE player_ids LIKE ?`,
		"%"+userID+"%").Scan(&u.unoMultiPlayed)
	d.QueryRow(`SELECT COUNT(*) FROM uno_multi_games WHERE winner_id = ?`,
		userID).Scan(&u.unoMultiWon)

	// wordle
	d.QueryRow(`SELECT COALESCE(puzzles_played,0) FROM wordle_stats WHERE user_id = ?`,
		userID).Scan(&u.wordlePlayed)

	// trivia
	d.QueryRow(`SELECT COALESCE(SUM(correct),0) FROM trivia_scores WHERE user_id = ?`,
		userID).Scan(&u.triviaCorrect)

	// arena
	d.QueryRow(`SELECT COALESCE(highest_tier,0), COALESCE(total_runs,0) FROM arena_stats WHERE user_id = ?`,
		userID).Scan(&u.arenaHighestTier, &u.arenaRuns)

	d.QueryRow(`SELECT COUNT(*) FROM arena_runs WHERE user_id = ? AND status = 'cashed_out'`,
		userID).Scan(&u.arenaWins)

	d.QueryRow(`SELECT COALESCE(AVG(tier), 0) FROM arena_runs WHERE user_id = ? AND status = 'cashed_out'`,
		userID).Scan(&u.arenaAvgCashTier)

	// adventure
	d.QueryRow(`SELECT COUNT(DISTINCT DATE(logged_at)) FROM adventure_activity_log WHERE user_id = ?`,
		userID).Scan(&u.advDays)

	rows, err := d.Query(`SELECT activity_type, COUNT(*) FROM adventure_activity_log WHERE user_id = ? GROUP BY activity_type`, userID)
	if err == nil {
		defer rows.Close()
		for rows.Next() {
			var atype string
			var cnt int
			if rows.Scan(&atype, &cnt) == nil {
				u.advActivities[atype] = cnt
			}
		}
	}

	d.QueryRow(`SELECT COUNT(*) FROM adventure_equipment WHERE user_id = ? AND masterwork = 1`,
		userID).Scan(&u.masterworkCount)

	// social
	d.QueryRow(`SELECT COUNT(*) FROM rep_cooldowns WHERE giver = ?`, userID).Scan(&u.repGiven)
	d.QueryRow(`SELECT COUNT(*) FROM rep_cooldowns WHERE receiver = ?`, userID).Scan(&u.repReceived)
	d.QueryRow(`SELECT COUNT(*) FROM reaction_log WHERE sender = ?`, userID).Scan(&u.reactionsGiven)

	return u
}

// ���── Archetype Evaluators ────────���─────────────────────────────────

func evaluateArchetypes(u userData, pct communityPercentiles) []archetypeResult {
	var results []archetypeResult

	m := max1arch(u.totalMsgs)

	// ── Communication Style ──

	avgWords := u.totalWords / m
	if u.totalMsgs >= thNovelistMinMsgs && avgWords >= thNovelistAvgWords {
		results = append(results, archetypeResult{
			Name: "Novelist", Category: "Communication",
			SignalScore: clampSignal(float64(avgWords-thNovelistAvgWords) / float64(thNovelistAvgWords)),
		})
	}

	if u.totalMsgs >= thMinimalistMinMsgs && avgWords <= thMinimalistMaxAvgWords {
		results = append(results, archetypeResult{
			Name: "Minimalist", Category: "Communication",
			SignalScore: clampSignal(float64(thMinimalistMaxAvgWords-avgWords+1) / float64(thMinimalistMaxAvgWords)),
		})
	}

	qPct := u.totalQuestions * 100 / m
	if u.totalMsgs >= thInquisitorMinMsgs && qPct >= thInquisitorPct {
		results = append(results, archetypeResult{
			Name: "Inquisitor", Category: "Communication",
			SignalScore: clampSignal(float64(qPct-thInquisitorPct) / float64(thInquisitorPct)),
		})
	}

	ePct := u.totalExcl * 100 / m
	if u.totalMsgs >= thEnthusiastMinMsgs && ePct >= thEnthusiastPct {
		results = append(results, archetypeResult{
			Name: "Enthusiast", Category: "Communication",
			SignalScore: clampSignal(float64(ePct-thEnthusiastPct) / float64(thEnthusiastPct)),
		})
	}

	if u.totalMsgs >= thChatterboxMinMsgs && avgWords >= thChatterboxMinAvgWords {
		results = append(results, archetypeResult{
			Name: "Chatterbox", Category: "Communication",
			SignalScore: clampSignal(float64(u.totalMsgs-thChatterboxMinMsgs) / float64(thChatterboxMinMsgs)),
		})
	}

	lPct := u.totalLinks * 100 / m
	if u.totalMsgs >= thLinkmasterMinMsgs && lPct >= thLinkmasterPct {
		results = append(results, archetypeResult{
			Name: "Linkmaster", Category: "Communication",
			SignalScore: clampSignal(float64(lPct-thLinkmasterPct) / float64(thLinkmasterPct)),
		})
	}

	if u.fancyWords >= thWordsmithMinFancyWords {
		results = append(results, archetypeResult{
			Name: "Wordsmith", Category: "Communication",
			SignalScore: clampSignal(float64(u.fancyWords) / float64(thWordsmithMinFancyWords*3)),
		})
	}

	// ── Temporal ──

	if u.totalMsgs >= thNightOwlMinMsgs {
		nightPct := u.nightMsgs * 100 / m
		if nightPct >= thNightOwlPct {
			results = append(results, archetypeResult{
				Name: "Night Owl", Category: "Temporal",
				SignalScore: clampSignal(float64(nightPct-thNightOwlPct) / float64(100-thNightOwlPct)),
			})
		}
	}

	if u.totalMsgs >= thEarlyBirdMinMsgs {
		morningPct := u.morningMsgs * 100 / m
		if morningPct >= thEarlyBirdPct {
			results = append(results, archetypeResult{
				Name: "Early Bird", Category: "Temporal",
				SignalScore: clampSignal(float64(morningPct-thEarlyBirdPct) / float64(100-thEarlyBirdPct)),
			})
		}
	}

	// ── Emotional Signature (LLM-gated) ──

	sentTotal := u.sentPositive + u.sentNegative + u.sentNeutral
	if u.classifiedCount >= thEmotionalMinClassified && sentTotal > 0 {
		posPct := u.sentPositive * 100 / sentTotal
		negPct := u.sentNegative * 100 / sentTotal
		neutPct := u.sentNeutral * 100 / sentTotal

		// Cheerleader
		if posPct >= thCheerleaderPosPct && u.repGiven >= pct.repGivenP75 && u.reactionsGiven >= pct.reactionsGivenP75 {
			results = append(results, archetypeResult{
				Name: "Cheerleader", Category: "Emotional",
				SignalScore: clampSignal(float64(posPct-thCheerleaderPosPct) / float64(100-thCheerleaderPosPct)),
			})
		}

		// Philosopher
		if neutPct >= thPhilosopherNeutPct && qPct >= thPhilosopherQPct && avgWords >= thPhilosopherAvgWords {
			results = append(results, archetypeResult{
				Name: "Philosopher", Category: "Emotional",
				SignalScore: clampSignal(float64(neutPct-thPhilosopherNeutPct) / float64(100-thPhilosopherNeutPct)),
			})
		}

		// Agitator
		if negPct >= thAgitatorNegPct && u.totalMsgs >= thAgitatorMinMsgs {
			results = append(results, archetypeResult{
				Name: "Agitator", Category: "Emotional",
				SignalScore: clampSignal(float64(negPct-thAgitatorNegPct) / float64(100-thAgitatorNegPct)),
			})
		}

		// Hype Machine
		if ePct >= thHypeMachineExclPct && posPct >= thHypeMachinePosPct && u.reactionsGiven >= pct.reactionsGivenP75 {
			results = append(results, archetypeResult{
				Name: "Hype Machine", Category: "Emotional",
				SignalScore: clampSignal((float64(ePct-thHypeMachineExclPct)/float64(thHypeMachineExclPct) +
					float64(posPct-thHypeMachinePosPct)/float64(100-thHypeMachinePosPct)) / 2),
			})
		}
	}

	// Wildcard (separate min classified threshold)
	if u.classifiedCount >= thWildcardMinClassified {
		stddev := math.Sqrt(u.sentVariance)
		if stddev >= thWildcardStdDev {
			results = append(results, archetypeResult{
				Name: "Wildcard", Category: "Emotional",
				SignalScore: clampSignal((stddev - thWildcardStdDev) / thWildcardStdDev),
			})
		}
	}

	// ── Economy ──

	if pct.whaleBalanceThreshold > 0 && u.balance >= pct.whaleBalanceThreshold {
		results = append(results, archetypeResult{
			Name: "Whale", Category: "Economy",
			SignalScore: clampSignal(u.balance / pct.whaleBalanceThreshold / 2),
		})
	}

	if u.gamblingLosses >= thDegenerateMinLosses && u.recentGaming {
		results = append(results, archetypeResult{
			Name: "Degenerate", Category: "Economy",
			SignalScore: clampSignal(float64(u.gamblingLosses) / float64(thDegenerateMinLosses*3)),
		})
	}

	if u.balance < thBrokeSpiritedMaxBalance && u.recentGaming {
		results = append(results, archetypeResult{
			Name: "Broke But Spirited", Category: "Economy",
			SignalScore: clampSignal((thBrokeSpiritedMaxBalance - u.balance) / thBrokeSpiritedMaxBalance),
		})
	}

	// ── Games ──

	totalGames := u.bjPlayed + u.hmPlayed + u.unoSoloPlayed + u.unoMultiPlayed
	totalWins := u.bjWon + u.hmWon + u.unoSoloWon + u.unoMultiWon
	if u.holdemPlayed > 0 {
		totalGames += u.holdemPlayed
		if u.holdemNetPositive {
			totalWins += u.holdemPlayed / 2 // approximate: net positive counts as ~50% wins
		}
	}
	if totalGames >= thSharkMinGames {
		winRate := totalWins * 100 / totalGames
		if winRate >= thSharkWinRate {
			results = append(results, archetypeResult{
				Name: "Shark", Category: "Games",
				SignalScore: clampSignal(float64(winRate-thSharkWinRate) / float64(100-thSharkWinRate)),
			})
		}
	}

	if u.wordlePlayed >= thWordleMinPuzzles {
		results = append(results, archetypeResult{
			Name: "Wordle Devotee", Category: "Games",
			SignalScore: clampSignal(float64(u.wordlePlayed-thWordleMinPuzzles) / float64(thWordleMinPuzzles)),
		})
	}

	if u.arenaHighestTier >= thArenaChampMinTier && u.arenaRuns > 0 {
		arenaWinRate := u.arenaWins * 100 / max1arch(u.arenaRuns)
		if arenaWinRate >= thArenaChampWinRate {
			results = append(results, archetypeResult{
				Name: "Arena Champion", Category: "Games",
				SignalScore: clampSignal(float64(u.arenaHighestTier-thArenaChampMinTier+1) / 2.0),
			})
		}
	}

	if u.arenaWins >= thArenaCowardMinRuns && u.arenaAvgCashTier > 0 && u.arenaAvgCashTier <= float64(thArenaCowardMaxAvgTier) {
		results = append(results, archetypeResult{
			Name: "Arena Coward", Category: "Games",
			SignalScore: clampSignal((float64(thArenaCowardMaxAvgTier) - u.arenaAvgCashTier + 1) / float64(thArenaCowardMaxAvgTier)),
		})
	}

	if u.triviaCorrect >= thTriviaNerdMinCorrect {
		results = append(results, archetypeResult{
			Name: "Trivia Nerd", Category: "Games",
			SignalScore: clampSignal(float64(u.triviaCorrect-thTriviaNerdMinCorrect) / float64(thTriviaNerdMinCorrect)),
		})
	}

	// ── Adventure ──

	if u.advDays >= thAdvMinDays {
		totalAdv := 0
		for _, c := range u.advActivities {
			totalAdv += c
		}
		if totalAdv > 0 {
			// Find plurality activity
			var topActivity string
			var topCount int
			for act, cnt := range u.advActivities {
				if cnt > topCount {
					topActivity = act
					topCount = cnt
				}
			}
			topShare := topCount * 100 / totalAdv

			activityArchetypes := map[string]string{
				"dungeon":  "Dungeon Crawler",
				"mining":   "The Miner",
				"foraging": "The Forager",
				"fishing":  "The Angler",
				"shop":     "The Merchant",
				"rest":     "Resting Face",
			}

			// Check for diverse adventurer first
			if len(u.advActivities) >= thAdvDiverseMinTypes && u.advDays >= thAdvDiverseMinDays && topShare <= thAdvDiverseMaxShare {
				results = append(results, archetypeResult{
					Name: "The Adventurer", Category: "Adventure",
					SignalScore: clampSignal(float64(len(u.advActivities)) / float64(thAdvDiverseMinTypes+2)),
				})
			} else if name, ok := activityArchetypes[topActivity]; ok {
				results = append(results, archetypeResult{
					Name: name, Category: "Adventure",
					SignalScore: clampSignal(float64(topShare) / 100.0),
				})
			}
		}
	}

	if u.masterworkCount >= thGearheadMinMasterwork {
		results = append(results, archetypeResult{
			Name: "Gearhead", Category: "Adventure",
			SignalScore: clampSignal(float64(u.masterworkCount) / float64(thGearheadMinMasterwork*2)),
		})
	}

	// ── Social ──

	if u.repGiven >= thPatronMinRepGiven {
		repReceived := max1arch(u.repReceived)
		if u.repGiven >= repReceived*thPatronRatioMultiplier {
			results = append(results, archetypeResult{
				Name: "Patron", Category: "Social",
				SignalScore: clampSignal(float64(u.repGiven) / float64(thPatronMinRepGiven*3)),
			})
		}
	}

	if u.reactionsGiven >= pct.reactionsGivenP75 && pct.reactionsGivenP75 > 0 && u.totalMsgs <= pct.medianMsgCount {
		results = append(results, archetypeResult{
			Name: "Reactor", Category: "Social",
			SignalScore: clampSignal(float64(u.reactionsGiven) / float64(pct.reactionsGivenP75*2)),
		})
	}

	// Fill in flavor text
	for i := range results {
		results[i].Flavor = archetypeFlavors[results[i].Name]
	}

	// Apply mutual exclusions
	results = applyExclusions(results)

	// Sort by signal score desc
	sort.Slice(results, func(i, j int) bool {
		return results[i].SignalScore > results[j].SignalScore
	})

	return results
}

func applyExclusions(results []archetypeResult) []archetypeResult {
	nameIdx := make(map[string]int)
	for i, r := range results {
		nameIdx[r.Name] = i
	}

	remove := make(map[int]bool)
	for _, pair := range mutualExclusions {
		idxA, hasA := nameIdx[pair[0]]
		idxB, hasB := nameIdx[pair[1]]
		if hasA && hasB {
			if results[idxA].SignalScore >= results[idxB].SignalScore {
				remove[idxB] = true
			} else {
				remove[idxA] = true
			}
		}
	}

	if len(remove) == 0 {
		return results
	}

	var filtered []archetypeResult
	for i, r := range results {
		if !remove[i] {
			filtered = append(filtered, r)
		}
	}
	return filtered
}

// ─── Cache Read/Write ────────��─────────────────────────────────────

// GetUserArchetypes returns cached archetypes for a user, sorted by signal_score desc.
func GetUserArchetypes(userID string) []archetypeResult {
	d := db.Get()
	rows, err := d.Query(`SELECT archetype, category, signal_score, flavor
		FROM user_archetypes WHERE user_id = ? ORDER BY signal_score DESC`, userID)
	if err != nil {
		return nil
	}
	defer rows.Close()

	var results []archetypeResult
	for rows.Next() {
		var r archetypeResult
		if rows.Scan(&r.Name, &r.Category, &r.SignalScore, &r.Flavor) == nil {
			results = append(results, r)
		}
	}
	return results
}

// GetUserArchetypesLimited returns up to maxDisplayArchetypes for profile cards.
func GetUserArchetypesLimited(userID string) []archetypeResult {
	results := GetUserArchetypes(userID)
	if len(results) > maxDisplayArchetypes {
		results = results[:maxDisplayArchetypes]
	}
	if len(results) == 0 {
		return []archetypeResult{{
			Name: "Regular", Category: "Fallback",
			Flavor: archetypeFlavors["Regular"],
		}}
	}
	return results
}

// ─── Refresh Engine ─────────────────────��──────────────────────────

// RefreshAllArchetypes recalculates archetypes for all users and writes to user_archetypes.
func RefreshAllArchetypes() {
	d := db.Get()

	// Get all user IDs
	rows, err := d.Query(`SELECT user_id FROM user_stats WHERE total_messages > 0`)
	if err != nil {
		slog.Error("archetype: failed to list users", "err", err)
		return
	}
	var userIDs []string
	for rows.Next() {
		var uid string
		if rows.Scan(&uid) == nil {
			userIDs = append(userIDs, uid)
		}
	}
	rows.Close()

	if len(userIDs) == 0 {
		return
	}

	// Compute community percentiles once
	pct := computePercentiles(d)

	// Evaluate each user
	type userResult struct {
		userID string
		archs  []archetypeResult
	}
	var allResults []userResult

	for _, uid := range userIDs {
		u := loadUserData(d, uid)
		archs := evaluateArchetypes(u, pct)
		if len(archs) > 0 {
			allResults = append(allResults, userResult{uid, archs})
		}
	}

	// Write to cache in a transaction
	tx, err := d.Begin()
	if err != nil {
		slog.Error("archetype: failed to begin tx", "err", err)
		return
	}

	_, err = tx.Exec(`DELETE FROM user_archetypes`)
	if err != nil {
		tx.Rollback()
		slog.Error("archetype: failed to clear cache", "err", err)
		return
	}

	stmt, err := tx.Prepare(`INSERT INTO user_archetypes (user_id, archetype, category, signal_score, flavor)
		VALUES (?, ?, ?, ?, ?)`)
	if err != nil {
		tx.Rollback()
		slog.Error("archetype: failed to prepare insert", "err", err)
		return
	}
	defer stmt.Close()

	totalAssigned := 0
	for _, ur := range allResults {
		for _, a := range ur.archs {
			if _, err := stmt.Exec(ur.userID, a.Name, a.Category, a.SignalScore, a.Flavor); err != nil {
				slog.Error("archetype: insert failed", "user", ur.userID, "arch", a.Name, "err", err)
			}
			totalAssigned++
		}
	}

	if err := tx.Commit(); err != nil {
		slog.Error("archetype: commit failed", "err", err)
		return
	}

	slog.Info("archetype: refresh complete", "users", len(allResults), "archetypes_assigned", totalAssigned)
}

// ─── Helpers ─────────���─────────────────────────────────────────────

func max1arch(n int) int {
	if n < 1 {
		return 1
	}
	return n
}

func clampSignal(v float64) float64 {
	if v < 0 {
		return 0
	}
	if v > 1 {
		return 1
	}
	return v
}

// FormatArchetypeNames returns a " · " separated list of archetype names.
func FormatArchetypeNames(archs []archetypeResult) string {
	if len(archs) == 0 {
		return "Regular"
	}
	names := make([]string, len(archs))
	for i, a := range archs {
		names[i] = a.Name
	}
	return strings.Join(names, " · ")
}

// FormatArchetypesFull returns a full display with names and flavor text.
func FormatArchetypesFull(archs []archetypeResult) string {
	if len(archs) == 0 {
		return fmt.Sprintf("**Regular**\n_%s_", archetypeFlavors["Regular"])
	}
	var sb strings.Builder
	for i, a := range archs {
		if i > 0 {
			sb.WriteString("\n")
		}
		sb.WriteString(fmt.Sprintf("**%s**\n_%s_", a.Name, a.Flavor))
	}
	return sb.String()
}
