package plugin

import (
	"bytes"
	"encoding/json"
	"fmt"
	"image"
	"image/color"
	"image/png"
	"log/slog"
	"math/rand/v2"
	"os"
	"path/filepath"
	"strings"

	_ "image/jpeg"

	_ "golang.org/x/image/webp"

	"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"

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

// esteemedEntry is a single satirical "esteemed community member" entry.
type esteemedEntry struct {
	ID              string   `json:"id"`
	Name            string   `json:"name"`
	Category        string   `json:"category"`
	ImageURL        string   `json:"image_url"`
	Characteristics []string `json:"distinguishing_characteristics"`
}

// EsteemPlugin posts satirical milk carton missing posters for fictional "esteemed community members."
type EsteemPlugin struct {
	Base
	entries     []esteemedEntry
	enabled     bool
	room        id.RoomID
	dataDir     string
	regularFont font.Face
	boldFont    font.Face
	smallFont   font.Face
	headerFont  font.Face
	titleFont   font.Face
}

// NewEsteemPlugin creates a new esteemed members plugin.
func NewEsteemPlugin(client *mautrix.Client) *EsteemPlugin {
	enabled := os.Getenv("FEATURE_ESTEEMED") != ""
	roomID := os.Getenv("ESTEEMED_ROOM")

	dataDir := os.Getenv("DATA_DIR")
	if dataDir == "" {
		dataDir = "./data"
	}

	p := &EsteemPlugin{
		Base:    NewBase(client),
		enabled: enabled,
		room:    id.RoomID(roomID),
		dataDir: dataDir,
	}

	if enabled {
		p.regularFont = loadFont(goregular.TTF, 15)
		p.boldFont = loadFont(gobold.TTF, 16)
		p.smallFont = loadFont(goregular.TTF, 12)
		p.headerFont = loadFont(gobold.TTF, 14)
		p.titleFont = loadFont(gobold.TTF, 20)
	}

	return p
}

func (p *EsteemPlugin) Name() string { return "esteemed" }

func (p *EsteemPlugin) Commands() []CommandDef {
	return []CommandDef{
		{Name: "esteemed", Description: "Preview a random esteemed member carton (admin only)", Usage: "!esteemed [name]", Category: "Admin", AdminOnly: true},
	}
}

func (p *EsteemPlugin) Init() error {
	if !p.enabled {
		return nil
	}

	// Load entries from esteemed.json
	jsonPath := "esteemed.json"
	data, err := os.ReadFile(jsonPath)
	if err != nil {
		slog.Warn("esteemed: could not load esteemed.json", "err", err)
		return nil
	}

	if err := json.Unmarshal(data, &p.entries); err != nil {
		slog.Warn("esteemed: invalid JSON", "err", err)
		return nil
	}

	slog.Info("esteemed: loaded entries", "count", len(p.entries))
	return nil
}

func (p *EsteemPlugin) OnMessage(ctx MessageContext) error {
	if !p.IsCommand(ctx.Body, "esteemed") {
		return nil
	}
	if !p.IsAdmin(ctx.Sender) {
		return nil
	}
	if len(p.entries) == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Esteemed members list not loaded.")
	}

	args := strings.TrimSpace(p.GetArgs(ctx.Body, "esteemed"))

	var entry esteemedEntry
	if args == "" {
		// Random entry (ignores dedup — this is a preview)
		entry = p.entries[rand.IntN(len(p.entries))]
	} else {
		// Search by name
		query := strings.ToLower(args)
		found := false
		for _, e := range p.entries {
			if strings.Contains(strings.ToLower(e.Name), query) || e.ID == query {
				entry = e
				found = true
				break
			}
		}
		if !found {
			return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("No esteemed member matching \"%s\" found.", args))
		}
	}

	imgData, err := p.renderCarton(entry)
	if err != nil {
		slog.Error("esteemed: render failed", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Render failed. Try again later.")
	}

	caption := fmt.Sprintf("Yet another one of our esteemed community has gone missing.\nIf found, please return %s to the community immediately.", entry.Name)
	return p.SendImage(ctx.RoomID, imgData, "esteemed.png", caption, cartonWidth, cartonHeight)
}
func (p *EsteemPlugin) OnReaction(_ ReactionContext) error { return nil }

// PostWeekly selects an unposted entry, generates a milk carton, and posts it.
// Called by the scheduler once a week.
func (p *EsteemPlugin) PostWeekly() {
	if !p.enabled || p.room == "" || len(p.entries) == 0 {
		return
	}

	// Pick an entry that hasn't been posted yet
	entry, ok := p.pickEntry()
	if !ok {
		slog.Info("esteemed: all entries have been posted")
		return
	}

	imgData, err := p.renderCarton(entry)
	if err != nil {
		slog.Error("esteemed: render failed", "entry", entry.ID, "err", err)
		return
	}

	caption := fmt.Sprintf("Yet another one of our esteemed community has gone missing.\nIf found, please return %s to the community immediately.", entry.Name)
	if err := p.SendImage(p.room, imgData, "esteemed.png", caption, cartonWidth, cartonHeight); err != nil {
		slog.Error("esteemed: send failed", "entry", entry.ID, "err", err)
		return
	}

	// Mark as posted
	db.MarkJobCompleted("esteemed", entry.ID)
	slog.Info("esteemed: posted", "entry", entry.ID, "name", entry.Name)
}

func (p *EsteemPlugin) pickEntry() (esteemedEntry, bool) {
	// Gather unposted entries
	var candidates []esteemedEntry
	for _, e := range p.entries {
		if !db.JobCompleted("esteemed", e.ID) {
			candidates = append(candidates, e)
		}
	}

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

	// Pick a random one
	return candidates[rand.IntN(len(candidates))], true
}

