package plugin

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

	"gogobee/internal/db"

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

// tmdbSearchResult represents a result from the TMDB search API.
type tmdbSearchResult struct {
	ID           int     `json:"id"`
	Title        string  `json:"title"`
	Name         string  `json:"name"` // for TV shows
	Overview     string  `json:"overview"`
	ReleaseDate  string  `json:"release_date"`
	FirstAirDate string  `json:"first_air_date"` // for TV shows
	VoteAverage  float64 `json:"vote_average"`
	VoteCount    int     `json:"vote_count"`
	MediaType    string  `json:"media_type"`
	PosterPath   string  `json:"poster_path"`
	GenreIDs     []int   `json:"genre_ids"`
}

// tmdbSearchResponse is the TMDB search API response.
type tmdbSearchResponse struct {
	Page         int                `json:"page"`
	Results      []tmdbSearchResult `json:"results"`
	TotalResults int                `json:"total_results"`
}

// tmdbMovieDetail has additional movie details.
type tmdbMovieDetail struct {
	ID          int     `json:"id"`
	Title       string  `json:"title"`
	Overview    string  `json:"overview"`
	ReleaseDate string  `json:"release_date"`
	VoteAverage float64 `json:"vote_average"`
	VoteCount   int     `json:"vote_count"`
	Runtime     int     `json:"runtime"`
	Status      string  `json:"status"`
	Tagline     string  `json:"tagline"`
	Budget      int64   `json:"budget"`
	Revenue     int64   `json:"revenue"`
	Genres      []struct {
		Name string `json:"name"`
	} `json:"genres"`
}

// tmdbTVDetail has additional TV show details.
type tmdbTVDetail struct {
	ID           int     `json:"id"`
	Name         string  `json:"name"`
	Overview     string  `json:"overview"`
	FirstAirDate string  `json:"first_air_date"`
	VoteAverage  float64 `json:"vote_average"`
	VoteCount    int     `json:"vote_count"`
	Status       string  `json:"status"`
	Tagline      string  `json:"tagline"`
	NumSeasons   int     `json:"number_of_seasons"`
	NumEpisodes  int     `json:"number_of_episodes"`
	Genres       []struct {
		Name string `json:"name"`
	} `json:"genres"`
	Networks []struct {
		Name string `json:"name"`
	} `json:"networks"`
}

// tmdbUpcomingResponse is the TMDB upcoming movies response.
type tmdbUpcomingResponse struct {
	Results []tmdbSearchResult `json:"results"`
}

// MoviesPlugin provides movie and TV lookups via TMDB.
type MoviesPlugin struct {
	Base
	apiKey     string
	httpClient *http.Client
}

// NewMoviesPlugin creates a new MoviesPlugin.
func NewMoviesPlugin(client *mautrix.Client) *MoviesPlugin {
	return &MoviesPlugin{
		Base:   NewBase(client),
		apiKey: os.Getenv("TMDB_API_KEY"),
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
		},
	}
}

func (p *MoviesPlugin) Name() string { return "movies" }

func (p *MoviesPlugin) Commands() []CommandDef {
	return []CommandDef{
		{Name: "movie", Description: "Search for movie info", Usage: "!movie <title>", Category: "Entertainment"},
		{Name: "tv", Description: "Search for TV show info", Usage: "!tv <title>", Category: "Entertainment"},
		{Name: "movie watch", Description: "Add a movie to your watchlist", Usage: "!movie watch <title>", Category: "Entertainment"},
		{Name: "movie watching", Description: "List your movie watchlist", Usage: "!movie watching", Category: "Entertainment"},
		{Name: "movie unwatch", Description: "Remove from watchlist by TMDB ID", Usage: "!movie unwatch <id>", Category: "Entertainment"},
		{Name: "upcoming", Description: "Show upcoming movies", Usage: "!upcoming movies", Category: "Entertainment"},
	}
}

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

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

func (p *MoviesPlugin) OnMessage(ctx MessageContext) error {
	var handler func(MessageContext) error
	switch {
	case p.IsCommand(ctx.Body, "movie"):
		handler = p.handleMovie
	case p.IsCommand(ctx.Body, "tv"):
		handler = p.handleTV
	case p.IsCommand(ctx.Body, "upcoming"):
		args := p.GetArgs(ctx.Body, "upcoming")
		if strings.ToLower(strings.TrimSpace(args)) == "movies" {
			handler = p.handleUpcoming
		}
	}
	if handler == nil {
		return nil
	}
	safeGo("movies-handler", func() {
		if err := handler(ctx); err != nil {
			slog.Error("movies: handler error", "err", err)
		}
	})
	return nil
}

