package plugin

import (
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"strings"
	"time"

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

	"maunium.net/go/mautrix"
)

var httpClient = &http.Client{Timeout: 15 * time.Second}

// LookupPlugin provides Wikipedia, dictionary, Urban Dictionary, and translation lookups.
type LookupPlugin struct {
	Base
	rateLimiter *RateLimitsPlugin
	dict        *dreamclient.Client
}

// NewLookupPlugin creates a new lookup plugin.
func NewLookupPlugin(client *mautrix.Client, rateLimiter *RateLimitsPlugin, dict *dreamclient.Client) *LookupPlugin {
	return &LookupPlugin{
		Base:        NewBase(client),
		rateLimiter: rateLimiter,
		dict:        dict,
	}
}

func (p *LookupPlugin) Name() string { return "lookup" }

func (p *LookupPlugin) Commands() []CommandDef {
	return []CommandDef{
		{Name: "wiki", Description: "Look up a Wikipedia summary", Usage: "!wiki <topic>", Category: "Lookup & Reference"},
		{Name: "define", Description: "Look up a word definition", Usage: "!define <word> [lang]", Category: "Lookup & Reference"},
		{Name: "urban", Description: "Look up Urban Dictionary definition", Usage: "!urban <term>", Category: "Lookup & Reference"},
		{Name: "translate", Description: "Look up word translations (en/fr/pt-PT)", Usage: "!translate <word> [lang]", Category: "Lookup & Reference"},
	}
}

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

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

func (p *LookupPlugin) OnMessage(ctx MessageContext) error {
	var handler func(MessageContext) error
	switch {
	case p.IsCommand(ctx.Body, "wiki"):
		handler = p.handleWiki
	case p.IsCommand(ctx.Body, "define"):
		handler = p.handleDefine
	case p.IsCommand(ctx.Body, "urban"):
		handler = p.handleUrban
	case p.IsCommand(ctx.Body, "translate"):
		handler = p.handleTranslate
	default:
		return nil
	}
	safeGo("lookup-handler", func() {
		if err := handler(ctx); err != nil {
			slog.Error("lookup: handler error", "err", err)
		}
	})
	return nil
}

func (p *LookupPlugin) handleWiki(ctx MessageContext) error {
	topic := strings.TrimSpace(p.GetArgs(ctx.Body, "wiki"))
	if topic == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !wiki <topic>")
	}

	// Check 24h cache
	cacheKey := "wiki:" + strings.ToLower(topic)
	if cached := db.CacheGet(cacheKey, 86400); cached != "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, cached)
	}

	encoded := url.PathEscape(strings.ReplaceAll(topic, " ", "_"))
	apiURL := fmt.Sprintf("https://en.wikipedia.org/api/rest_v1/page/summary/%s", encoded)

	resp, err := httpClient.Get(apiURL)
	if err != nil {
		slog.Error("lookup: wiki request", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to reach Wikipedia.")
	}
	defer resp.Body.Close()

	if resp.StatusCode == 404 {
		return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("No Wikipedia article found for \"%s\".", topic))
	}
	if resp.StatusCode != 200 {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Wikipedia returned an error.")
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to read Wikipedia response.")
	}

	var result struct {
		Title       string `json:"title"`
		Extract     string `json:"extract"`
		ContentURLs struct {
			Desktop struct {
				Page string `json:"page"`
			} `json:"desktop"`
		} `json:"content_urls"`
	}
	if err := json.Unmarshal(body, &result); err != nil {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to parse Wikipedia response.")
	}

	extract := result.Extract
	if len(extract) > 500 {
		extract = extract[:500] + "..."
	}

	msg := fmt.Sprintf("%s\n\n%s\n\n%s", result.Title, extract, result.ContentURLs.Desktop.Page)
	db.CacheSet(cacheKey, msg)
	return p.SendReply(ctx.RoomID, ctx.EventID, msg)
}

