package plugin

import (
	"bytes"
	"context"
	"fmt"
	"image"
	"image/color"
	"image/png"
	"log/slog"
	"math"
	"math/rand/v2"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	// Embedded Go fonts
	_ "image/jpeg"

	"gogobee/internal/db"

	"github.com/fogleman/gg"
	"golang.org/x/image/font"
	"golang.org/x/image/font/gofont/gobold"
	"golang.org/x/image/font/gofont/goregular"
	"golang.org/x/image/font/opentype"

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

const (
	cartonWidth  = 400
	cartonHeight = 600
	photoSize    = 140
)

// MilkCartonPlugin generates milk carton style "missing person" images.
type MilkCartonPlugin struct {
	Base
	rateLimiter   *RateLimitsPlugin
	thresholdDays int
	maxDays       int
	minMessages   int
	excludeUsers  map[string]bool
	regularFont   font.Face
	boldFont      font.Face
	smallFont     font.Face
	headerFont    font.Face
}

// NewMilkCartonPlugin creates a new milk carton plugin.
func NewMilkCartonPlugin(client *mautrix.Client, rateLimiter *RateLimitsPlugin) *MilkCartonPlugin {
	threshold := 14
	if v := os.Getenv("MISSING_THRESHOLD_DAYS"); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n > 0 {
			threshold = n
		}
	}

	maxDays := 90
	if v := os.Getenv("MISSING_MAX_DAYS"); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n > 0 {
			maxDays = n
		}
	}

	minMsgs := 10
	if v := os.Getenv("MISSING_MIN_MESSAGES"); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n > 0 {
			minMsgs = n
		}
	}

	exclude := make(map[string]bool)
	if v := os.Getenv("MISSING_EXCLUDE_USERS"); v != "" {
		for _, u := range strings.Split(v, ",") {
			u = strings.TrimSpace(u)
			if u != "" {
				exclude[u] = true
			}
		}
	}

	// Load embedded fonts
	regFont := loadFont(goregular.TTF, 16)
	bldFont := loadFont(gobold.TTF, 18)
	smlFont := loadFont(goregular.TTF, 13)
	hdrFont := loadFont(gobold.TTF, 22)

	return &MilkCartonPlugin{
		Base:          NewBase(client),
		rateLimiter:   rateLimiter,
		thresholdDays: threshold,
		maxDays:       maxDays,
		minMessages:   minMsgs,
		excludeUsers:  exclude,
		regularFont:   regFont,
		boldFont:      bldFont,
		smallFont:     smlFont,
		headerFont:    hdrFont,
	}
}

func loadFont(ttfData []byte, size float64) font.Face {
	f, err := opentype.Parse(ttfData)
	if err != nil {
		slog.Error("milkcarton: parse font", "err", err)
		return nil
	}
	face, err := opentype.NewFace(f, &opentype.FaceOptions{
		Size:    size,
		DPI:     72,
		Hinting: font.HintingFull,
	})
	if err != nil {
		slog.Error("milkcarton: create face", "err", err)
		return nil
	}
	return face
}

func (p *MilkCartonPlugin) Name() string { return "milkcarton" }

func (p *MilkCartonPlugin) Commands() []CommandDef {
	return []CommandDef{
		{Name: "haveyouseenthem", Description: "Generate a milk carton missing poster for a user", Usage: "!haveyouseenthem @user", Category: "Fun & Games"},
		{Name: "missing", Description: "List members who haven't posted recently", Usage: "!missing [post [@user]]", Category: "Fun & Games"},
	}
}

func (p *MilkCartonPlugin) Init() error { return nil }

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

func (p *MilkCartonPlugin) OnMessage(ctx MessageContext) error {
	if p.IsCommand(ctx.Body, "haveyouseenthem") {
		return p.handleHaveYouSeenThem(ctx)
	}
	if p.IsCommand(ctx.Body, "missing") {
		return p.handleMissing(ctx)
	}
	return nil
}

