package plugin

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"time"

	"gogobee/internal/db"

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

// jikanAnime represents an anime entry from the Jikan API.
type jikanAnime struct {
	MalID    int     `json:"mal_id"`
	URL      string  `json:"url"`
	Title    string  `json:"title"`
	TitleEng string  `json:"title_english"`
	Type     string  `json:"type"`
	Episodes int     `json:"episodes"`
	Status   string  `json:"status"`
	Score    float64 `json:"score"`
	Synopsis string  `json:"synopsis"`
	Aired    struct {
		String string `json:"string"`
		From   string `json:"from"`
		To     string `json:"to"`
	} `json:"aired"`
	Broadcast struct {
		Day  string `json:"day"`
		Time string `json:"time"`
		TZ   string `json:"timezone"`
	} `json:"broadcast"`
	Genres []struct {
		Name string `json:"name"`
	} `json:"genres"`
	Studios []struct {
		Name string `json:"name"`
	} `json:"studios"`
	Season string `json:"season"`
	Year   int    `json:"year"`
}

// jikanSearchResponse is the Jikan search API response.
type jikanSearchResponse struct {
	Data       []jikanAnime `json:"data"`
	Pagination struct {
		HasNext int `json:"has_next_page"`
	} `json:"pagination"`
}

// jikanSeasonResponse is the Jikan season API response.
type jikanSeasonResponse struct {
	Data []jikanAnime `json:"data"`
}

// AnimePlugin provides anime lookups via the Jikan/MAL API.
type AnimePlugin struct {
	Base
	httpClient *http.Client
	mu         sync.Mutex
	lastCall   time.Time // for rate limiting Jikan (400ms between calls)
}

// NewAnimePlugin creates a new AnimePlugin.
func NewAnimePlugin(client *mautrix.Client) *AnimePlugin {
	return &AnimePlugin{
		Base: NewBase(client),
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
		},
	}
}

func (p *AnimePlugin) Name() string { return "anime" }

func (p *AnimePlugin) Commands() []CommandDef {
	return []CommandDef{
		{Name: "anime", Description: "Search for anime info", Usage: "!anime <title>", Category: "Entertainment"},
		{Name: "anime watch", Description: "Add anime to your watchlist", Usage: "!anime watch <title>", Category: "Entertainment"},
		{Name: "anime watching", Description: "List your anime watchlist", Usage: "!anime watching", Category: "Entertainment"},
		{Name: "anime unwatch", Description: "Remove anime from watchlist by MAL ID", Usage: "!anime unwatch <id>", Category: "Entertainment"},
		{Name: "anime season", Description: "Show current season anime", Usage: "!anime season", Category: "Entertainment"},
		{Name: "anime upcoming", Description: "Show upcoming anime", Usage: "!anime upcoming", Category: "Entertainment"},
	}
}

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

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

func (p *AnimePlugin) OnMessage(ctx MessageContext) error {
	if !p.IsCommand(ctx.Body, "anime") {
		return nil
	}

	args := p.GetArgs(ctx.Body, "anime")
	if args == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID,
			"Usage: !anime <title> | !anime watch|watching|unwatch|season|upcoming")
	}

	parts := strings.SplitN(args, " ", 2)
	sub := strings.ToLower(parts[0])

	// Subcommands that only touch the DB don't need async
	switch sub {
	case "watching":
		return p.handleWatching(ctx)
	case "unwatch":
		if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" {
			return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !anime unwatch <id>")
		}
		return p.handleUnwatch(ctx, strings.TrimSpace(parts[1]))
	}

	// Subcommands that hit the Jikan API run async to avoid blocking dispatch
	safeGo("anime-handler", func() {
		var err error
		switch sub {
		case "watch":
			if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" {
				err = p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !anime watch <title>")
			} else {
				err = p.handleWatch(ctx, strings.TrimSpace(parts[1]))
			}
		case "season":
			err = p.handleSeason(ctx)
		case "upcoming":
			err = p.handleUpcoming(ctx)
		default:
			err = p.handleSearch(ctx, args)
		}
		if err != nil {
			slog.Error("anime: handler error", "err", err)
		}
	})
	return nil
}

// rateLimit enforces the 400ms delay between Jikan API calls.
func (p *AnimePlugin) rateLimit() {
	p.mu.Lock()
	defer p.mu.Unlock()
	elapsed := time.Since(p.lastCall)
	if elapsed < 400*time.Millisecond {
		time.Sleep(400*time.Millisecond - elapsed)
	}
	p.lastCall = time.Now()
}