func (p *LookupPlugin) handleDefine(ctx MessageContext) error {
	args := strings.TrimSpace(p.GetArgs(ctx.Body, "define"))
	if args == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !define <word> [lang]")
	}

	parts := strings.Fields(args)
	word := parts[0]
	var filterLang string
	if len(parts) >= 2 {
		filterLang = normaliseLangExt(parts[len(parts)-1])
		if filterLang != "" && len(parts) > 2 {
			word = strings.Join(parts[:len(parts)-1], " ")
		} else if filterLang == "" {
			// Last token isn't a lang — treat the whole thing as the word.
			word = args
		}
	}

	wordLower := strings.ToLower(word)

	// Check 24h cache.
	cacheKey := "define:" + wordLower
	if filterLang != "" {
		cacheKey += ":" + filterLang
	}
	if cached := db.CacheGet(cacheKey, 86400); cached != "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, cached)
	}

	var sb strings.Builder
	foundDreamDict := false

	// Try DreamDict across languages.
	if p.dict != nil {
		langs := []string{"en", "fr", "pt-PT", "zh"}
		if filterLang != "" {
			langs = []string{filterLang}
		}

		// Fire antonym fetch concurrently (for whichever lang the word is found in).
		type antResult struct {
			ants []string
		}
		antCh := make(chan antResult, 1)
		antStarted := false

		for _, lang := range langs {
			defs, err := p.dict.Define(wordLower, lang)
			if err != nil {
				slog.Error("lookup: dreamdict define", "lang", lang, "err", err)
				continue
			}
			if len(defs) == 0 {
				continue
			}

			if !foundDreamDict {
				sb.WriteString(fmt.Sprintf("Definition of \"%s\":\n", word))
				foundDreamDict = true
			}

			langName := langDisplayName(lang)
			sb.WriteString(fmt.Sprintf("\n🏷️ %s\n", langName))

			count := 0
			for _, def := range defs {
				if count >= 4 {
					break
				}
				if def.POS != "" {
					sb.WriteString(fmt.Sprintf("  (%s) %s\n", def.POS, def.Gloss))
				} else {
					sb.WriteString(fmt.Sprintf("  %s\n", def.Gloss))
				}
				count++
			}

			// Start antonym fetch for the first language we find a match in.
			if !antStarted {
				antStarted = true
				antLang := lang
				safeGo("lookup-antonyms", func() {
					ants, err := p.dict.Antonyms(wordLower, antLang)
					if err != nil {
						antCh <- antResult{}
						return
					}
					antCh <- antResult{ants: ants}
				})
			}
		}

		// Collect antonyms (500ms timeout).
		if antStarted {
			timer := time.NewTimer(500 * time.Millisecond)
			select {
			case r := <-antCh:
				timer.Stop()
				if len(r.ants) > 0 {
					display := r.ants
					if len(display) > 3 {
						display = display[:3]
					}
					sb.WriteString(fmt.Sprintf("\nAntonyms: %s\n", strings.Join(display, ", ")))
				}
			case <-timer.C:
			}
		}
	}

	// Fall back to free dictionary API for English if DreamDict had no results.
	if !foundDreamDict && (filterLang == "" || filterLang == "en") {
		apiURL := fmt.Sprintf("https://api.dictionaryapi.dev/api/v2/entries/en/%s", url.PathEscape(word))

		resp, err := httpClient.Get(apiURL)
		if err != nil {
			slog.Error("lookup: define request", "err", err)
			return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to reach dictionary API.")
		}
		defer resp.Body.Close()

		if resp.StatusCode == 404 {
			return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("No definition found for \"%s\".", word))
		}

		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to read dictionary response.")
		}

		var entries []struct {
			Word     string `json:"word"`
			Meanings []struct {
				PartOfSpeech string `json:"partOfSpeech"`
				Definitions  []struct {
					Definition string `json:"definition"`
					Example    string `json:"example"`
				} `json:"definitions"`
			} `json:"meanings"`
		}

		if err := json.Unmarshal(body, &entries); err != nil || len(entries) == 0 {
			return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("No definition found for \"%s\".", word))
		}

		entry := entries[0]
		sb.WriteString(fmt.Sprintf("Definition of \"%s\":\n\n", entry.Word))

		for i, meaning := range entry.Meanings {
			if i >= 3 {
				break
			}
			sb.WriteString(fmt.Sprintf("(%s)\n", meaning.PartOfSpeech))
			for j, def := range meaning.Definitions {
				if j >= 2 {
					break
				}
				sb.WriteString(fmt.Sprintf("  %d. %s\n", j+1, def.Definition))
				if def.Example != "" {
					sb.WriteString(fmt.Sprintf("     Example: \"%s\"\n", def.Example))
				}
			}
		}
	} else if !foundDreamDict {
		return p.SendReply(ctx.RoomID, ctx.EventID,
			fmt.Sprintf("No definition found for \"%s\" in %s.", word, langDisplayName(filterLang)))
	}

	msg := sb.String()
	db.CacheSet(cacheKey, msg)
	return p.SendReply(ctx.RoomID, ctx.EventID, msg)
}