func (p *EsteemPlugin) loadEntryImage(entry esteemedEntry) image.Image {
	imgPath := filepath.Join(p.dataDir, "esteemed", entry.ID+".jpg")
	f, err := os.Open(imgPath)
	if err != nil {
		return nil
	}
	defer f.Close()

	img, _, err := image.Decode(f)
	if err != nil {
		slog.Warn("esteemed: decode image", "entry", entry.ID, "err", err)
		return nil
	}
	return img
}

func (p *EsteemPlugin) renderCarton(entry esteemedEntry) ([]byte, error) {
	dc := gg.NewContext(cartonWidth, cartonHeight)

	// Background — cream/off-white
	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: "ESTEEMED COMMUNITY MEMBER"
	if p.headerFont != nil {
		dc.SetFontFace(p.headerFont)
	}
	dc.SetColor(color.RGBA{180, 30, 30, 255})
	dc.DrawStringAnchored("ESTEEMED COMMUNITY MEMBER", float64(cartonWidth)/2, 40, 0.5, 0.5)

	// Sub-header: "MISSING"
	if p.titleFont != nil {
		dc.SetFontFace(p.titleFont)
	}
	dc.DrawStringAnchored("MISSING", float64(cartonWidth)/2, 66, 0.5, 0.5)

	// Photo area
	photoX := float64(cartonWidth)/2 - float64(photoSize)/2
	photoY := 85.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()

	avatar := p.loadEntryImage(entry)
	if avatar != nil {
		avatarResized := resizeImage(avatar, photoSize, photoSize)
		dc.DrawImage(avatarResized, int(photoX), int(photoY))
	} else {
		// Silhouette for missing images
		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})

	name := entry.Name
	if len(name) > 30 {
		name = name[:27] + "..."
	}
	dc.DrawStringAnchored(name, float64(cartonWidth)/2, yPos, 0.5, 0.5)

	// Category
	yPos += 20
	if p.smallFont != nil {
		dc.SetFontFace(p.smallFont)
	}
	dc.SetColor(color.RGBA{120, 100, 100, 255})
	catDisplay := formatCategory(entry.Category)
	dc.DrawStringAnchored(catDisplay, float64(cartonWidth)/2, yPos, 0.5, 0.5)

	// "Last seen" with a random funny timeframe
	yPos += 24
	if p.regularFont != nil {
		dc.SetFontFace(p.regularFont)
	}
	dc.SetColor(color.RGBA{180, 30, 30, 255})
	lastSeen := randomLastSeen()
	dc.DrawStringAnchored(fmt.Sprintf("Last seen: %s", lastSeen), 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 += 18
	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 += 6
	if p.smallFont != nil {
		dc.SetFontFace(p.smallFont)
	}
	dc.SetColor(color.RGBA{80, 80, 80, 255})

	maxItems := 4
	if len(entry.Characteristics) < maxItems {
		maxItems = len(entry.Characteristics)
	}
	maxWidth := float64(cartonWidth) - 60 // padding on each side
	for i := 0; i < maxItems; i++ {
		wrapped := dc.WordWrap(entry.Characteristics[i], maxWidth)
		for _, wline := range wrapped {
			yPos += 16
			if yPos > float64(cartonHeight)-55 {
				break
			}
			dc.DrawStringAnchored(wline, 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 our", float64(cartonWidth)/2, float64(cartonHeight)-48, 0.5, 0.5)
	dc.DrawStringAnchored("community for \"cuddles\"", float64(cartonWidth)/2, float64(cartonHeight)-34, 0.5, 0.5)

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

func formatCategory(cat string) string {
	replacer := strings.NewReplacer(
		"tech_grifters_and_crypto", "Tech & Crypto Division",
		"politicians", "Political Affairs Bureau",
		"corporate_villains", "Corporate Relations Dept.",
		"reality_tv_royalty", "Entertainment Division",
		"internet_infamous", "Digital Community Outreach",
		"fictional_characters", "Literary & Media Wing",
		"historical_figures", "Heritage Society",
	)
	result := replacer.Replace(cat)
	if result == cat {
		// Fallback: replace underscores and capitalize first letter
		s := strings.ReplaceAll(cat, "_", " ")
		if len(s) > 0 {
			return strings.ToUpper(s[:1]) + s[1:]
		}
		return s
	}
	return result
}

func randomLastSeen() string {
	options := []string{
		"fleeing the group chat",
		"updating their LinkedIn bio",
		"somewhere near a microphone",
		"posting without reading the room",
		"starting a new venture",
		"giving unsolicited advice",
		"somewhere on the timeline",
		"rebranding after the incident",
		"in the replies, unfortunately",
		"near a camera crew",
		"drafting a press release",
		"pivoting to their next opportunity",
		"explaining why it was taken out of context",
		"launching a podcast",
		"signing autographs for no one",
	}
	return options[rand.IntN(len(options))]
}

// drawSilhouette draws a generic person silhouette. Extracted as a shared helper.
func drawSilhouette(dc *gg.Context, x, y, size float64) {
	cx := x + size/2
	cy := y + size/2

	dc.SetColor(color.RGBA{170, 165, 155, 255})

	// Head
	headRadius := size * 0.18
	dc.DrawCircle(cx, cy-size*0.12, headRadius)
	dc.Fill()

	// Body
	dc.DrawEllipse(cx, cy+size*0.22, size*0.28, size*0.25)
	dc.Fill()
}
