using System.Globalization;
using System.Text.Json;
using Microsoft.Extensions.Options;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Metadata;
using TeleWave.Domain.Library;
using static TeleWave.Infrastructure.Metadata.MetadataJson;

namespace TeleWave.Infrastructure.Metadata;

/// <summary>Провайдер метаданных OMDb (omdbapi.com, данные IMDb). Требует API-ключ.</summary>
public sealed class OmdbMetadataProvider(
    IHttpClientFactory httpFactory,
    IOptions<MetadataOptions> options
) : IMetadataProvider
{
    private readonly OmdbOptions _omdb = options.Value.Omdb;

    public string Key => "omdb";

    public async Task<IReadOnlyList<MetadataCandidate>> SearchShowsAsync(
        string query,
        ShowKind kind,
        CancellationToken cancellationToken
    )
    {
        // Тип сужает выдачу, но не влияет на идентификатор: imdbID уникален и для фильма, и для
        // сериала, поэтому GetShowAsync ходит по нему одинаково — в отличие от TMDb, где tv и movie
        // нумеруются врозь.
        var type = kind == ShowKind.Series ? "series" : "movie";
        var url =
            $"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&type={type}&s={Uri.EscapeDataString(query)}";
        using var doc = await GetAsync(httpFactory, url, cancellationToken);
        if (!doc.RootElement.TryGetProperty("Search", out var search))
            return [];

        var list = new List<MetadataCandidate>();
        foreach (var item in search.EnumerateArray())
        {
            var id = Clean(GetString(item, "imdbID"));
            if (id is null)
                continue;

            // Тип читаем из ответа, а не подставляем запрошенный: фильтр в запросе OMDb соблюдает,
            // но отдельные серии (Type = episode) иногда просачиваются, а шоу из них не собирают.
            var found = KindFrom(Clean(GetString(item, "Type")));
            if (found is null)
                continue;

            list.Add(
                new MetadataCandidate(
                    id,
                    Clean(GetString(item, "Title")) ?? "—",
                    YearFrom(GetString(item, "Year")),
                    null,
                    Clean(GetString(item, "Poster")),
                    found
                )
            );
        }
        return list;
    }

    /// <summary>Тип здесь не используется: imdbID однозначен для фильмов и сериалов одинаково.</summary>
    public async Task<ShowMetadata?> GetShowAsync(
        string externalId,
        ShowKind kind,
        CancellationToken cancellationToken
    )
    {
        var url = $"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}";
        using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
        if (doc is null || !IsResponseTrue(doc.RootElement))
            return null;
        var root = doc.RootElement;
        return new ShowMetadata(
            externalId,
            Clean(GetString(root, "Title")) ?? "—",
            YearFrom(GetString(root, "Year")),
            Clean(GetString(root, "Plot")),
            Clean(GetString(root, "Poster")),
            GenresFrom(Clean(GetString(root, "Genre"))),
            // «Rated» приезжает только в ответе по id — то есть ровно здесь, без лишнего запроса.
            // Отдаём как есть: приводить к шкале будет приложение (ContentRating).
            Clean(GetString(root, "Rated"))
        );
    }

    /// <summary>Тип из выдачи OMDb: «series»/«movie»; «episode» и незнакомое — null (в шоу не годятся).</summary>
    private static ShowKind? KindFrom(string? type) =>
        type?.ToLowerInvariant() switch
        {
            "series" => ShowKind.Series,
            "movie" => ShowKind.Single,
            _ => null,
        };

    /// <summary>OMDb отдаёт жанры одной строкой через запятую: «Action, Sci-Fi».</summary>
    private static List<string> GenresFrom(string? value) =>
        string.IsNullOrWhiteSpace(value)
            ? []
            : value
                .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                .ToList();

    public async Task<EpisodeMetadata?> GetEpisodeAsync(
        string externalId,
        int season,
        int episode,
        CancellationToken cancellationToken
    )
    {
        var url =
            $"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}"
            + $"&Season={season}&Episode={episode}";
        using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
        if (doc is null || !IsResponseTrue(doc.RootElement))
            return null;
        var root = doc.RootElement;
        return new EpisodeMetadata(
            Clean(GetString(root, "Title")) ?? "—",
            Clean(GetString(root, "Plot")),
            Clean(GetString(root, "Poster")),
            DateFrom(GetString(root, "Released"))
        );
    }

    public async Task<int?> GetSeasonEpisodeCountAsync(
        string externalId,
        int season,
        CancellationToken cancellationToken
    )
    {
        var url =
            $"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}&Season={season}";
        using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
        if (
            doc is null
            || !IsResponseTrue(doc.RootElement)
            || !doc.RootElement.TryGetProperty("Episodes", out var episodes)
            || episodes.ValueKind != JsonValueKind.Array
        )
            return null;
        return episodes.GetArrayLength();
    }

    /// <summary>
    /// OMDb перечня сезонов не отдаёт — только их количество (<c>totalSeasons</c>, строкой) в ответе
    /// по id. Считаем сезоны сплошной нумерацией с единицы: разрывов в ней у OMDb не бывает.
    /// </summary>
    public async Task<IReadOnlyList<int>> GetSeasonNumbersAsync(
        string externalId,
        CancellationToken cancellationToken
    )
    {
        var url = $"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}";
        using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
        if (doc is null || !IsResponseTrue(doc.RootElement))
            return [];

        var total = Clean(GetString(doc.RootElement, "totalSeasons"));
        return int.TryParse(total, CultureInfo.InvariantCulture, out var count) && count > 0
            ? Enumerable.Range(1, count).ToList()
            : [];
    }

    private static bool IsResponseTrue(JsonElement root) =>
        GetString(root, "Response") is { } r
        && r.Equals("True", StringComparison.OrdinalIgnoreCase);

    /// <summary>OMDb отдаёт «N/A» вместо отсутствующих значений — приводим к null.</summary>
    private static string? Clean(string? value) =>
        string.IsNullOrWhiteSpace(value) || value == "N/A" ? null : value;

    private static DateOnly? DateFrom(string? released) =>
        DateTime.TryParse(released, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)
            ? DateOnly.FromDateTime(dt)
            : null;
}