func (p *MoviesPlugin) handleMovie(ctx MessageContext) error {
	args := p.GetArgs(ctx.Body, "movie")
	if args == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID,
			"Usage: !movie <title> | !movie watch|watching|unwatch <title|id>")
	}

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

	switch sub {
	case "watch":
		if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" {
			return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !movie watch <title>")
		}
		return p.handleWatchMovie(ctx, strings.TrimSpace(parts[1]))
	case "watching":
		return p.handleWatching(ctx)
	case "unwatch":
		if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" {
			return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !movie unwatch <id>")
		}
		return p.handleUnwatch(ctx, strings.TrimSpace(parts[1]))
	default:
		return p.handleMovieSearch(ctx, args)
	}
}

func (p *MoviesPlugin) handleMovieSearch(ctx MessageContext, query string) error {
	if p.apiKey == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Movie lookups are not configured (missing API key).")
	}

	detail, err := p.searchAndFetchMovie(query)
	if err != nil {
		slog.Error("movies: search failed", "query", query, "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to search for movie.")
	}
	if detail == nil {
		return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("No movies found for \"%s\".", query))
	}

	return p.SendReply(ctx.RoomID, ctx.EventID, p.formatMovie(detail))
}

func (p *MoviesPlugin) searchAndFetchMovie(query string) (*tmdbMovieDetail, error) {
	encoded := url.QueryEscape(query)
	apiURL := fmt.Sprintf("https://api.themoviedb.org/3/search/movie?api_key=%s&query=%s&page=1",
		p.apiKey, encoded)

	var searchResp tmdbSearchResponse
	if err := p.tmdbGet(apiURL, &searchResp); err != nil {
		return nil, err
	}

	if len(searchResp.Results) == 0 {
		return nil, nil
	}

	movieID := searchResp.Results[0].ID
	return p.fetchMovieDetail(movieID)
}

func (p *MoviesPlugin) fetchMovieDetail(movieID int) (*tmdbMovieDetail, error) {
	d := db.Get()

	// Check cache (24h TTL)
	var cached string
	var cachedAt int64
	err := d.QueryRow(
		`SELECT data, cached_at FROM movie_cache WHERE tmdb_id = ?`, movieID,
	).Scan(&cached, &cachedAt)
	if err == nil && time.Now().Unix()-cachedAt < 24*3600 {
		var detail tmdbMovieDetail
		if json.Unmarshal([]byte(cached), &detail) == nil {
			return &detail, nil
		}
	}

	apiURL := fmt.Sprintf("https://api.themoviedb.org/3/movie/%d?api_key=%s", movieID, p.apiKey)
	var detail tmdbMovieDetail
	if err := p.tmdbGet(apiURL, &detail); err != nil {
		return nil, err
	}

	// Update cache
	data, _ := json.Marshal(detail)
	db.Exec("movies: cache write",
		`INSERT INTO movie_cache (tmdb_id, data, cached_at) VALUES (?, ?, ?)
		 ON CONFLICT(tmdb_id) DO UPDATE SET data = ?, cached_at = ?`,
		movieID, string(data), time.Now().Unix(), string(data), time.Now().Unix(),
	)

	return &detail, nil
}

func (p *MoviesPlugin) formatMovie(m *tmdbMovieDetail) string {
	var sb strings.Builder

	sb.WriteString(fmt.Sprintf("%s", m.Title))
	if m.ReleaseDate != "" {
		if t, err := time.Parse("2006-01-02", m.ReleaseDate); err == nil {
			sb.WriteString(fmt.Sprintf(" (%d)", t.Year()))
		}
	}
	sb.WriteString("\n")

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

	if m.VoteAverage > 0 {
		sb.WriteString(fmt.Sprintf("Rating: %.1f/10 (%d votes)\n", m.VoteAverage, m.VoteCount))
	}

	if m.Runtime > 0 {
		sb.WriteString(fmt.Sprintf("Runtime: %dh %dm\n", m.Runtime/60, m.Runtime%60))
	}

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

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

	if m.Overview != "" {
		overview := m.Overview
		if len(overview) > 400 {
			overview = overview[:400] + "..."
		}
		sb.WriteString(fmt.Sprintf("\n%s\n", overview))
	}

	sb.WriteString(fmt.Sprintf("\nhttps://www.themoviedb.org/movie/%d", m.ID))
	return sb.String()
}

