package plugin

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"math/rand"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
	"unicode"

	"gogobee/internal/db"
	"gogobee/internal/dreamclient"

	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/id"
)

// profanityKeywords is a basic list used for pre-filtering.
var profanityKeywords = []string{
	"fuck", "shit", "damn", "hell", "ass", "bitch", "crap", "dick",
	"bastard", "piss", "cock", "cunt", "douche", "dumbass", "idiot",
	"moron", "stupid", "stfu", "wtf", "lmao", "lmfao",
}

var thanksPatternRe = regexp.MustCompile(`(?i)\b(thanks|thank\s+you|thankyou|thx|ty|tysm|tyvm|appreciate)\b`)
var mentionRe = regexp.MustCompile(`@[a-zA-Z0-9._=-]+:[a-zA-Z0-9.-]+`)

// classificationResult holds the parsed LLM JSON response.
type classificationResult struct {
	Sentiment         string   `json:"sentiment"`
	SentimentScore    float64  `json:"sentiment_score"`
	Topics            []string `json:"topics"`
	Profanity         bool     `json:"profanity"`
	ProfanitySeverity int      `json:"profanity_severity"`
	InsultTarget      string   `json:"insult_target"`
	WOTDUsed          bool     `json:"wotd_used"`
	GratitudeTarget   string   `json:"gratitude_target"`
}

// queueItem holds a message pending classification.
type queueItem struct {
	UserID        id.UserID
	RoomID        id.RoomID
	EventID       id.EventID
	Body          string
	FormattedBody string
}

// LLMPassivePlugin classifies messages using Ollama and reacts accordingly.
type LLMPassivePlugin struct {
	Base
	xp          *XPPlugin
	dict        *dreamclient.Client
	ollamaHost  string
	ollamaModel string
	sampleRate  float64
	enabled     bool

	mu      sync.Mutex
	queue   []queueItem
	backoff time.Duration

	httpClient *http.Client
	stopCh     chan struct{}
}

// NewLLMPassivePlugin creates a new LLM passive classification plugin.
func NewLLMPassivePlugin(client *mautrix.Client, xp *XPPlugin, dict *dreamclient.Client) *LLMPassivePlugin {
	host := os.Getenv("OLLAMA_HOST")
	model := os.Getenv("OLLAMA_MODEL")
	enabled := host != "" && model != ""

	sampleRate := 0.15
	if v := os.Getenv("LLM_SAMPLE_RATE"); v != "" {
		if parsed, err := strconv.ParseFloat(v, 64); err == nil && parsed >= 0 && parsed <= 1 {
			sampleRate = parsed
		}
	}

	p := &LLMPassivePlugin{
		Base:        NewBase(client),
		xp:          xp,
		dict:        dict,
		ollamaHost:  host,
		ollamaModel: model,
		sampleRate:  sampleRate,
		enabled:     enabled,
		backoff:     5 * time.Second,
		httpClient:  &http.Client{Timeout: 30 * time.Second},
		stopCh:      make(chan struct{}),
	}

	return p
}

func (p *LLMPassivePlugin) Name() string { return "llm_passive" }

func (p *LLMPassivePlugin) Commands() []CommandDef {
	return []CommandDef{
		{Name: "potty", Description: "Show profanity count for a user", Usage: "!potty [@user]", Category: "LLM & Sentiment"},
		{Name: "pottyboard", Description: "Top 10 most profane users", Usage: "!pottyboard", Category: "LLM & Sentiment"},
		{Name: "insults", Description: "Show insult stats for a user", Usage: "!insults [@user]", Category: "LLM & Sentiment"},
		{Name: "insultboard", Description: "Top 10 most insulted users", Usage: "!insultboard", Category: "LLM & Sentiment"},
		{Name: "sentiment", Description: "Show sentiment stats for a user", Usage: "!sentiment [@user]", Category: "LLM & Sentiment"},
		{Name: "roomsentiment", Description: "Show sentiment breakdown for this room", Usage: "!roomsentiment", Category: "LLM & Sentiment"},
	}
}

