#nullable enable
using ErsatzTV.Core.Api.MediaSources;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace ErsatzTV.Application.MediaSources;

public class GetAllMediaSourcesForApiHandler(
    IDbContextFactory<TvContext> dbContextFactory)
    : IRequestHandler<GetAllMediaSourcesForApi, List<MediaSourceResponseModel>>
{
    // A never-scanned library has a null LastScan at runtime, but historical DB rows still carry the
    // 0001-01-01 MinValue sentinel written by the old Reset_* migrations. Coerce any such residual
    // sentinel to null so the API/MCP surface reports "never scanned" as null (parity with the UI),
    // regardless of DB history or provider. Belt-and-suspenders alongside the NullOutNeverScannedLastScan
    // data migration.
    private static readonly DateTime NeverScannedThreshold = new(2000, 1, 1);

    private static DateTime? NormalizeLastScan(DateTime? lastScan) =>
        lastScan is { } value && value < NeverScannedThreshold ? null : lastScan;

    public async Task<List<MediaSourceResponseModel>> Handle(
        GetAllMediaSourcesForApi request,
        CancellationToken cancellationToken)
    {
        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        List<MediaSource> mediaSources = await dbContext.MediaSources
                .AsNoTracking()
                .Include(s => s.Libraries)
                .ThenInclude(l => l.Paths)
                .ToListAsync(cancellationToken);

        Dictionary<int, int> itemCountsByLibrary = await GetItemCountsByLibrary(dbContext, cancellationToken);
        Dictionary<int, string> addressByMediaSourceId = await GetConnectionAddresses(dbContext, cancellationToken);

        var result = new List<MediaSourceResponseModel>();
        foreach (MediaSource mediaSource in mediaSources)
        {
            List<MediaSourceLibraryResponseModel> libraryModels = mediaSource.Libraries
                .Filter(ShouldIncludeLibrary)
                .OrderBy(l => l.MediaKind)
                .ThenBy(l => l.Name)
                .Map(l => new MediaSourceLibraryResponseModel(
                    l.Id,
                    l.Name,
                    l.MediaKind,
                    NormalizeLastScan(l.LastScan),
                    itemCountsByLibrary.TryGetValue(l.Id, out int count) ? count : 0))
                .ToList();

            string? address = addressByMediaSourceId.TryGetValue(mediaSource.Id, out string? a) ? a : null;

            result.Add(
                new MediaSourceResponseModel(
                    mediaSource.Id,
                    GetKind(mediaSource),
                    GetName(mediaSource),
                    address,
                    libraryModels));
        }

        return result
            .OrderBy(s => s.Kind == "Local" ? 0 : 1)
            .ThenBy(s => s.Kind)
            .ThenBy(s => s.Name)
            .ToList();
    }

    private static async Task<Dictionary<int, int>> GetItemCountsByLibrary(
        TvContext dbContext,
        CancellationToken cancellationToken)
    {
        // EF instead of Dapper: with zero rows, Microsoft.Data.Sqlite reports the
        // COUNT(*) column as BLOB, so Dapper builds an incompatible deserializer
        // and every fresh (empty) database returned 500 from this endpoint.
        var counts = await dbContext.MediaItems
            .AsNoTracking()
            .GroupBy(mi => mi.LibraryPath.LibraryId)
            .Select(g => new { LibraryId = g.Key, Count = g.Count() })
            .ToListAsync(cancellationToken);

        return counts.ToDictionary(c => c.LibraryId, c => c.Count);
    }

    private static async Task<Dictionary<int, string>> GetConnectionAddresses(
        TvContext dbContext,
        CancellationToken cancellationToken)
    {
        var addresses = new Dictionary<int, string>();

        foreach (PlexMediaSource plex in await dbContext.PlexMediaSources
                     .AsNoTracking()
                     .Include(s => s.Connections)
                     .ToListAsync(cancellationToken))
        {
            foreach (PlexConnection connection in Optional(plex.Connections.SingleOrDefault(c => c.IsActive)))
            {
                addresses[plex.Id] = connection.Uri;
            }
        }

        foreach (JellyfinMediaSource jellyfin in await dbContext.JellyfinMediaSources
                     .AsNoTracking()
                     .Include(s => s.Connections)
                     .ToListAsync(cancellationToken))
        {
            foreach (JellyfinConnection connection in jellyfin.Connections.HeadOrNone())
            {
                addresses[jellyfin.Id] = connection.Address;
            }
        }

        foreach (EmbyMediaSource emby in await dbContext.EmbyMediaSources
                     .AsNoTracking()
                     .Include(s => s.Connections)
                     .ToListAsync(cancellationToken))
        {
            foreach (EmbyConnection connection in emby.Connections.HeadOrNone())
            {
                addresses[emby.Id] = connection.Address;
            }
        }

        return addresses;
    }

    private static bool ShouldIncludeLibrary(Library library) =>
        library switch
        {
            LocalLibrary => library.Paths.Count > 0,
            PlexLibrary plex => plex.ShouldSyncItems,
            JellyfinLibrary jellyfin => jellyfin.ShouldSyncItems,
            EmbyLibrary emby => emby.ShouldSyncItems,
            _ => false
        };

    private static string GetKind(MediaSource mediaSource) =>
        mediaSource switch
        {
            PlexMediaSource => "Plex",
            JellyfinMediaSource => "Jellyfin",
            EmbyMediaSource => "Emby",
            _ => "Local"
        };

    private static string GetName(MediaSource mediaSource) =>
        mediaSource switch
        {
            PlexMediaSource plex => plex.ServerName,
            JellyfinMediaSource jellyfin => jellyfin.ServerName,
            EmbyMediaSource emby => emby.ServerName,
            _ => "Local"
        };
}