func (p *MoviesPlugin) handleTV(ctx MessageContext) error {
	query := p.GetArgs(ctx.Body, "tv")
	if query == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !tv <title>")
	}

	if p.apiKey == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, "TV lookups are not configured (missing API key).")
	}

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

	encoded := url.QueryEscape(query)
	apiURL := fmt.Sprintf("https://api.themoviedb.org/3/search/tv?api_key=%s&query=%s&page=1",
		p.apiKey, encoded)

	var searchResp tmdbSearchResponse
	if err := p.tmdbGet(apiURL, &searchResp); err != nil {
		slog.Error("movies: tv search failed", "query", query, "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to search for TV show.")
	}

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

	tvID := searchResp.Results[0].ID
	detail, err := p.fetchTVDetail(tvID)
	if err != nil {
		slog.Error("movies: tv detail fetch failed", "id", tvID, "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to fetch TV show details.")
	}

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

func (p *MoviesPlugin) fetchTVDetail(tvID int) (*tmdbTVDetail, error) {
	apiURL := fmt.Sprintf("https://api.themoviedb.org/3/tv/%d?api_key=%s", tvID, p.apiKey)
	var detail tmdbTVDetail
	if err := p.tmdbGet(apiURL, &detail); err != nil {
		return nil, err
	}
	return &detail, nil
}

func (p *MoviesPlugin) formatTV(tv *tmdbTVDetail) string {
	var sb strings.Builder

	sb.WriteString(fmt.Sprintf("%s", tv.Name))
	if tv.FirstAirDate != "" {
		if t, err := time.Parse("2006-01-02", tv.FirstAirDate); err == nil {
			sb.WriteString(fmt.Sprintf(" (%d)", t.Year()))
		}
	}
	sb.WriteString("\n")

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

	if tv.VoteAverage > 0 {
		sb.WriteString(fmt.Sprintf("Rating: %.1f/10 (%d votes)\n", tv.VoteAverage, tv.VoteCount))
	}

	sb.WriteString(fmt.Sprintf("Seasons: %d | Episodes: %d\n", tv.NumSeasons, tv.NumEpisodes))

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

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

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

	if tv.Overview != "" {
		overview := tv.Overview
		if len(overview) > 400 {
			overview = overview[:400] + "..."
		}
		sb.WriteString(fmt.Sprintf("\n%s\n", overview))
	}

	sb.WriteString(fmt.Sprintf("\nhttps://www.themoviedb.org/tv/%d", tv.ID))
	return sb.String()
}

func (p *MoviesPlugin) handleWatchMovie(ctx MessageContext, title string) error {
	if p.apiKey == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Movie lookups are not configured (missing API key).")
	}

	encoded := url.QueryEscape(title)
	apiURL := fmt.Sprintf("https://api.themoviedb.org/3/search/movie?api_key=%s&query=%s&page=1",
		p.apiKey, encoded)

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

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

	result := searchResp.Results[0]
	d := db.Get()

	_, err := d.Exec(
		`INSERT INTO movie_watchlist (user_id, tmdb_id, title, media_type, room_id) VALUES (?, ?, ?, 'movie', ?)
		 ON CONFLICT(user_id, tmdb_id) DO NOTHING`,
		string(ctx.Sender), result.ID, result.Title, string(ctx.RoomID),
	)
	if err != nil {
		slog.Error("movies: watchlist add", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to add to watchlist.")
	}

	return p.SendReply(ctx.RoomID, ctx.EventID,
		fmt.Sprintf("Added \"%s\" (TMDB ID: %d) to your movie watchlist.", result.Title, result.ID))
}