func (p *LLMPassivePlugin) Init() error {
	if p.enabled {
		slog.Info("llm_passive: enabled", "host", p.ollamaHost, "model", p.ollamaModel, "sample_rate", p.sampleRate)
		go p.processQueue()
	} else {
		slog.Warn("llm_passive: disabled (OLLAMA_HOST or OLLAMA_MODEL not set)",
			"host", p.ollamaHost, "model", p.ollamaModel)
	}
	return nil
}

func (p *LLMPassivePlugin) OnReaction(_ ReactionContext) error { return nil }

func (p *LLMPassivePlugin) OnMessage(ctx MessageContext) error {
	// Handle commands
	if p.IsCommand(ctx.Body, "potty") {
		return p.handlePotty(ctx)
	}
	if p.IsCommand(ctx.Body, "pottyboard") {
		return p.handlePottyboard(ctx)
	}
	if p.IsCommand(ctx.Body, "insults") {
		return p.handleInsults(ctx)
	}
	if p.IsCommand(ctx.Body, "insultboard") {
		return p.handleInsultboard(ctx)
	}
	if p.IsCommand(ctx.Body, "roomsentiment") {
		return p.handleRoomSentiment(ctx)
	}
	if p.IsCommand(ctx.Body, "sentiment") {
		return p.handleSentiment(ctx)
	}

	if !p.enabled {
		return nil
	}

	// Skip command messages
	if ctx.IsCommand {
		return nil
	}

	// Check for fancy words on every message (independent of LLM sampling)
	if p.dict != nil {
		safeGo("llm-fancy-word", func() {
			if p.hasFancyWord(ctx.Body) {
				_ = p.SendReact(ctx.RoomID, ctx.EventID, "\U0001f393") // 🎓 graduation cap
				db.Exec("llm: track fancy word",
					`UPDATE user_stats SET fancy_words = fancy_words + 1 WHERE user_id = ?`,
					string(ctx.Sender))
			}
		})
	}

	// Pre-filter: only classify messages that match certain criteria
	var fmtBody string
	if ctx.Event != nil {
		if mc := ctx.Event.Content.AsMessage(); mc != nil {
			fmtBody = mc.FormattedBody
		}
	}
	if !p.shouldClassify(ctx.Body, fmtBody) {
		slog.Debug("llm_passive: message did not pass pre-filter", "body_len", len(ctx.Body))
		return nil
	}

	// Enqueue for async classification
	slog.Debug("llm_passive: enqueuing message for classification", "user", ctx.Sender, "body_len", len(ctx.Body))
	var formattedBody string
	if ctx.Event != nil {
		if mc := ctx.Event.Content.AsMessage(); mc != nil {
			formattedBody = mc.FormattedBody
		}
	}
	p.mu.Lock()
	p.queue = append(p.queue, queueItem{
		UserID:        ctx.Sender,
		RoomID:        ctx.RoomID,
		EventID:       ctx.EventID,
		Body:          ctx.Body,
		FormattedBody: formattedBody,
	})
	p.mu.Unlock()

	return nil
}

// shouldClassify applies pre-filtering heuristics.
func (p *LLMPassivePlugin) shouldClassify(body, formattedBody string) bool {
	// Skip single-character messages (trivia answers, etc.)
	if len(strings.TrimSpace(body)) <= 1 {
		return false
	}

	lower := strings.ToLower(body)

	// Check profanity keywords
	for _, kw := range profanityKeywords {
		if strings.Contains(lower, kw) {
			return true
		}
	}

	// Check mentions (plain text or HTML formatted body)
	if mentionRe.MatchString(body) {
		return true
	}
	if strings.Contains(formattedBody, "matrix.to/#/@") {
		return true
	}

	// Check WOTD pattern (simple heuristic)
	if strings.Contains(lower, "wotd") {
		return true
	}

	// Check non-ASCII
	for _, r := range body {
		if r > unicode.MaxASCII {
			return true
		}
	}

	// Check thanks patterns
	if thanksPatternRe.MatchString(body) {
		return true
	}

	// Random sample of remaining messages (controlled by LLM_SAMPLE_RATE)
	if p.sampleRate >= 1.0 || rand.Float64() < p.sampleRate {
		return true
	}

	return false
}

