using ErsatzTV.Core.Api.Search;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace ErsatzTV.Application.Search.Queries;

public class GetSearchFieldValuesHandler(IDbContextFactory<TvContext> dbContextFactory)
    : IRequestHandler<GetSearchFieldValues, Option<SearchFieldValuesResponseModel>>
{
    private const int DefaultLimit = 50;
    private const int MaxLimit = 50;

    public async Task<Option<SearchFieldValuesResponseModel>> Handle(
        GetSearchFieldValues request,
        CancellationToken cancellationToken)
    {
        SearchFieldResponseModel field = SearchFieldCatalog.Fields
            .FirstOrDefault(f => f.Name == request.Name);

        if (field is null || field.Type != "text")
        {
            return Option<SearchFieldValuesResponseModel>.None;
        }

        int limit = request.Limit <= 0 ? DefaultLimit : Math.Clamp(request.Limit, 1, MaxLimit);
        string qLower = (request.Query ?? string.Empty).ToLower();

        // in-memory special cases (no DB query needed)
        switch (request.Name)
        {
            case "state":
                return new SearchFieldValuesResponseModel(
                    FilterSortTake(Enum.GetNames<MediaItemState>(), qLower, limit));
            case "video_dynamic_range":
                return new SearchFieldValuesResponseModel(
                    FilterSortTake(["hdr", "sdr"], qLower, limit));
        }

        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        if (request.Name == "content_rating")
        {
            return new SearchFieldValuesResponseModel(
                await GetContentRatingValues(dbContext, qLower, limit, cancellationToken));
        }

        IQueryable<string> source = GetSource(dbContext, request.Name);
        if (source is null)
        {
            return Option<SearchFieldValuesResponseModel>.None;
        }

        List<string> values = await source
            .Where(v => v != null && v.ToLower().StartsWith(qLower))
            .Distinct()
            .OrderBy(v => v)
            .Take(limit)
            .ToListAsync(cancellationToken);

        return new SearchFieldValuesResponseModel(values);
    }

    private static IQueryable<string> GetSource(TvContext dbContext, string name) => name switch
    {
        "genre" or "show_genre" => dbContext.Set<Genre>().Select(g => g.Name),
        "studio" => dbContext.Set<Studio>().Select(s => s.Name),
        "director" => dbContext.Set<Director>().Select(d => d.Name),
        "writer" => dbContext.Set<Writer>().Select(w => w.Name),
        "actor" => dbContext.Actors.Select(a => a.Name),
        // entity artists only; free-text music-video/song artist credits are not included (known limitation)
        "artist" => dbContext.ArtistMetadata.Select(m => m.Title),
        "tag" => dbContext.Set<Tag>()
            .Where(t => t.ExternalTypeId != Tag.NfoCountryTypeId && t.ExternalTypeId != Tag.PlexNetworkTypeId)
            .Select(t => t.Name),
        "network" => dbContext.Set<Tag>()
            .Where(t => t.ExternalTypeId == Tag.PlexNetworkTypeId)
            .Select(t => t.Name),
        "collection" => dbContext.Collections.Select(c => c.Name),
        "video_codec" => dbContext.MediaStreams
            .Where(s => s.MediaStreamKind == MediaStreamKind.Video && s.Codec != null)
            .Select(s => s.Codec),
        "album" => dbContext.MusicVideoMetadata
            .Where(m => m.Album != null)
            .Select(m => m.Album)
            .Concat(dbContext.SongMetadata.Where(m => m.Album != null).Select(m => m.Album)),
        _ => null
    };

    private static async Task<List<string>> GetContentRatingValues(
        TvContext dbContext,
        string qLower,
        int limit,
        CancellationToken cancellationToken)
    {
        List<string> raw = await dbContext.MovieMetadata
            .Where(m => m.ContentRating != null)
            .Select(m => m.ContentRating)
            .Concat(dbContext.ShowMetadata.Where(m => m.ContentRating != null).Select(m => m.ContentRating))
            .Concat(dbContext.OtherVideoMetadata.Where(m => m.ContentRating != null).Select(m => m.ContentRating))
            .Concat(dbContext.RemoteStreamMetadata.Where(m => m.ContentRating != null).Select(m => m.ContentRating))
            .Distinct()
            .ToListAsync(cancellationToken);

        IEnumerable<string> split = raw
            .SelectMany(cr => cr.Split('/'))
            .Select(cr => cr.Trim())
            .Where(cr => !string.IsNullOrEmpty(cr))
            .Distinct();

        return FilterSortTake(split, qLower, limit);
    }

    private static List<string> FilterSortTake(IEnumerable<string> values, string qLower, int limit) =>
        values
            .Where(v => v.ToLower().StartsWith(qLower))
            .OrderBy(v => v)
            .Take(limit)
            .ToList();
}