func (p *LookupPlugin) handleUrban(ctx MessageContext) error {
	term := strings.TrimSpace(p.GetArgs(ctx.Body, "urban"))
	if term == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !urban <term>")
	}

	d := db.Get()
	termLower := strings.ToLower(term)

	// Check cache (24h)
	var cachedData string
	var cachedAt int64
	err := d.QueryRow(
		`SELECT data, cached_at FROM urban_cache WHERE term = ?`,
		termLower,
	).Scan(&cachedData, &cachedAt)

	now := time.Now().UTC().Unix()
	if err == nil && now-cachedAt < 86400 {
		return p.SendReply(ctx.RoomID, ctx.EventID, cachedData)
	}

	// Fetch from API
	apiURL := fmt.Sprintf("https://api.urbandictionary.com/v0/define?term=%s", url.QueryEscape(term))

	resp, err := httpClient.Get(apiURL)
	if err != nil {
		slog.Error("lookup: urban request", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to reach Urban Dictionary.")
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to read Urban Dictionary response.")
	}

	var result struct {
		List []struct {
			Word       string `json:"word"`
			Definition string `json:"definition"`
			Example    string `json:"example"`
			ThumbsUp   int    `json:"thumbs_up"`
			ThumbsDown int    `json:"thumbs_down"`
		} `json:"list"`
	}

	if err := json.Unmarshal(body, &result); err != nil || len(result.List) == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("No Urban Dictionary definition found for \"%s\".", term))
	}

	entry := result.List[0]
	def := entry.Definition
	// Remove bracket notation used by Urban Dictionary
	def = strings.ReplaceAll(def, "[", "")
	def = strings.ReplaceAll(def, "]", "")
	if len(def) > 400 {
		def = def[:400] + "..."
	}

	example := entry.Example
	example = strings.ReplaceAll(example, "[", "")
	example = strings.ReplaceAll(example, "]", "")
	if len(example) > 200 {
		example = example[:200] + "..."
	}

	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("Urban Dictionary: %s\n\n", entry.Word))
	sb.WriteString(fmt.Sprintf("%s\n", def))
	if example != "" {
		sb.WriteString(fmt.Sprintf("\nExample: %s\n", example))
	}
	sb.WriteString(fmt.Sprintf("\n+%d / -%d", entry.ThumbsUp, entry.ThumbsDown))

	msg := sb.String()

	// Cache the result
	db.Exec("lookup: urban cache",
		`INSERT INTO urban_cache (term, data, cached_at) VALUES (?, ?, ?)
		 ON CONFLICT(term) DO UPDATE SET data = ?, cached_at = ?`,
		termLower, msg, now, msg, now,
	)

	return p.SendReply(ctx.RoomID, ctx.EventID, msg)
}