// processQueue processes the classification queue with backoff.
func (p *LLMPassivePlugin) processQueue() {
	for {
		select {
		case <-p.stopCh:
			return
		default:
		}

		p.mu.Lock()
		if len(p.queue) == 0 {
			p.mu.Unlock()
			time.Sleep(1 * time.Second)
			continue
		}
		item := p.queue[0]
		p.queue = p.queue[1:]
		p.mu.Unlock()

		slog.Debug("llm_passive: processing queued item", "user", item.UserID, "body_preview", truncate(item.Body, 50))
		err := p.classifyAndProcess(item)
		if err != nil {
			slog.Error("llm: classification failed", "err", err, "user", item.UserID)
			// Backoff on error
			p.mu.Lock()
			p.backoff *= 2
			if p.backoff > 5*time.Minute {
				p.backoff = 5 * time.Minute
			}
			p.mu.Unlock()
			time.Sleep(p.backoff)
		} else {
			// Reset backoff on success
			p.mu.Lock()
			p.backoff = 5 * time.Second
			p.mu.Unlock()
		}
	}
}

// extractMentionMap builds a display-name-to-MXID mapping from the HTML formatted body.
func extractMentionMap(formattedBody string) map[string]string {
	if formattedBody == "" {
		return nil
	}
	// Match: <a href="https://matrix.to/#/@user:server">Display Name</a>
	re := regexp.MustCompile(`<a\s+href="https://matrix\.to/#/(@[^"]+)">([^<]+)</a>`)
	matches := re.FindAllStringSubmatch(formattedBody, -1)
	if len(matches) == 0 {
		return nil
	}
	result := make(map[string]string)
	for _, m := range matches {
		if len(m) >= 3 {
			result[m[2]] = m[1] // display name -> MXID
		}
	}
	return result
}