func (p *AnimePlugin) jikanGet(apiURL string, target interface{}) error {
	p.rateLimit()

	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
	if err != nil {
		return err
	}

	resp, err := p.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("jikan returned status %d", resp.StatusCode)
	}

	return json.NewDecoder(resp.Body).Decode(target)
}

func (p *AnimePlugin) handleSearch(ctx MessageContext, query string) error {
	// Search via API
	encoded := url.QueryEscape(query)
	apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime?q=%s&limit=3&sfw=true", encoded)

	var searchResp jikanSearchResponse
	if err := p.jikanGet(apiURL, &searchResp); err != nil {
		slog.Error("anime: search failed", "query", query, "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to search for anime.")
	}

	if len(searchResp.Data) == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("No anime found for \"%s\".", query))
	}

	anime := searchResp.Data[0]

	// Cache the result
	data, _ := json.Marshal(anime)
	db.Exec("anime: cache write",
		`INSERT INTO anime_cache (mal_id, data, cached_at) VALUES (?, ?, ?)
		 ON CONFLICT(mal_id) DO UPDATE SET data = ?, cached_at = ?`,
		anime.MalID, string(data), time.Now().Unix(), string(data), time.Now().Unix(),
	)

	return p.SendReply(ctx.RoomID, ctx.EventID, p.formatAnime(anime))
}

func (p *AnimePlugin) formatAnime(a jikanAnime) string {
	var sb strings.Builder

	title := a.Title
	if a.TitleEng != "" && a.TitleEng != a.Title {
		title = fmt.Sprintf("%s (%s)", a.TitleEng, a.Title)
	}

	sb.WriteString(fmt.Sprintf("%s\n", title))
	sb.WriteString(fmt.Sprintf("MAL ID: %d\n", a.MalID))

	if a.Type != "" {
		sb.WriteString(fmt.Sprintf("Type: %s", a.Type))
		if a.Episodes > 0 {
			sb.WriteString(fmt.Sprintf(" (%d episodes)", a.Episodes))
		}
		sb.WriteString("\n")
	}

	if a.Status != "" {
		sb.WriteString(fmt.Sprintf("Status: %s\n", a.Status))
	}

	if a.Score > 0 {
		sb.WriteString(fmt.Sprintf("Score: %.2f/10\n", a.Score))
	}

	if a.Aired.String != "" {
		sb.WriteString(fmt.Sprintf("Aired: %s\n", a.Aired.String))
	}

	if len(a.Genres) > 0 {
		genres := make([]string, len(a.Genres))
		for i, g := range a.Genres {
			genres[i] = g.Name
		}
		sb.WriteString(fmt.Sprintf("Genres: %s\n", strings.Join(genres, ", ")))
	}

	if len(a.Studios) > 0 {
		studios := make([]string, len(a.Studios))
		for i, s := range a.Studios {
			studios[i] = s.Name
		}
		sb.WriteString(fmt.Sprintf("Studios: %s\n", strings.Join(studios, ", ")))
	}

	if a.Broadcast.Day != "" {
		sb.WriteString(fmt.Sprintf("Broadcast: %s %s (%s)\n", a.Broadcast.Day, a.Broadcast.Time, a.Broadcast.TZ))
	}

	if a.Synopsis != "" {
		synopsis := a.Synopsis
		if len(synopsis) > 300 {
			synopsis = synopsis[:300] + "..."
		}
		sb.WriteString(fmt.Sprintf("\n%s\n", synopsis))
	}

	if a.URL != "" {
		sb.WriteString(fmt.Sprintf("\n%s", a.URL))
	}

	return sb.String()
}

