using System.IO.Enumeration;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg.Selector;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using Microsoft.Extensions.Logging;
using NCalc;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

namespace ErsatzTV.Core.FFmpeg;

public class CustomStreamSelector(ILocalFileSystem localFileSystem, ILogger<CustomStreamSelector> logger)
    : ICustomStreamSelector
{
    public async Task<StreamSelectorResult> SelectStreams(
        Channel channel,
        DateTimeOffset contentStartTime,
        MediaItemAudioVersion audioVersion,
        List<Subtitle> allSubtitles)
    {
        try
        {
            string streamSelectorFile = Path.Combine(
                FileSystemLayout.ChannelStreamSelectorsFolder,
                channel.StreamSelector);

            if (!localFileSystem.FileExists(streamSelectorFile))
            {
                logger.LogWarning("YAML stream selector file {File} does not exist; aborting.", channel.StreamSelector);
                return StreamSelectorResult.None;
            }

            StreamSelector streamSelector = await LoadStreamSelector(streamSelectorFile);

            var audioStreams = audioVersion.MediaVersion.Streams
                .Where(s => s.MediaStreamKind == MediaStreamKind.Audio)
                .ToList();

            foreach (StreamSelectorItem streamSelectorItem in streamSelector.Items)
            {
                if (!string.IsNullOrWhiteSpace(streamSelectorItem.ContentCondition))
                {
                    if (!ContentMatchesCondition(channel, contentStartTime, streamSelectorItem.ContentCondition))
                    {
                        logger.LogDebug(
                            "Content does not match selector item {@SelectorItem}",
                            streamSelectorItem);
                        continue;
                    }
                }

                var candidateAudioStreams = audioStreams.ToDictionary(a => a, _ => int.MaxValue);
                var candidateSubtitles = allSubtitles.ToDictionary(s => s, _ => int.MaxValue);

                var passesAudio = false;
                var passesSubtitles = false;

                // try to find matching audio stream
                foreach (MediaStream audioStream in audioStreams.ToList())
                {
                    var matches = false;
                    string safeTitle = (audioStream.Title ?? string.Empty).ToLowerInvariant();
                    string safeLanguage = (audioStream.Language ?? "und").ToLowerInvariant();

                    if (streamSelectorItem.AudioLanguages.Count > 0)
                    {
                        // match any of the listed languages
                        for (var langIndex = 0; langIndex < streamSelectorItem.AudioLanguages.Count; langIndex++)
                        {
                            string audioLanguage = streamSelectorItem.AudioLanguages[langIndex];

                            // special case
                            if (audioLanguage == "*")
                            {
                                matches = true;
                            }

                            matches = matches || FileSystemName.MatchesSimpleExpression(
                                audioLanguage.ToLowerInvariant(),
                                safeLanguage);

                            // store lang index for prioritizing later
                            if (matches && candidateAudioStreams[audioStream] == int.MaxValue)
                            {
                                candidateAudioStreams[audioStream] = langIndex;
                            }
                        }
                    }
                    else
                    {
                        matches = true;
                    }

                    if (streamSelectorItem.AudioTitleBlocklist
                        .Any(block => safeTitle.Contains(block, StringComparison.OrdinalIgnoreCase)))
                    {
                        matches = false;
                    }

                    if (streamSelectorItem.AudioTitleAllowlist.Count > 0)
                    {
                        int matchCount = streamSelectorItem.AudioTitleAllowlist
                            .Count(block => safeTitle.Contains(block, StringComparison.OrdinalIgnoreCase));

                        if (matchCount == 0)
                        {
                            matches = false;
                        }
                    }

                    if (!string.IsNullOrWhiteSpace(streamSelectorItem.AudioCondition))
                    {
                        if (!AudioMatchesCondition(audioStream, streamSelectorItem.AudioCondition))
                        {
                            matches = false;
                        }
                    }

                    if (!matches)
                    {
                        candidateAudioStreams.Remove(audioStream);

                        logger.LogDebug(
                            "Audio stream {@Stream} does not match selector item {@SelectorItem}",
                            new { Language = safeLanguage, Title = safeTitle },
                            streamSelectorItem);
                    }
                    else
                    {
                        passesAudio = true;

                        logger.LogDebug(
                            "Audio stream {@Stream} matches selector item {@SelectorItem}",
                            new { Language = safeLanguage, Title = safeTitle },
                            streamSelectorItem);
                    }
                }

                // try to find matching subtitle stream
                if (streamSelectorItem.DisableSubtitles)
                {
                    candidateSubtitles.Clear();
                    passesSubtitles = true;
                }
                else
                {
                    foreach (Subtitle subtitle in allSubtitles.ToList())
                    {
                        var matches = false;
                        string safeTitle = (subtitle.Title ?? string.Empty).ToLowerInvariant();
                        string safeLanguage = (subtitle.Language ?? "und").ToLowerInvariant();

                        if (streamSelectorItem.SubtitleLanguages.Count > 0)
                        {
                            // match any of the listed languages
                            for (var langIndex = 0; langIndex < streamSelectorItem.SubtitleLanguages.Count; langIndex++)
                            {
                                string subtitleLanguage = streamSelectorItem.SubtitleLanguages[langIndex];

                                // special case
                                if (subtitleLanguage == "*")
                                {
                                    matches = true;
                                }

                                matches = matches || FileSystemName.MatchesSimpleExpression(
                                    subtitleLanguage,
                                    safeLanguage);

                                // store lang index for prioritizing later
                                if (matches && candidateSubtitles[subtitle] == int.MaxValue)
                                {
                                    candidateSubtitles[subtitle] = langIndex;
                                }
                            }
                        }
                        else
                        {
                            matches = true;
                        }

                        if (streamSelectorItem.SubtitleTitleBlocklist
                            .Any(block => safeTitle.Contains(block, StringComparison.OrdinalIgnoreCase)))
                        {
                            matches = false;
                        }

                        if (streamSelectorItem.SubtitleTitleAllowlist.Count > 0)
                        {
                            int matchCount = streamSelectorItem.SubtitleTitleAllowlist
                                .Count(block => safeTitle.Contains(block, StringComparison.OrdinalIgnoreCase));

                            if (matchCount == 0)
                            {
                                matches = false;
                            }
                        }

                        if (!string.IsNullOrWhiteSpace(streamSelectorItem.SubtitleCondition))
                        {
                            if (!SubtitleMatchesCondition(subtitle, streamSelectorItem.SubtitleCondition))
                            {
                                matches = false;
                            }
                        }

                        if (!matches)
                        {
                            candidateSubtitles.Remove(subtitle);

                            logger.LogDebug(
                                "Subtitle {@Subtitle} does not match selector item {@SelectorItem}",
                                new { Language = safeLanguage, Title = safeTitle },
                                streamSelectorItem);
                        }
                        else
                        {
                            passesSubtitles = true;

                            logger.LogDebug(
                                "Subtitle {@Subtitle} matches selector item {@SelectorItem}",
                                new { Language = safeLanguage, Title = safeTitle },
                                streamSelectorItem);
                        }
                    }
                }

                Option<MediaStream> maybeAudioStream = candidateAudioStreams
                    .OrderBy(a => a.Value)
                    .Select(a => a.Key)
                    .HeadOrNone();

                Option<Subtitle> maybeSubtitle = candidateSubtitles
                    .OrderBy(s => s.Value)
                    .Select(s => s.Key)
                    .HeadOrNone();

                if (passesAudio && passesSubtitles)
                {
                    return new StreamSelectorResult(maybeAudioStream, maybeSubtitle);
                }
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Unexpected error selecting streams");
        }

        return StreamSelectorResult.None;
    }

    private static bool AudioMatchesCondition(MediaStream audioStream, string audioCondition)
    {
        var expression = new Expression(audioCondition);
        expression.EvaluateParameter += (name, e) =>
        {
            e.Result = name switch
            {
                "id" => audioStream.Index,
                "title" => (audioStream.Title ?? string.Empty).ToLowerInvariant(),
                "lang" => (audioStream.Language ?? string.Empty).ToLowerInvariant(),
                "default" => audioStream.Default,
                "forced" => audioStream.Forced,
                "codec" => (audioStream.Codec ?? string.Empty).ToLowerInvariant(),
                "channels" => audioStream.Channels,
                _ => e.Result
            };
        };

        return expression.Evaluate() as bool? == true;
    }

    private static bool SubtitleMatchesCondition(Subtitle subtitle, string subtitleCondition)
    {
        var expression = new Expression(subtitleCondition);
        expression.EvaluateParameter += (name, e) =>
        {
            e.Result = name switch
            {
                "id" => subtitle.StreamIndex,
                "title" => (subtitle.Title ?? string.Empty).ToLowerInvariant(),
                "lang" => (subtitle.Language ?? string.Empty).ToLowerInvariant(),
                "default" => subtitle.Default,
                "forced" => subtitle.Forced,
                "sdh" => subtitle.SDH,
                "codec" => (subtitle.Codec ?? string.Empty).ToLowerInvariant(),
                "external" => subtitle.SubtitleKind is SubtitleKind.Sidecar,
                _ => e.Result
            };
        };

        return expression.Evaluate() as bool? == true;
    }

    private static bool ContentMatchesCondition(
        Channel channel,
        DateTimeOffset contentStartTime,
        string contentCondition)
    {
        var expression = new Expression(contentCondition);
        expression.EvaluateParameter += (name, e) =>
        {
            e.Result = name switch
            {
                "channel_number" => channel.Number,
                "channel_name" => channel.Name,
                "time_of_day_seconds" => contentStartTime.LocalDateTime.TimeOfDay.TotalSeconds,
                _ => e.Result
            };
        };

        return expression.Evaluate() as bool? == true;
    }

    private async Task<StreamSelector> LoadStreamSelector(string streamSelectorFile)
    {
        try
        {
            string yaml = await localFileSystem.ReadAllText(streamSelectorFile);

            IDeserializer deserializer = new DeserializerBuilder()
                .WithNamingConvention(CamelCaseNamingConvention.Instance)
                .Build();

            return deserializer.Deserialize<StreamSelector>(yaml);
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Error loading YAML stream selector");
            throw;
        }
    }
}