// classifyAndProcess sends a message to Ollama and processes the result.
func (p *LLMPassivePlugin) classifyAndProcess(item queueItem) error {
	// Build mention context so the LLM knows actual MXIDs
	mentionMap := extractMentionMap(item.FormattedBody)
	var mentionHint string
	if len(mentionMap) > 0 {
		var parts []string
		for displayName, mxid := range mentionMap {
			parts = append(parts, fmt.Sprintf("%s = %s", displayName, mxid))
		}
		mentionHint = "\nMentioned users: " + strings.Join(parts, ", ")
	}

	// Fetch today's WOTD so the LLM can check for correct usage
	today := time.Now().UTC().Format("2006-01-02")
	var todayWOTD string
	db.Get().QueryRow(`SELECT word FROM wotd_log WHERE date = ?`, today).Scan(&todayWOTD)

	result, err := p.callOllama(item.Body+mentionHint, todayWOTD)
	if err != nil {
		return fmt.Errorf("ollama call: %w", err)
	}

	// Resolve any display names in LLM targets back to MXIDs
	if result.InsultTarget != "" && !strings.Contains(result.InsultTarget, ":") {
		if mxid, ok := mentionMap[result.InsultTarget]; ok {
			result.InsultTarget = mxid
		}
	}
	if result.GratitudeTarget != "" && !strings.Contains(result.GratitudeTarget, ":") {
		if mxid, ok := mentionMap[result.GratitudeTarget]; ok {
			result.GratitudeTarget = mxid
		}
	}

	// Store classification
	topicsJSON, _ := json.Marshal(result.Topics)
	profanityInt := 0
	if result.Profanity {
		profanityInt = 1
	}
	wotdInt := 0
	if result.WOTDUsed {
		wotdInt = 1
	}

	db.Exec("llm: store classification",
		`INSERT INTO llm_classifications (user_id, room_id, message_text, sentiment, sentiment_score, topics, profanity, profanity_severity, insult_target, wotd_used, gratitude_target)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		string(item.UserID), string(item.RoomID), item.Body,
		result.Sentiment, result.SentimentScore, string(topicsJSON),
		profanityInt, result.ProfanitySeverity, result.InsultTarget, wotdInt, result.GratitudeTarget,
	)

	// Aggregate sentiment stats
	sentimentCol := "neutral"
	validSentiments := map[string]bool{
		"positive": true, "negative": true, "neutral": true,
		"excited": true, "sarcastic": true, "frustrated": true,
		"curious": true, "grateful": true, "humorous": true, "supportive": true,
	}
	if validSentiments[result.Sentiment] {
		sentimentCol = result.Sentiment
	}
	db.Exec("llm: update user sentiment stats",
		fmt.Sprintf(
			`INSERT INTO sentiment_stats (user_id, %s, total_score) VALUES (?, 1, ?)
			 ON CONFLICT(user_id) DO UPDATE SET %s = %s + 1, total_score = total_score + ?`,
			sentimentCol, sentimentCol, sentimentCol),
		string(item.UserID), result.SentimentScore, result.SentimentScore,
	)

	// Aggregate room sentiment stats
	db.Exec("llm: update room sentiment stats",
		fmt.Sprintf(
			`INSERT INTO room_sentiment_stats (room_id, %s, total_score) VALUES (?, 1, ?)
			 ON CONFLICT(room_id) DO UPDATE SET %s = %s + 1, total_score = total_score + ?`,
			sentimentCol, sentimentCol, sentimentCol),
		string(item.RoomID), result.SentimentScore, result.SentimentScore,
	)

	// Track profanity with severity
	if result.Profanity {
		severity := result.ProfanitySeverity
		if severity < 1 {
			severity = 1
		}
		if severity > 3 {
			severity = 3
		}
		mildInc, modInc, scorchInc := 0, 0, 0
		switch severity {
		case 1:
			mildInc = 1
		case 2:
			modInc = 1
		case 3:
			scorchInc = 1
		}
		db.Exec("llm: track profanity",
			`INSERT INTO potty_mouth (user_id, count, mild, moderate, scorching) VALUES (?, 1, ?, ?, ?)
			 ON CONFLICT(user_id) DO UPDATE SET count = count + 1, mild = mild + ?, moderate = moderate + ?, scorching = scorching + ?`,
			string(item.UserID), mildInc, modInc, scorchInc, mildInc, modInc, scorchInc,
		)
	}

	// React if someone insults the bot
	if result.InsultTarget == string(p.Client.UserID) {
		botReactions := []string{
			"\U0001f595", // 🖕
			"\U0001f52a", // 🔪
			"\U0001f5e1", // 🗡️
			"\U0001fa78", // 🩸
			"\U0001f4a3", // 💣
		}
		emoji := botReactions[rand.Intn(len(botReactions))]
		_ = p.SendReact(item.RoomID, item.EventID, emoji)
	}

	// Track insults
	if result.InsultTarget != "" {
		db.Exec("llm: track insult sender",
			`INSERT INTO insult_log (user_id, times_insulting) VALUES (?, 1)
			 ON CONFLICT(user_id) DO UPDATE SET times_insulting = times_insulting + 1`,
			string(item.UserID),
		)
		db.Exec("llm: track insult target",
			`INSERT INTO insult_log (user_id, times_insulted) VALUES (?, 1)
			 ON CONFLICT(user_id) DO UPDATE SET times_insulted = times_insulted + 1`,
			result.InsultTarget,
		)
	}

	// Sentiment/profanity emoji reactions disabled — logging continues above.
	// Insult-response reactions (bot reacts when insulted) remain active above.
	if result.WOTDUsed {
		_ = p.SendReact(item.RoomID, item.EventID, "\U0001f4d6") // 📖 open book
	}
	if result.GratitudeTarget != "" {
		_ = p.SendReact(item.RoomID, item.EventID, "\U0001f49c") // 💜 purple heart
	}

	// Grant XP for gratitude
	if result.GratitudeTarget != "" && p.xp != nil {
		targetID := id.UserID(result.GratitudeTarget)
		if targetID != item.UserID {
			p.xp.GrantXP(targetID, 5, "gratitude")
		}
	}

	return nil
}

// ollamaRequest is the request body for the Ollama API.
type ollamaRequest struct {
	Model  string `json:"model"`
	Prompt string `json:"prompt"`
	Stream bool   `json:"stream"`
	Think  bool   `json:"think"`
}

// ollamaResponse is the response from the Ollama API.
type ollamaResponse struct {
	Response string `json:"response"`
}

// callOllama sends a classification prompt to Ollama and parses the JSON result.
func (p *LLMPassivePlugin) callOllama(messageText, wotd string) (*classificationResult, error) {
	wotdInstruction := `"wotd_used": false`
	if wotd != "" {
		wotdInstruction = fmt.Sprintf(`"wotd_used": true | false (whether the message uses the word "%s" correctly and meaningfully — not just mentioning or quoting it)`, wotd)
	}

	prompt := fmt.Sprintf(`Classify the following chat message. Respond ONLY with valid JSON (no markdown, no explanation).

JSON schema:
{
  "sentiment": "positive" | "negative" | "neutral" | "excited" | "sarcastic" | "frustrated" | "curious" | "grateful" | "humorous" | "supportive",
  "sentiment_score": number between -1.0 and 1.0,
  "topics": ["topic1", "topic2"],
  "profanity": true | false,
  "profanity_severity": 0 | 1 | 2 | 3 (0=none, 1=mild e.g. damn/hell/crap, 2=moderate e.g. shit/ass/bitch, 3=scorching e.g. fuck/cunt and slurs),
  "insult_target": "" or "@user:server" if someone is being insulted,
  %s,
  "gratitude_target": "" or "@user:server" if thanking someone
}

Message: %s`, wotdInstruction, messageText)

	reqBody := ollamaRequest{
		Model:  p.ollamaModel,
		Prompt: prompt,
		Stream: false,
	}

	body, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}

	url := strings.TrimRight(p.ollamaHost, "/") + "/api/generate"
	slog.Debug("llm_passive: calling ollama", "url", url, "model", p.ollamaModel)
	resp, err := p.httpClient.Post(url, "application/json", bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("ollama request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		respBody, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("ollama status %d: %s", resp.StatusCode, string(respBody))
	}

	var ollamaResp ollamaResponse
	if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
		return nil, fmt.Errorf("decode ollama response: %w", err)
	}

	result, err := parseClassification(ollamaResp.Response)
	if err != nil {
		return nil, fmt.Errorf("parse classification: %w", err)
	}

	return result, nil
}

// parseClassification parses a JSON classification response with repair logic.
// fancyWordMaxFreq is the threshold below which a word is considered rare/sophisticated.
// SCOWL tiers: 10=1000, 20=800, 35=600, 50=400, 60=200, 70=50.
// A frequency of 100 or below corresponds to SCOWL tier 65-70 (rare words).
const fancyWordMaxFreq = 100

// fancyWordMinLength filters out short words that may just be uncommon abbreviations.
const fancyWordMinLength = 6

// fancyWordCache caches frequency lookups to avoid repeated requests.
var fancyWordCache sync.Map // word (string) -> freq (int)

// hasFancyWord checks if any word in the message is a rare/sophisticated English word
// by looking up frequency scores in DreamDict via a single batch request.
func (p *LLMPassivePlugin) hasFancyWord(message string) bool {
	// Extract candidate words
	var candidates []string
	var cached []string
	for _, w := range strings.Fields(strings.ToLower(message)) {
		w = strings.Trim(w, ".,!?;:\"'()[]{}…—–-")
		if len(w) < fancyWordMinLength {
			continue
		}
		if strings.Contains(w, "://") || strings.HasPrefix(w, "@") {
			continue
		}
		// Check cache first
		if freq, ok := fancyWordCache.Load(w); ok {
			if f := freq.(int); f > 0 && f <= fancyWordMaxFreq {
				return true
			}
			cached = append(cached, w)
			continue
		}
		candidates = append(candidates, w)
	}

	if len(candidates) == 0 {
		return false
	}

	// Single batch request for all uncached words
	freqs, err := p.dict.FrequencyBatch(candidates, "en")
	if err != nil {
		slog.Debug("hasFancyWord: batch lookup failed", "err", err)
		return false
	}

	for _, w := range candidates {
		freq := freqs[w] // 0 if not in result (unknown word)
		fancyWordCache.Store(w, freq)
		if freq > 0 && freq <= fancyWordMaxFreq {
			return true
		}
	}
	return false
}

func parseClassification(raw string) (*classificationResult, error) {
	cleaned := raw

	// Remove <think>...</think> blocks (Qwen 3.5 reasoning)
	if i := strings.Index(cleaned, "<think>"); i != -1 {
		if j := strings.Index(cleaned, "</think>"); j != -1 {
			cleaned = cleaned[:i] + cleaned[j+len("</think>"):]
			cleaned = strings.TrimSpace(cleaned)
		}
	}

	// Remove markdown code fences
	cleaned = strings.TrimSpace(cleaned)
	if strings.HasPrefix(cleaned, "```json") {
		cleaned = strings.TrimPrefix(cleaned, "```json")
	} else if strings.HasPrefix(cleaned, "```") {
		cleaned = strings.TrimPrefix(cleaned, "```")
	}
	if strings.HasSuffix(cleaned, "```") {
		cleaned = strings.TrimSuffix(cleaned, "```")
	}
	cleaned = strings.TrimSpace(cleaned)

	// Try to parse directly first
	var result classificationResult
	if err := json.Unmarshal([]byte(cleaned), &result); err == nil {
		return &result, nil
	}

	// Repair: replace single quotes with double quotes
	repaired := strings.ReplaceAll(cleaned, "'", "\"")

	// Repair: remove trailing commas before } or ]
	trailingCommaRe := regexp.MustCompile(`,\s*([}\]])`)
	repaired = trailingCommaRe.ReplaceAllString(repaired, "$1")

	if err := json.Unmarshal([]byte(repaired), &result); err != nil {
		return nil, fmt.Errorf("JSON parse failed after repair: %w (raw: %s)", err, raw)
	}

	return &result, nil
}

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "..."
}