func (p *MilkCartonPlugin) handleHaveYouSeenThem(ctx MessageContext) error {
	args := strings.TrimSpace(p.GetArgs(ctx.Body, "haveyouseenthem"))
	if args == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !haveyouseenthem @user")
	}

	targetID, ok := p.ResolveUser(args, ctx.RoomID)
	if !ok {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Could not find a user matching that name.")
	}

	// Rate limit: 1 carton per room per day
	if p.rateLimiter != nil && !p.rateLimiter.CheckLimit(id.UserID(ctx.RoomID), "milkcarton", 1) {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Milk carton limit reached for today. Try again tomorrow.")
	}

	return p.generateAndPost(ctx.RoomID, targetID)
}

func (p *MilkCartonPlugin) handleMissing(ctx MessageContext) error {
	args := strings.TrimSpace(p.GetArgs(ctx.Body, "missing"))

	if strings.HasPrefix(strings.ToLower(args), "post") {
		postArgs := strings.TrimSpace(args[4:])

		// Rate limit: 1 carton per room per day
		if p.rateLimiter != nil && !p.rateLimiter.CheckLimit(id.UserID(ctx.RoomID), "milkcarton", 1) {
			return p.SendReply(ctx.RoomID, ctx.EventID, "Milk carton limit reached for today. Try again tomorrow.")
		}

		if postArgs != "" {
			// !missing post @user
			targetID := id.UserID(strings.TrimPrefix(strings.TrimSpace(postArgs), "@"))
			return p.generateAndPost(ctx.RoomID, targetID)
		}

		// !missing post — generate for longest-absent member
		missing := p.getMissingMembers()
		if len(missing) == 0 {
			return p.SendReply(ctx.RoomID, ctx.EventID, "No missing members found. Everyone's been active!")
		}
		return p.generateAndPost(ctx.RoomID, id.UserID(missing[0].userID))
	}

	// !missing — list missing members
	missing := p.getMissingMembers()
	if len(missing) == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, "No missing members found. Everyone's been active!")
	}

	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("Missing members (inactive %d-%d days):\n\n", p.thresholdDays, p.maxDays))

	limit := 15
	if len(missing) < limit {
		limit = len(missing)
	}
	for i := 0; i < limit; i++ {
		m := missing[i]
		sb.WriteString(fmt.Sprintf("  %s — last seen %s\n", m.userID, humanDuration(m.daysSince)))
	}
	if len(missing) > limit {
		sb.WriteString(fmt.Sprintf("\n...and %d more", len(missing)-limit))
	}

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

type missingMember struct {
	userID    string
	daysSince int
}

func (p *MilkCartonPlugin) getMissingMembers() []missingMember {
	d := db.Get()
	now := time.Now().UTC()
	minDate := now.AddDate(0, 0, -p.maxDays).Format("2006-01-02")
	maxDate := now.AddDate(0, 0, -p.thresholdDays).Format("2006-01-02")

	rows, err := d.Query(`
		SELECT da.user_id, MAX(da.date) as last_date
		FROM daily_activity da
		JOIN user_stats us ON us.user_id = da.user_id
		WHERE us.total_messages >= ?
		GROUP BY da.user_id
		HAVING last_date >= ? AND last_date <= ?
		ORDER BY last_date ASC`,
		p.minMessages, minDate, maxDate,
	)
	if err != nil {
		slog.Error("milkcarton: query missing", "err", err)
		return nil
	}
	defer rows.Close()

	type candidate struct {
		userID, lastDate string
	}
	var candidates []candidate
	for rows.Next() {
		var c candidate
		if err := rows.Scan(&c.userID, &c.lastDate); err != nil {
			continue
		}
		candidates = append(candidates, c)
	}
	rows.Close()

	var result []missingMember
	for _, c := range candidates {
		// Skip excluded users
		if p.excludeUsers[c.userID] {
			continue
		}

		// Skip bots (our own user ID)
		if id.UserID(c.userID) == p.Client.UserID {
			continue
		}

		// Skip users with active away/afk status
		var awayStatus int
		_ = d.QueryRow(`SELECT 1 FROM presence WHERE user_id = ? AND status IN ('away', 'afk')`, c.userID).Scan(&awayStatus)
		if awayStatus == 1 {
			continue
		}

		lastTime, err := time.Parse("2006-01-02", c.lastDate)
		if err != nil {
			continue
		}
		days := int(now.Sub(lastTime).Hours() / 24)
		result = append(result, missingMember{userID: c.userID, daysSince: days})
	}
	return result
}

