// Copyright (C) 2024 Intro-Skipper contributors <intro-skipper.org>
// SPDX-License-Identifier: GPL-3.0-only.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Mime;
using IntroSkipper.Data;
using MediaBrowser.Common.Api;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace IntroSkipper.Controllers;

/// <summary>
/// Audio fingerprint visualization controller. Allows browsing fingerprints on a per episode basis.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="VisualizationController"/> class.
/// </remarks>
/// <param name="logger">Logger.</param>
[Authorize(Policy = Policies.RequiresElevation)]
[ApiController]
[Produces(MediaTypeNames.Application.Json)]
[Route("Intros")]
public class VisualizationController(ILogger<VisualizationController> logger) : ControllerBase
{
    private readonly ILogger<VisualizationController> _logger = logger;

    /// <summary>
    /// Returns all show names and seasons.
    /// </summary>
    /// <returns>Dictionary of show names to a list of season names.</returns>
    [HttpGet("Shows")]
    public ActionResult<Dictionary<Guid, ShowInfos>> GetShowSeasons()
    {
        _logger.LogDebug("Returning season IDs by series name");

        var showSeasons = new Dictionary<Guid, ShowInfos>();

        foreach (var kvp in Plugin.Instance!.QueuedMediaItems)
        {
            if (kvp.Value.FirstOrDefault() is QueuedEpisode first)
            {
                var seriesId = first.SeriesId;
                var seasonId = kvp.Key;

                var seasonNumber = first.SeasonNumber;
                if (!showSeasons.TryGetValue(seriesId, out var showInfo))
                {
                    showInfo = new ShowInfos { SeriesName = first.SeriesName, ProductionYear = GetProductionYear(seriesId), LibraryName = GetLibraryName(seriesId), IsMovie = first.IsMovie, Seasons = [] };
                    showSeasons[seriesId] = showInfo;
                }

                showInfo.Seasons[seasonId] = seasonNumber;
            }
        }

        // Sort the dictionary by SeriesName and the seasons by SeasonName
        var sortedShowSeasons = showSeasons
            .OrderBy(kvp => kvp.Value.SeriesName)
            .ToDictionary(
                kvp => kvp.Key,
                kvp => new ShowInfos
                {
                    SeriesName = kvp.Value.SeriesName,
                    ProductionYear = kvp.Value.ProductionYear,
                    LibraryName = kvp.Value.LibraryName,
                    IsMovie = kvp.Value.IsMovie,
                    Seasons = kvp.Value.Seasons
                        .OrderBy(s => s.Value)
                        .ToDictionary(s => s.Key, s => s.Value)
                });

        return sortedShowSeasons;
    }

    /// <summary>
    /// Returns the ignore list for the provided season.
    /// </summary>
    /// <param name="seasonId">Season ID.</param>
    /// <returns>List of episode titles.</returns>
    [HttpGet("IgnoreListSeason/{SeasonId}")]
    public ActionResult<IgnoreListItem> GetIgnoreListSeason([FromRoute] Guid seasonId)
    {
        if (!Plugin.Instance!.QueuedMediaItems.ContainsKey(seasonId))
        {
            return NotFound();
        }

        if (!Plugin.Instance!.IgnoreList.TryGetValue(seasonId, out _))
        {
            return new IgnoreListItem(seasonId);
        }

        return new IgnoreListItem(Plugin.Instance!.IgnoreList[seasonId]);
    }

    /// <summary>
    /// Returns the ignore list for the provided series.
    /// </summary>
    /// <param name="seriesId">Show ID.</param>
    /// <returns>List of episode titles.</returns>
    [HttpGet("IgnoreListSeries/{SeriesId}")]
    public ActionResult<IgnoreListItem> GetIgnoreListSeries([FromRoute] Guid seriesId)
    {
        var seasonIds = Plugin.Instance!.QueuedMediaItems
            .Where(kvp => kvp.Value.Any(e => e.SeriesId == seriesId))
            .Select(kvp => kvp.Key)
            .ToList();

        if (seasonIds.Count == 0)
        {
            return NotFound();
        }

        return new IgnoreListItem(Guid.Empty)
        {
            IgnoreIntro = seasonIds.All(seasonId => Plugin.Instance!.IsIgnored(seasonId, AnalysisMode.Introduction)),
            IgnoreCredits = seasonIds.All(seasonId => Plugin.Instance!.IsIgnored(seasonId, AnalysisMode.Credits))
        };
    }

    /// <summary>
    /// Returns the names and unique identifiers of all episodes in the provided season.
    /// </summary>
    /// <param name="seriesId">Show ID.</param>
    /// <param name="seasonId">Season ID.</param>
    /// <returns>List of episode titles.</returns>
    [HttpGet("Show/{SeriesId}/{SeasonId}")]
    public ActionResult<List<EpisodeVisualization>> GetSeasonEpisodes([FromRoute] Guid seriesId, [FromRoute] Guid seasonId)
    {
        if (!Plugin.Instance!.QueuedMediaItems.TryGetValue(seasonId, out var episodes))
        {
            return NotFound();
        }

        if (!episodes.Any(e => e.SeriesId == seriesId))
        {
            return NotFound();
        }

        var showName = episodes.FirstOrDefault()?.SeriesName!;

        return episodes.Select(e => new EpisodeVisualization(e.EpisodeId, e.Name)).ToList();
    }