// Command handlers

func (p *LLMPassivePlugin) handlePotty(ctx MessageContext) error {
	target := ctx.Sender
	args := p.GetArgs(ctx.Body, "potty")
	if args != "" {
		if resolved, ok := p.ResolveUser(args, ctx.RoomID); ok {
			target = resolved
		}
	}

	d := db.Get()
	var count, mild, moderate, scorching int
	err := d.QueryRow(
		`SELECT COALESCE(count, 0), COALESCE(mild, 0), COALESCE(moderate, 0), COALESCE(scorching, 0)
		 FROM potty_mouth WHERE user_id = ?`,
		string(target),
	).Scan(&count, &mild, &moderate, &scorching)
	if err != nil {
		count = 0
	}

	msg := fmt.Sprintf("Profanity for %s: %s total (🫣 mild: %s, 😲 moderate: %s, 🤬 scorching: %s)",
		string(target), formatNumber(count), formatNumber(mild), formatNumber(moderate), formatNumber(scorching))
	return p.SendReply(ctx.RoomID, ctx.EventID, msg)
}

func (p *LLMPassivePlugin) handlePottyboard(ctx MessageContext) error {
	members := p.RoomMembers(ctx.RoomID)

	d := db.Get()
	rows, err := d.Query(
		`SELECT user_id, count FROM potty_mouth ORDER BY count DESC`,
	)
	if err != nil {
		slog.Error("llm: pottyboard query", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to load potty board.")
	}
	defer rows.Close()

	var sb strings.Builder
	sb.WriteString("Potty Mouth Board — Top 10\n\n")

	medals := []string{"\U0001f947", "\U0001f948", "\U0001f949"}
	i := 0
	for rows.Next() && i < 10 {
		var userID string
		var count int
		if err := rows.Scan(&userID, &count); err != nil {
			continue
		}
		if members != nil && !members[id.UserID(userID)] {
			continue
		}
		prefix := fmt.Sprintf("#%d", i+1)
		if i < len(medals) {
			prefix = medals[i]
		}
		sb.WriteString(fmt.Sprintf("%s %s — %s incidents\n", prefix, userID, formatNumber(count)))
		i++
	}

	if i == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, "No profanity data yet.")
	}

	return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
}