func (p *MilkCartonPlugin) generateAndPost(roomID id.RoomID, targetID id.UserID) error {
	d := db.Get()
	uid := string(targetID)

	// Get display name
	displayName := uid
	resp, err := p.Client.GetDisplayName(context.Background(), targetID)
	if err == nil && resp.DisplayName != "" {
		displayName = resp.DisplayName
	}

	// Get last seen
	var lastDate string
	err = d.QueryRow(`SELECT MAX(date) FROM daily_activity WHERE user_id = ?`, uid).Scan(&lastDate)
	lastSeen := "Unknown"
	if err == nil && lastDate != "" {
		if t, err := time.Parse("2006-01-02", lastDate); err == nil {
			days := int(time.Since(t).Hours() / 24)
			lastSeen = humanDuration(days)
		}
	}

	// Get level and XP
	var xp, level int
	_ = d.QueryRow(`SELECT xp, level FROM users WHERE user_id = ?`, uid).Scan(&xp, &level)

	// Get characteristics
	characteristics := p.deriveCharacteristics(uid)

	// Get avatar
	avatarImg := p.fetchAvatar(targetID)

	// Generate milk carton image
	imgData, err := p.renderCarton(displayName, uid, lastSeen, level, xp, characteristics, avatarImg)
	if err != nil {
		slog.Error("milkcarton: render", "err", err)
		return p.SendReply(roomID, id.EventID(""), "Failed to generate milk carton image.")
	}

	caption := fmt.Sprintf("🥛 Has anyone seen %s lately?", displayName)
	return p.SendImage(roomID, imgData, "milkcarton.png", caption, cartonWidth, cartonHeight)
}

func (p *MilkCartonPlugin) fetchAvatar(userID id.UserID) image.Image {
	// Try Matrix avatar
	avatarURL, err := p.Client.GetAvatarURL(context.Background(), userID)
	if err == nil && !avatarURL.IsEmpty() {
		data, err := p.Client.DownloadBytes(context.Background(), avatarURL)
		if err == nil {
			img, _, err := image.Decode(bytes.NewReader(data))
			if err == nil {
				return img
			}
		}
	}

	// Try placeholder directory
	dataDir := os.Getenv("DATA_DIR")
	if dataDir == "" {
		dataDir = "./data"
	}
	placeholderDir := filepath.Join(dataDir, "placeholders")
	if entries, err := os.ReadDir(placeholderDir); err == nil && len(entries) > 0 {
		var images []string
		for _, e := range entries {
			name := strings.ToLower(e.Name())
			if strings.HasSuffix(name, ".png") || strings.HasSuffix(name, ".jpg") || strings.HasSuffix(name, ".jpeg") {
				images = append(images, filepath.Join(placeholderDir, e.Name()))
			}
		}
		if len(images) > 0 {
			chosen := images[rand.IntN(len(images))]
			f, err := os.Open(chosen)
			if err == nil {
				defer f.Close()
				img, _, err := image.Decode(f)
				if err == nil {
					return img
				}
			}
		}
	}

	// Fallback: nil — renderCarton will draw a silhouette
	return nil
}