    /// <summary>
    /// Fingerprint the provided episode and returns the uncompressed fingerprint data points.
    /// </summary>
    /// <param name="id">Episode id.</param>
    /// <returns>Read only collection of fingerprint points.</returns>
    [HttpGet("Episode/{Id}/Chromaprint")]
    public ActionResult<uint[]> GetEpisodeFingerprint([FromRoute] Guid id)
    {
        // Search through all queued episodes to find the requested id
        foreach (var season in Plugin.Instance!.QueuedMediaItems)
        {
            foreach (var needle in season.Value)
            {
                if (needle.EpisodeId == id)
                {
                    return FFmpegWrapper.Fingerprint(needle, AnalysisMode.Introduction);
                }
            }
        }

        return NotFound();
    }

    /// <summary>
    /// Erases all timestamps for the provided season.
    /// </summary>
    /// <param name="seriesId">Show ID.</param>
    /// <param name="seasonId">Season ID.</param>
    /// <param name="eraseCache">Erase cache.</param>
    /// <response code="204">Season timestamps erased.</response>
    /// <response code="404">Unable to find season in provided series.</response>
    /// <returns>No content.</returns>
    [HttpDelete("Show/{SeriesId}/{SeasonId}")]
    public ActionResult EraseSeason([FromRoute] Guid seriesId, [FromRoute] Guid seasonId, [FromQuery] bool eraseCache = false)
    {
        var episodes = Plugin.Instance!.QueuedMediaItems
            .Where(kvp => kvp.Key == seasonId)
            .SelectMany(kvp => kvp.Value.Where(e => e.SeriesId == seriesId))
            .ToList();

        if (episodes.Count == 0)
        {
            return NotFound();
        }

        _logger.LogInformation("Erasing timestamps for series {SeriesId} season {SeasonId} at user request", seriesId, seasonId);

        foreach (var e in episodes)
        {
            Plugin.Instance!.Intros.TryRemove(e.EpisodeId, out _);
            Plugin.Instance!.Credits.TryRemove(e.EpisodeId, out _);
            e.State.ResetStates();
            if (eraseCache)
            {
                FFmpegWrapper.DeleteEpisodeCache(e.EpisodeId);
            }
        }

        Plugin.Instance!.SaveTimestamps(AnalysisMode.Introduction | AnalysisMode.Credits);

        return NoContent();
    }

    /// <summary>
    /// Updates the ignore list for the provided season.
    /// </summary>
    /// <param name="ignoreListItem">New ignore list items.</param>
    /// <param name="save">Save the ignore list.</param>
    /// <returns>No content.</returns>
    [HttpPost("IgnoreList/UpdateSeason")]
    public ActionResult UpdateIgnoreListSeason([FromBody] IgnoreListItem ignoreListItem, bool save = true)
    {
        if (!Plugin.Instance!.QueuedMediaItems.ContainsKey(ignoreListItem.SeasonId))
        {
            return NotFound();
        }

        if (ignoreListItem.IgnoreIntro || ignoreListItem.IgnoreCredits)
        {
            Plugin.Instance!.IgnoreList.AddOrUpdate(ignoreListItem.SeasonId, ignoreListItem, (_, _) => ignoreListItem);
        }
        else
        {
            Plugin.Instance!.IgnoreList.TryRemove(ignoreListItem.SeasonId, out _);
        }

        if (save)
        {
            Plugin.Instance!.SaveIgnoreList();
        }

        return NoContent();
    }

    /// <summary>
    /// Updates the ignore list for the provided series.
    /// </summary>
    /// <param name="seriesId">Series ID.</param>
    /// <param name="ignoreListItem">New ignore list items.</param>
    /// <returns>No content.</returns>
    [HttpPost("IgnoreList/UpdateSeries/{SeriesId}")]
    public ActionResult UpdateIgnoreListSeries([FromRoute] Guid seriesId, [FromBody] IgnoreListItem ignoreListItem)
    {
        var seasonIds = Plugin.Instance!.QueuedMediaItems
            .Where(kvp => kvp.Value.Any(e => e.SeriesId == seriesId))
            .Select(kvp => kvp.Key)
            .ToList();

        if (seasonIds.Count == 0)
        {
            return NotFound();
        }

        foreach (var seasonId in seasonIds)
        {
            UpdateIgnoreListSeason(new IgnoreListItem(ignoreListItem) { SeasonId = seasonId }, false);
        }

        Plugin.Instance!.SaveIgnoreList();

        return NoContent();
    }

    /// <summary>
    /// Updates the introduction timestamps for the provided episode.
    /// </summary>
    /// <param name="id">Episode ID to update timestamps for.</param>
    /// <param name="timestamps">New introduction start and end times.</param>
    /// <response code="204">New introduction timestamps saved.</response>
    /// <returns>No content.</returns>
    [HttpPost("Episode/{Id}/UpdateIntroTimestamps")]
    [Obsolete("deprecated use Episode/{Id}/Timestamps")]
    public ActionResult UpdateIntroTimestamps([FromRoute] Guid id, [FromBody] Intro timestamps)
    {
        if (timestamps.IntroEnd > 0.0)
        {
            var tr = new TimeRange(timestamps.IntroStart, timestamps.IntroEnd);
            Plugin.Instance!.Intros[id] = new Segment(id, tr);
            Plugin.Instance.SaveTimestamps(AnalysisMode.Introduction);
        }

        return NoContent();
    }

    private static string GetProductionYear(Guid seriesId)
    {
        return seriesId == Guid.Empty
            ? "Unknown"
            : Plugin.Instance?.GetItem(seriesId)?.ProductionYear?.ToString(CultureInfo.InvariantCulture) ?? "Unknown";
    }

    private static string GetLibraryName(Guid seriesId)
    {
        if (seriesId == Guid.Empty)
        {
            return "Unknown";
        }

        var collectionFolders = Plugin.Instance?.GetCollectionFolders(seriesId);
        return collectionFolders?.Count > 0
            ? string.Join(", ", collectionFolders.Select(folder => folder.Name))
            : "Unknown";
    }
}