func (p *LLMPassivePlugin) handleInsults(ctx MessageContext) error {
	target := ctx.Sender
	args := p.GetArgs(ctx.Body, "insults")
	if args != "" {
		if resolved, ok := p.ResolveUser(args, ctx.RoomID); ok {
			target = resolved
		}
	}

	d := db.Get()
	var insulted, insulting int
	err := d.QueryRow(
		`SELECT COALESCE(times_insulted, 0), COALESCE(times_insulting, 0) FROM insult_log WHERE user_id = ?`,
		string(target),
	).Scan(&insulted, &insulting)
	if err != nil {
		insulted = 0
		insulting = 0
	}

	msg := fmt.Sprintf("Insult stats for %s:\nTimes insulted: %s\nTimes insulting others: %s",
		string(target), formatNumber(insulted), formatNumber(insulting))
	return p.SendReply(ctx.RoomID, ctx.EventID, msg)
}

func (p *LLMPassivePlugin) handleInsultboard(ctx MessageContext) error {
	members := p.RoomMembers(ctx.RoomID)

	d := db.Get()
	rows, err := d.Query(
		`SELECT user_id, times_insulted FROM insult_log ORDER BY times_insulted DESC`,
	)
	if err != nil {
		slog.Error("llm: insultboard query", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to load insult board.")
	}
	defer rows.Close()

	var sb strings.Builder
	sb.WriteString("Most Insulted — Top 10\n\n")

	medals := []string{"\U0001f947", "\U0001f948", "\U0001f949"}
	i := 0
	for rows.Next() && i < 10 {
		var userID string
		var count int
		if err := rows.Scan(&userID, &count); err != nil {
			continue
		}
		if members != nil && !members[id.UserID(userID)] {
			continue
		}
		prefix := fmt.Sprintf("#%d", i+1)
		if i < len(medals) {
			prefix = medals[i]
		}
		sb.WriteString(fmt.Sprintf("%s %s — %s times\n", prefix, userID, formatNumber(count)))
		i++
	}

	if i == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, "No insult data yet.")
	}

	return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
}

