package metadata

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

	"git.vakhrushev.me/av/jellybit/internal/logging"
)

const tmdbDefaultBaseURL = "https://api.themoviedb.org/3"

// TMDBConfig — настройки клиента TMDB.
type TMDBConfig struct {
	APIKey  string
	Proxy   string
	Timeout time.Duration
	BaseURL string // пусто → api.themoviedb.org; задаётся в тестах
	// Language — абстрактный код языка вывода ("ru" | "en"); диалект локали TMDB
	// выводит сам клиент (tmdbLocale). Знание диалекта живёт здесь, у провайдера,
	// который на нём говорит, а не в общем слое конфига.
	Language string
}

// tmdbLocale переводит абстрактный код языка вывода в диалект локали TMDB.
// Тотальна: непокрытый вход (пусто, неизвестный код) → en-US, чтобы запрос
// никогда не ушёл без локали (defense in depth поверх валидации конфига).
// Множество кодов задаёт config.validate ({ru, en}); при добавлении кода —
// синхронно добавь ветку здесь, иначе он молча уедет в en-US.
func tmdbLocale(lang string) string {
	switch lang {
	case "ru":
		return "ru-RU"
	default:
		return "en-US"
	}
}

// TMDB — клиент The Movie Database (API v3, авторизация по api_key).
type TMDB struct {
	apiKey   string
	baseURL  string
	language string
	hc       *http.Client
	log      *slog.Logger
}

// NewTMDB собирает клиент TMDB. logger nil → slog.Default().
func NewTMDB(cfg TMDBConfig, logger *slog.Logger) (*TMDB, error) {
	if cfg.APIKey == "" {
		return nil, fmt.Errorf("metadata: tmdb api_key required")
	}
	hc, err := newHTTPClient(cfg.Proxy, cfg.Timeout)
	if err != nil {
		return nil, err
	}
	base := cfg.BaseURL
	if base == "" {
		base = tmdbDefaultBaseURL
	}
	if logger == nil {
		logger = slog.Default()
	}
	return &TMDB{apiKey: cfg.APIKey, baseURL: strings.TrimRight(base, "/"), language: tmdbLocale(cfg.Language), hc: hc, log: logger}, nil
}

func (t *TMDB) Name() string { return "tmdb" }

type tmdbSearchResp struct {
	Results []struct {
		ID            int    `json:"id"`
		Title         string `json:"title"`          // movie
		OriginalTitle string `json:"original_title"` // movie
		Name          string `json:"name"`           // tv
		OriginalName  string `json:"original_name"`  // tv
		ReleaseDate   string `json:"release_date"`   // movie
		FirstAirDate  string `json:"first_air_date"` // tv
	} `json:"results"`
}

// Search ищет фильм/сериал по названию и году.
func (t *TMDB) Search(ctx context.Context, q Query) ([]Candidate, error) {
	var path string
	params := url.Values{"api_key": {t.apiKey}, "query": {q.Title}, "include_adult": {"false"}, "language": {t.language}}
	switch q.Type {
	case Movie:
		path = "/search/movie"
		if q.Year > 0 {
			params.Set("year", strconv.Itoa(q.Year))
		}
	case Series:
		path = "/search/tv"
		if q.Year > 0 {
			params.Set("first_air_date_year", strconv.Itoa(q.Year))
		}
	default:
		return nil, fmt.Errorf("metadata: tmdb: unknown type %q", q.Type)
	}

	var resp tmdbSearchResp
	op := strings.TrimPrefix(path, "/") // search/movie | search/tv
	if err := getJSON(ctx, t.hc, t.log, logging.ServiceTMDB, op, t.baseURL+path+"?"+params.Encode(), nil, &resp); err != nil {
		return nil, fmt.Errorf("tmdb search: %w", err)
	}

	webPath := "movie"
	if q.Type == Series {
		webPath = "tv"
	}
	out := make([]Candidate, 0, len(resp.Results))
	for _, r := range resp.Results {
		title, orig, date := r.Title, r.OriginalTitle, r.ReleaseDate
		if q.Type == Series {
			title, orig, date = r.Name, r.OriginalName, r.FirstAirDate
		}
		out = append(out, Candidate{
			Provider:      "tmdb",
			ID:            strconv.Itoa(r.ID),
			Title:         title,
			OriginalTitle: orig,
			Year:          yearOf(date),
			URL:           "https://www.themoviedb.org/" + webPath + "/" + strconv.Itoa(r.ID),
		})
	}
	return out, nil
}

type tmdbTVResp struct {
	Seasons []struct {
		SeasonNumber int `json:"season_number"`
		EpisodeCount int `json:"episode_count"`
	} `json:"seasons"`
}

// SeasonEpisodeCounts возвращает число серий по сезонам сериала.
func (t *TMDB) SeasonEpisodeCounts(ctx context.Context, id string) (map[int]int, error) {
	params := url.Values{"api_key": {t.apiKey}}
	var resp tmdbTVResp
	if err := getJSON(ctx, t.hc, t.log, logging.ServiceTMDB, "tv", t.baseURL+"/tv/"+url.PathEscape(id)+"?"+params.Encode(), nil, &resp); err != nil {
		return nil, fmt.Errorf("tmdb tv %s: %w", id, err)
	}
	out := make(map[int]int, len(resp.Seasons))
	for _, s := range resp.Seasons {
		out[s.SeasonNumber] = s.EpisodeCount
	}
	return out, nil
}

type tmdbCreditsResp struct {
	Crew []struct {
		Job  string `json:"job"`
		Name string `json:"name"`
	} `json:"crew"`
}

type tmdbTVDetailsResp struct {
	CreatedBy []struct {
		Name string `json:"name"`
	} `json:"created_by"`
}

// Director возвращает режиссёра: для фильма — из crew (job "Director"), для
// сериала — создателей (created_by). Пусто — режиссёр не указан. Best-effort:
// вызывающий гасит ошибку.
func (t *TMDB) Director(ctx context.Context, mt MediaType, id string) (string, error) {
	params := url.Values{"api_key": {t.apiKey}, "language": {t.language}}
	if mt == Series {
		var resp tmdbTVDetailsResp
		if err := getJSON(ctx, t.hc, t.log, logging.ServiceTMDB, "tv",
			t.baseURL+"/tv/"+url.PathEscape(id)+"?"+params.Encode(), nil, &resp); err != nil {
			return "", fmt.Errorf("tmdb tv %s: %w", id, err)
		}
		names := make([]string, 0, len(resp.CreatedBy))
		for _, c := range resp.CreatedBy {
			if strings.TrimSpace(c.Name) != "" {
				names = append(names, c.Name)
			}
		}
		return strings.Join(names, ", "), nil
	}
	var resp tmdbCreditsResp
	if err := getJSON(ctx, t.hc, t.log, logging.ServiceTMDB, "movie/credits",
		t.baseURL+"/movie/"+url.PathEscape(id)+"/credits?"+params.Encode(), nil, &resp); err != nil {
		return "", fmt.Errorf("tmdb movie credits %s: %w", id, err)
	}
	for _, c := range resp.Crew {
		if c.Job == "Director" && strings.TrimSpace(c.Name) != "" {
			return c.Name, nil
		}
	}
	return "", nil
}

// yearOf достаёт год из даты вида "1999-03-31".
func yearOf(date string) int {
	if len(date) < 4 {
		return 0
	}
	y, err := strconv.Atoi(date[:4])
	if err != nil {
		return 0
	}
	return y
}