func (p *AnimePlugin) handleWatch(ctx MessageContext, title string) error {
	// Search for the anime first to get its MAL ID
	encoded := url.QueryEscape(title)
	apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime?q=%s&limit=1&sfw=true", encoded)

	var searchResp jikanSearchResponse
	if err := p.jikanGet(apiURL, &searchResp); err != nil {
		slog.Error("anime: watch search failed", "title", title, "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to search for anime.")
	}

	if len(searchResp.Data) == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("No anime found for \"%s\".", title))
	}

	anime := searchResp.Data[0]
	d := db.Get()

	_, err := d.Exec(
		`INSERT INTO anime_watchlist (user_id, mal_id, title, room_id) VALUES (?, ?, ?, ?)
		 ON CONFLICT(user_id, mal_id) DO NOTHING`,
		string(ctx.Sender), anime.MalID, anime.Title, string(ctx.RoomID),
	)
	if err != nil {
		slog.Error("anime: watchlist add", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to add to watchlist.")
	}

	// Also cache the anime data
	data, _ := json.Marshal(anime)
	db.Exec("anime: cache watchlist entry",
		`INSERT INTO anime_cache (mal_id, data, cached_at) VALUES (?, ?, ?)
		 ON CONFLICT(mal_id) DO UPDATE SET data = ?, cached_at = ?`,
		anime.MalID, string(data), time.Now().Unix(), string(data), time.Now().Unix(),
	)

	displayTitle := anime.Title
	if anime.TitleEng != "" {
		displayTitle = anime.TitleEng
	}

	return p.SendReply(ctx.RoomID, ctx.EventID,
		fmt.Sprintf("Added \"%s\" (MAL ID: %d) to your watchlist.", displayTitle, anime.MalID))
}

func (p *AnimePlugin) handleWatching(ctx MessageContext) error {
	d := db.Get()
	rows, err := d.Query(
		`SELECT mal_id, title FROM anime_watchlist WHERE user_id = ? ORDER BY title`,
		string(ctx.Sender),
	)
	if err != nil {
		slog.Error("anime: watching list", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to load watchlist.")
	}
	defer rows.Close()

	var sb strings.Builder
	sb.WriteString("Your Anime Watchlist:\n\n")
	count := 0
	for rows.Next() {
		var malID int
		var title string
		if err := rows.Scan(&malID, &title); err != nil {
			continue
		}
		sb.WriteString(fmt.Sprintf("  [%d] %s\n", malID, title))
		count++
	}

	if count == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID,
			"Your anime watchlist is empty. Use !anime watch <title> to add anime.")
	}

	sb.WriteString("\nUse !anime unwatch <id> to remove an entry.")
	return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
}

func (p *AnimePlugin) handleUnwatch(ctx MessageContext, idStr string) error {
	malID, err := strconv.Atoi(idStr)
	if err != nil {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Please provide a valid MAL ID number. Check !anime watching for your list.")
	}

	d := db.Get()
	res, err := d.Exec(
		`DELETE FROM anime_watchlist WHERE user_id = ? AND mal_id = ?`,
		string(ctx.Sender), malID,
	)
	if err != nil {
		slog.Error("anime: unwatch", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to remove from watchlist.")
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("MAL ID %d is not in your watchlist.", malID))
	}
	return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("Removed MAL ID %d from your watchlist.", malID))
}

func (p *AnimePlugin) handleSeason(ctx MessageContext) error {
	// Check 6h cache
	cacheKey := "anime_season"
	if cached := db.CacheGet(cacheKey, 86400); cached != "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, cached)
	}

	apiURL := "https://api.jikan.moe/v4/seasons/now?limit=10&sfw=true"

	var resp jikanSeasonResponse
	if err := p.jikanGet(apiURL, &resp); err != nil {
		slog.Error("anime: season fetch failed", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to fetch current season anime.")
	}

	if len(resp.Data) == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, "No anime found for the current season.")
	}

	var sb strings.Builder
	sb.WriteString("Current Season Anime:\n\n")

	for i, a := range resp.Data {
		title := a.Title
		if a.TitleEng != "" {
			title = a.TitleEng
		}
		scoreStr := "N/A"
		if a.Score > 0 {
			scoreStr = fmt.Sprintf("%.1f", a.Score)
		}
		sb.WriteString(fmt.Sprintf("%d. %s [%s] - Score: %s\n", i+1, title, a.Type, scoreStr))
	}

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

func (p *AnimePlugin) handleUpcoming(ctx MessageContext) error {
	// Check 6h cache
	cacheKey := "anime_upcoming"
	if cached := db.CacheGet(cacheKey, 86400); cached != "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, cached)
	}

	apiURL := "https://api.jikan.moe/v4/seasons/upcoming?limit=10&sfw=true"

	var resp jikanSeasonResponse
	if err := p.jikanGet(apiURL, &resp); err != nil {
		slog.Error("anime: upcoming fetch failed", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to fetch upcoming anime.")
	}

	if len(resp.Data) == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, "No upcoming anime found.")
	}

	var sb strings.Builder
	sb.WriteString("Upcoming Anime:\n\n")

	for i, a := range resp.Data {
		title := a.Title
		if a.TitleEng != "" {
			title = a.TitleEng
		}
		info := a.Type
		if a.Aired.String != "" {
			info += " | " + a.Aired.String
		}
		sb.WriteString(fmt.Sprintf("%d. %s [%s]\n", i+1, title, info))
	}

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