func (p *LLMPassivePlugin) handleSentiment(ctx MessageContext) error {
	target := ctx.Sender
	args := p.GetArgs(ctx.Body, "sentiment")
	if args != "" {
		if resolved, ok := p.ResolveUser(args, ctx.RoomID); ok {
			target = resolved
		}
	}

	d := db.Get()
	var positive, negative, neutral, excited, sarcastic, frustrated, curious, grateful, humorous, supportive int
	var totalScore float64
	err := d.QueryRow(
		`SELECT COALESCE(positive, 0), COALESCE(negative, 0), COALESCE(neutral, 0),
		        COALESCE(excited, 0), COALESCE(sarcastic, 0), COALESCE(frustrated, 0),
		        COALESCE(curious, 0), COALESCE(grateful, 0), COALESCE(humorous, 0),
		        COALESCE(supportive, 0), COALESCE(total_score, 0)
		 FROM sentiment_stats WHERE user_id = ?`,
		string(target),
	).Scan(&positive, &negative, &neutral, &excited, &sarcastic, &frustrated,
		&curious, &grateful, &humorous, &supportive, &totalScore)
	if err != nil {
		return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("No sentiment data for %s yet.", string(target)))
	}

	total := positive + negative + neutral + excited + sarcastic + frustrated + curious + grateful + humorous + supportive
	avgScore := 0.0
	if total > 0 {
		avgScore = totalScore / float64(total)
	}

	mood := "neutral"
	if avgScore > 0.3 {
		mood = "mostly positive"
	} else if avgScore > 0.1 {
		mood = "leaning positive"
	} else if avgScore < -0.3 {
		mood = "mostly negative"
	} else if avgScore < -0.1 {
		mood = "leaning negative"
	}

	// Build sentiment breakdown, only showing non-zero counts
	type sentEntry struct {
		emoji string
		label string
		count int
	}
	entries := []sentEntry{
		{"👍", "Positive", positive},
		{"🔥", "Excited", excited},
		{"🤗", "Supportive", supportive},
		{"💜", "Grateful", grateful},
		{"😂", "Humorous", humorous},
		{"🧐", "Curious", curious},
		{"😐", "Neutral", neutral},
		{"🤨", "Sarcastic", sarcastic},
		{"😮\u200d💨", "Frustrated", frustrated},
		{"👎", "Negative", negative},
	}

	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("Sentiment for %s:\n", string(target)))
	for _, e := range entries {
		if e.count > 0 {
			sb.WriteString(fmt.Sprintf("  %s %s: %s\n", e.emoji, e.label, formatNumber(e.count)))
		}
	}
	sb.WriteString(fmt.Sprintf("Average mood: %.2f (%s)", avgScore, mood))

	return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
}