func (p *MilkCartonPlugin) renderCarton(
	displayName, userID, lastSeen string,
	level, xp int,
	characteristics []string,
	avatar image.Image,
) ([]byte, error) {
	dc := gg.NewContext(cartonWidth, cartonHeight)

	// Background — cream/off-white like a milk carton
	dc.SetColor(color.RGBA{255, 253, 245, 255})
	dc.Clear()

	// Border
	dc.SetColor(color.RGBA{180, 60, 60, 255})
	dc.SetLineWidth(4)
	dc.DrawRoundedRectangle(8, 8, float64(cartonWidth-16), float64(cartonHeight-16), 12)
	dc.Stroke()

	// Inner border
	dc.SetColor(color.RGBA{200, 80, 80, 255})
	dc.SetLineWidth(1.5)
	dc.DrawRoundedRectangle(16, 16, float64(cartonWidth-32), float64(cartonHeight-32), 8)
	dc.Stroke()

	// Header: "HAVE YOU SEEN THIS PERSON?"
	if p.headerFont != nil {
		dc.SetFontFace(p.headerFont)
	}
	dc.SetColor(color.RGBA{180, 30, 30, 255})
	dc.DrawStringAnchored("HAVE YOU SEEN", float64(cartonWidth)/2, 48, 0.5, 0.5)
	dc.DrawStringAnchored("THIS PERSON?", float64(cartonWidth)/2, 74, 0.5, 0.5)

	// Photo area
	photoX := float64(cartonWidth)/2 - float64(photoSize)/2
	photoY := 95.0

	// Photo border
	dc.SetColor(color.RGBA{160, 50, 50, 255})
	dc.SetLineWidth(2)
	dc.DrawRectangle(photoX-3, photoY-3, float64(photoSize)+6, float64(photoSize)+6)
	dc.Stroke()

	// Photo background
	dc.SetColor(color.RGBA{230, 225, 215, 255})
	dc.DrawRectangle(photoX, photoY, float64(photoSize), float64(photoSize))
	dc.Fill()

	if avatar != nil {
		// Resize and crop avatar to fit photo area
		avatarResized := resizeImage(avatar, photoSize, photoSize)
		dc.DrawImage(avatarResized, int(photoX), int(photoY))
	} else {
		// Draw silhouette
		drawSilhouette(dc, photoX, photoY, float64(photoSize))
	}

	// Name
	yPos := photoY + float64(photoSize) + 25
	if p.boldFont != nil {
		dc.SetFontFace(p.boldFont)
	}
	dc.SetColor(color.RGBA{40, 40, 40, 255})

	// Truncate display name if too long
	name := displayName
	if len(name) > 28 {
		name = name[:25] + "..."
	}
	dc.DrawStringAnchored(name, float64(cartonWidth)/2, yPos, 0.5, 0.5)

	// User ID
	yPos += 22
	if p.smallFont != nil {
		dc.SetFontFace(p.smallFont)
	}
	dc.SetColor(color.RGBA{100, 100, 100, 255})
	shortID := userID
	if len(shortID) > 35 {
		shortID = shortID[:32] + "..."
	}
	dc.DrawStringAnchored(shortID, float64(cartonWidth)/2, yPos, 0.5, 0.5)

	// Last seen
	yPos += 28
	if p.regularFont != nil {
		dc.SetFontFace(p.regularFont)
	}
	dc.SetColor(color.RGBA{180, 30, 30, 255})
	dc.DrawStringAnchored(fmt.Sprintf("Last seen: %s", lastSeen), float64(cartonWidth)/2, yPos, 0.5, 0.5)

	// Level / XP
	yPos += 22
	dc.SetColor(color.RGBA{80, 80, 80, 255})
	if p.smallFont != nil {
		dc.SetFontFace(p.smallFont)
	}
	dc.DrawStringAnchored(fmt.Sprintf("Level %d | %d XP", level, xp), float64(cartonWidth)/2, yPos, 0.5, 0.5)

	// Divider
	yPos += 18
	dc.SetColor(color.RGBA{200, 80, 80, 180})
	dc.SetLineWidth(1)
	dc.DrawLine(40, yPos, float64(cartonWidth)-40, yPos)
	dc.Stroke()

	// Characteristics header
	yPos += 20
	if p.boldFont != nil {
		dc.SetFontFace(p.boldFont)
	}
	dc.SetColor(color.RGBA{60, 60, 60, 255})
	dc.DrawStringAnchored("Distinguishing Characteristics", float64(cartonWidth)/2, yPos, 0.5, 0.5)

	// Characteristics
	yPos += 8
	if p.smallFont != nil {
		dc.SetFontFace(p.smallFont)
	}
	dc.SetColor(color.RGBA{80, 80, 80, 255})
	for _, c := range characteristics {
		yPos += 18
		dc.DrawStringAnchored(fmt.Sprintf("• %s", c), float64(cartonWidth)/2, yPos, 0.5, 0.5)
	}

	// Footer
	if p.smallFont != nil {
		dc.SetFontFace(p.smallFont)
	}
	dc.SetColor(color.RGBA{140, 50, 50, 255})
	dc.DrawStringAnchored("If found, please return to the community", float64(cartonWidth)/2, float64(cartonHeight)-42, 0.5, 0.5)

	dc.SetColor(color.RGBA{180, 180, 180, 255})
	dc.DrawStringAnchored("🥛 milk carton community alert 🥛", float64(cartonWidth)/2, float64(cartonHeight)-24, 0.5, 0.5)

	// Encode to PNG
	var buf bytes.Buffer
	if err := png.Encode(&buf, dc.Image()); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

// deriveCharacteristics generates flavor text from cached archetypes.
func (p *MilkCartonPlugin) deriveCharacteristics(userID string) []string {
	archetypes := GetUserArchetypesLimited(userID)

	// Pick up to 3 archetype flavor texts (highest signal score first)
	var chars []string
	for _, a := range archetypes {
		if a.Flavor != "" {
			chars = append(chars, a.Flavor)
		}
		if len(chars) >= 3 {
			break
		}
	}

	if len(chars) > 0 {
		return chars
	}

	// Fallback when no archetypes cached yet
	fallbacks := []string{
		"Considered armed with strong opinions",
		"May be found near a keyboard",
		"Last seen staring at a screen",
		"Approach with memes",
	}
	for len(chars) < 2 {
		chars = append(chars, fallbacks[rand.IntN(len(fallbacks))])
	}

	return chars
}

// resizeImage scales an image to fit within the target dimensions.
func resizeImage(src image.Image, targetW, targetH int) image.Image {
	srcBounds := src.Bounds()
	srcW := srcBounds.Dx()
	srcH := srcBounds.Dy()

	// Calculate scale to fill target (crop to fit)
	scaleX := float64(targetW) / float64(srcW)
	scaleY := float64(targetH) / float64(srcH)
	scale := math.Max(scaleX, scaleY)

	newW := int(float64(srcW) * scale)
	newH := int(float64(srcH) * scale)

	dc := gg.NewContext(targetW, targetH)
	dc.DrawImage(src, (targetW-newW)/2, (targetH-newH)/2)

	// Use gg's built-in scaling — anchor to top so heads aren't cropped
	dcScaled := gg.NewContext(targetW, targetH)
	dcScaled.Scale(scale, scale)
	offsetX := -float64(srcW)/2 + float64(targetW)/(2*scale)
	offsetY := 0.0 // top-anchored crop
	dcScaled.DrawImage(src, int(offsetX), int(offsetY))

	return dcScaled.Image()
}

func humanDuration(days int) string {
	if days == 0 {
		return "today"
	}
	if days == 1 {
		return "yesterday"
	}
	if days < 7 {
		return fmt.Sprintf("%d days ago", days)
	}
	weeks := days / 7
	if weeks == 1 {
		return "1 week ago"
	}
	if days < 30 {
		return fmt.Sprintf("%d weeks ago", weeks)
	}
	months := days / 30
	if months == 1 {
		return "1 month ago"
	}
	return fmt.Sprintf("%d months ago", months)
}