func (p *MoviesPlugin) handleWatching(ctx MessageContext) error {
	d := db.Get()
	rows, err := d.Query(
		`SELECT tmdb_id, title, media_type FROM movie_watchlist WHERE user_id = ? ORDER BY title`,
		string(ctx.Sender),
	)
	if err != nil {
		slog.Error("movies: 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 Movie/TV Watchlist:\n\n")
	count := 0
	for rows.Next() {
		var tmdbID int
		var title, mediaType string
		if err := rows.Scan(&tmdbID, &title, &mediaType); err != nil {
			continue
		}
		sb.WriteString(fmt.Sprintf("  [%d] %s (%s)\n", tmdbID, title, mediaType))
		count++
	}

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

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

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

	d := db.Get()
	res, err := d.Exec(
		`DELETE FROM movie_watchlist WHERE user_id = ? AND tmdb_id = ?`,
		string(ctx.Sender), tmdbID,
	)
	if err != nil {
		slog.Error("movies: 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("TMDB ID %d is not in your watchlist.", tmdbID))
	}
	return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("Removed TMDB ID %d from your watchlist.", tmdbID))
}

func (p *MoviesPlugin) handleUpcoming(ctx MessageContext) error {
	if p.apiKey == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Movie lookups are not configured (missing API key).")
	}

	// Check 6h cache
	cacheKey := "upcoming_movies"
	if cached := db.CacheGet(cacheKey, 86400); cached != "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, cached)
	}

	apiURL := fmt.Sprintf("https://api.themoviedb.org/3/movie/upcoming?api_key=%s&region=US&page=1", p.apiKey)

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

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

	limit := 10
	if len(resp.Results) < limit {
		limit = len(resp.Results)
	}

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

	for i := 0; i < limit; i++ {
		r := resp.Results[i]
		title := r.Title
		if title == "" {
			title = r.Name
		}
		dateStr := r.ReleaseDate
		if dateStr == "" {
			dateStr = "TBA"
		}
		ratingStr := ""
		if r.VoteAverage > 0 {
			ratingStr = fmt.Sprintf(" - %.1f/10", r.VoteAverage)
		}
		sb.WriteString(fmt.Sprintf("%d. %s (%s)%s\n", i+1, title, dateStr, ratingStr))
	}

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

func (p *MoviesPlugin) tmdbGet(apiURL string, target interface{}) error {
	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("tmdb returned status %d", resp.StatusCode)
	}

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

// PostDailyReleases checks release dates against watchlist and DMs users about releases.
// Intended to be called by the scheduler daily.
func (p *MoviesPlugin) PostDailyReleases(roomID id.RoomID) {
	if p.apiKey == "" {
		slog.Warn("movies: skipping daily releases, no API key configured")
		return
	}

	today := time.Now().UTC().Format("2006-01-02")
	todayKey := fmt.Sprintf("%s:%s", today, roomID)
	if db.JobCompleted("movie_releases", todayKey) {
		slog.Info("movies: already sent daily releases", "date", todayKey)
		return
	}
	success := false
	defer func() {
		if success {
			db.MarkJobCompleted("movie_releases", todayKey)
		}
	}()

	d := db.Get()

	// Get all unique TMDB IDs from watchlists
	rows, err := d.Query(`SELECT DISTINCT tmdb_id, title, media_type FROM movie_watchlist`)
	if err != nil {
		slog.Error("movies: daily releases query", "err", err)
		return
	}

	type watchedItem struct {
		tmdbID    int
		title     string
		mediaType string
	}
	var items []watchedItem
	for rows.Next() {
		var item watchedItem
		if err := rows.Scan(&item.tmdbID, &item.title, &item.mediaType); err != nil {
			continue
		}
		items = append(items, item)
	}
	rows.Close()

	for _, item := range items {
		if item.mediaType != "movie" {
			continue
		}

		detail, err := p.fetchMovieDetail(item.tmdbID)
		if err != nil {
			slog.Error("movies: daily release fetch", "tmdb_id", item.tmdbID, "err", err)
			continue
		}

		if detail.ReleaseDate != today {
			continue
		}

		// Find watchers
		wRows, err := d.Query(
			`SELECT user_id FROM movie_watchlist WHERE tmdb_id = ?`, item.tmdbID,
		)
		if err != nil {
			continue
		}

		msg := fmt.Sprintf("Release day! \"%s\" is out today!\n%s\nhttps://www.themoviedb.org/movie/%d",
			detail.Title, detail.Tagline, detail.ID)

		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("movies: DM watcher", "user", uid, "err", err)
			}
		}
		wRows.Close()
	}

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