// dreamdictLangs are the supported DreamDict languages in lookup order.
var dreamdictLangs = []string{"en", "fr", "pt-PT"}

// dreamdictLangNames maps language tags to display names.
var dreamdictLangNames = map[string]string{
	"en":    "English",
	"fr":    "French",
	"pt-PT": "Portuguese",
}

// normaliseLang normalises a language tag to the canonical DreamDict form.
func normaliseLang(s string) string {
	s = strings.ToLower(strings.TrimSpace(s))
	switch s {
	case "en":
		return "en"
	case "fr":
		return "fr"
	case "pt-pt", "pt":
		return "pt-PT"
	default:
		return ""
	}
}

func (p *LookupPlugin) handleTranslate(ctx MessageContext) error {
	args := strings.TrimSpace(p.GetArgs(ctx.Body, "translate"))
	if args == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID,
			"Usage: !translate <word> [lang]\nSupported languages: en, fr, pt-PT")
	}

	if p.dict == nil {
		return p.SendReply(ctx.RoomID, ctx.EventID, "❌ Dictionary service unavailable. Try again shortly.")
	}

	parts := strings.Fields(args)
	word := parts[0]
	var sourceLang string

	if len(parts) >= 2 {
		sourceLang = normaliseLang(parts[1])
		if sourceLang == "" {
			return p.SendReply(ctx.RoomID, ctx.EventID,
				fmt.Sprintf("❌ Unknown language \"%s\". Supported: en, fr, pt-PT", parts[1]))
		}
	}

	// If lang specified, validate word exists in that language
	if sourceLang != "" {
		valid, err := p.dict.IsValidWord(word, sourceLang)
		if err != nil {
			slog.Error("lookup: translate valid check", "err", err)
			return p.SendReply(ctx.RoomID, ctx.EventID, "❌ Dictionary service unavailable. Try again shortly.")
		}
		if !valid {
			return p.SendReply(ctx.RoomID, ctx.EventID,
				fmt.Sprintf("❌ \"%s\" not found in %s.", word, dreamdictLangNames[sourceLang]))
		}
	} else {
		// Auto-detect: try each language in order
		for _, lang := range dreamdictLangs {
			valid, err := p.dict.IsValidWord(word, lang)
			if err != nil {
				slog.Error("lookup: translate auto-detect", "lang", lang, "err", err)
				return p.SendReply(ctx.RoomID, ctx.EventID, "❌ Dictionary service unavailable. Try again shortly.")
			}
			if valid {
				sourceLang = lang
				break
			}
		}
		if sourceLang == "" {
			return p.SendReply(ctx.RoomID, ctx.EventID,
				fmt.Sprintf("❌ \"%s\" not found in any supported language.", word))
		}
	}

	// Translate to other languages
	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("🔤 %s (%s)\n", word, sourceLang))

	for _, targetLang := range dreamdictLangs {
		if targetLang == sourceLang {
			continue
		}
		translations, err := p.dict.Translate(word, sourceLang, targetLang)
		if err != nil {
			slog.Error("lookup: translate call", "from", sourceLang, "to", targetLang, "err", err)
			sb.WriteString(fmt.Sprintf("  → %s: (no direct translation)\n", targetLang))
			continue
		}
		if len(translations) == 0 {
			sb.WriteString(fmt.Sprintf("  → %s: (no direct translation)\n", targetLang))
			continue
		}
		display := translations
		extra := 0
		if len(display) > 5 {
			extra = len(display) - 5
			display = display[:5]
		}
		line := strings.Join(display, ", ")
		if extra > 0 {
			line += fmt.Sprintf(" (+%d more)", extra)
		}
		sb.WriteString(fmt.Sprintf("  → %s: %s\n", targetLang, line))
	}

	return p.SendReply(ctx.RoomID, ctx.EventID, strings.TrimRight(sb.String(), "\n"))
}