// PostDailyReleases checks broadcast days and DMs watchers about anime airing today.
// Intended to be called by the scheduler daily.
func (p *AnimePlugin) PostDailyReleases(roomID id.RoomID) {
	todayKey := fmt.Sprintf("%s:%s", time.Now().UTC().Format("2006-01-02"), roomID)
	if db.JobCompleted("anime_releases", todayKey) {
		slog.Info("anime: already sent daily releases", "date", todayKey)
		return
	}
	success := false
	defer func() {
		if success {
			db.MarkJobCompleted("anime_releases", todayKey)
		}
	}()

	d := db.Get()
	today := strings.ToLower(time.Now().Weekday().String())
	// Jikan uses plural day names like "Mondays", "Tuesdays", etc.
	todayPlural := today + "s"

	// Get all unique MAL IDs from watchlists
	rows, err := d.Query(`SELECT DISTINCT mal_id, title FROM anime_watchlist`)
	if err != nil {
		slog.Error("anime: daily releases query", "err", err)
		return
	}

	type animeEntry struct {
		malID int
		title string
	}
	var watchedAnime []animeEntry
	for rows.Next() {
		var e animeEntry
		if err := rows.Scan(&e.malID, &e.title); err != nil {
			continue
		}
		watchedAnime = append(watchedAnime, e)
	}
	rows.Close()

	for _, entry := range watchedAnime {
		// Check cache or fetch anime details
		var animeData jikanAnime
		var cached string
		var cachedAt int64

		err := d.QueryRow(
			`SELECT data, cached_at FROM anime_cache WHERE mal_id = ?`, entry.malID,
		).Scan(&cached, &cachedAt)

		needsFetch := err != nil || time.Now().Unix()-cachedAt > 24*3600

		if !needsFetch {
			if json.Unmarshal([]byte(cached), &animeData) != nil {
				needsFetch = true
			}
		}

		if needsFetch {
			apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime/%d", entry.malID)
			var resp struct {
				Data jikanAnime `json:"data"`
			}
			if err := p.jikanGet(apiURL, &resp); err != nil {
				slog.Error("anime: daily fetch", "mal_id", entry.malID, "err", err)
				continue
			}
			animeData = resp.Data

			// Update cache
			data, _ := json.Marshal(animeData)
			db.Exec("anime: update daily cache",
				`INSERT INTO anime_cache (mal_id, data, cached_at) VALUES (?, ?, ?)
				 ON CONFLICT(mal_id) DO UPDATE SET data = ?, cached_at = ?`,
				entry.malID, string(data), time.Now().Unix(), string(data), time.Now().Unix(),
			)
		}

		// Check if this anime broadcasts today
		broadcastDay := strings.ToLower(animeData.Broadcast.Day)
		if broadcastDay != todayPlural && broadcastDay != today {
			continue
		}

		// Only notify for currently airing anime
		if animeData.Status != "Currently Airing" {
			continue
		}

		// Find watchers for this anime
		wRows, err := d.Query(
			`SELECT user_id FROM anime_watchlist WHERE mal_id = ?`, entry.malID,
		)
		if err != nil {
			continue
		}

		displayTitle := animeData.Title
		if animeData.TitleEng != "" {
			displayTitle = animeData.TitleEng
		}

		msg := fmt.Sprintf("New episode alert! %s airs today (%s %s %s).\n%s",
			displayTitle, animeData.Broadcast.Day, animeData.Broadcast.Time,
			animeData.Broadcast.TZ, animeData.URL)

		for wRows.Next() {
			var uid string
			if err := wRows.Scan(&uid); err != nil {
				continue
			}
			if err := p.SendDM(id.UserID(uid), msg); err != nil {
				slog.Error("anime: DM watcher", "user", uid, "err", err)
			}
		}
		wRows.Close()
	}

	success = true
	slog.Info("anime: daily releases check completed")
}