func (p *LLMPassivePlugin) handleRoomSentiment(ctx MessageContext) error {
	d := db.Get()
	var positive, negative, neutral, excited, sarcastic, frustrated, curious, grateful, humorous, supportive int
	var totalScore float64
	err := d.QueryRow(
		`SELECT COALESCE(positive, 0), COALESCE(negative, 0), COALESCE(neutral, 0),
		        COALESCE(excited, 0), COALESCE(sarcastic, 0), COALESCE(frustrated, 0),
		        COALESCE(curious, 0), COALESCE(grateful, 0), COALESCE(humorous, 0),
		        COALESCE(supportive, 0), COALESCE(total_score, 0)
		 FROM room_sentiment_stats WHERE room_id = ?`,
		string(ctx.RoomID),
	).Scan(&positive, &negative, &neutral, &excited, &sarcastic, &frustrated,
		&curious, &grateful, &humorous, &supportive, &totalScore)
	if err != nil {
		return p.SendReply(ctx.RoomID, ctx.EventID, "No sentiment data for this room yet.")
	}

	total := positive + negative + neutral + excited + sarcastic + frustrated + curious + grateful + humorous + supportive
	avgScore := 0.0
	if total > 0 {
		avgScore = totalScore / float64(total)
	}

	mood := "neutral"
	if avgScore > 0.3 {
		mood = "mostly positive"
	} else if avgScore > 0.1 {
		mood = "leaning positive"
	} else if avgScore < -0.3 {
		mood = "mostly negative"
	} else if avgScore < -0.1 {
		mood = "leaning negative"
	}

	type sentEntry struct {
		emoji string
		label string
		count int
	}
	entries := []sentEntry{
		{"👍", "Positive", positive},
		{"🔥", "Excited", excited},
		{"🤗", "Supportive", supportive},
		{"💜", "Grateful", grateful},
		{"😂", "Humorous", humorous},
		{"🧐", "Curious", curious},
		{"😐", "Neutral", neutral},
		{"🤨", "Sarcastic", sarcastic},
		{"😮\u200d💨", "Frustrated", frustrated},
		{"👎", "Negative", negative},
	}

	var sb strings.Builder
	sb.WriteString("**Room Sentiment:**\n")
	for _, e := range entries {
		if e.count > 0 {
			pct := float64(e.count) / float64(total) * 100
			sb.WriteString(fmt.Sprintf("  %s %s: %s (%.0f%%)\n", e.emoji, e.label, formatNumber(e.count), pct))
		}
	}
	sb.WriteString(fmt.Sprintf("\n%s messages classified | Average mood: %.2f (%s)", formatNumber(total), avgScore, mood))

	return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
}
