using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Newtonsoft.Json;

namespace ErsatzTV.Core.Scheduling;

internal static class HistoryDetails
{
    private static readonly JsonSerializerSettings JsonSettings = new()
    {
        NullValueHandling = NullValueHandling.Ignore
    };

    public static string KeyForBlockItem(BlockItem blockItem)
    {
        dynamic key = new
        {
            blockItem.BlockId,
            blockItem.PlaybackOrder,
            blockItem.CollectionType,
            blockItem.CollectionId,
            blockItem.MultiCollectionId,
            blockItem.SmartCollectionId,
            blockItem.MediaItemId,
            blockItem.SearchQuery
        };

        return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
    }

    public static string KeyForYamlContent(YamlPlayoutContentItem contentItem)
    {
        var key = new Dictionary<string, object>
        {
            { "Key", contentItem.Key },
            { "Order", contentItem.Order }
        };

        // we need to ignore history when any of these properties change
        if (contentItem is YamlPlayoutContentMarathonItem marathonItem)
        {
            key["ItemOrder"] = marathonItem.ItemOrder;
            //key.ShuffleGroups = marathonItem.ShuffleGroups;
            key["GroupBy"] = marathonItem.GroupBy;
            //key.PlayAllItems = marathonItem.PlayAllItems;
        }

        return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
    }

    public static string KeyForSchedulingContent(string key, PlaybackOrder playbackOrder)
    {
        var historyKey = new Dictionary<string, object>
        {
            { "Key", key },
            { "Order", playbackOrder.ToString() }
        };

        return JsonConvert.SerializeObject(historyKey, Formatting.None, JsonSettings);
    }

    public static string KeyForSchedulingMarathonContent(string key, PlaybackOrder itemPlaybackOrder, string groupBy)
    {
        var historyKey = new Dictionary<string, object>
        {
            { "Key", key },
            { "Order", nameof(PlaybackOrder.None) },

            // we need to ignore history when any of these properties change
            { "ItemOrder", itemPlaybackOrder.ToString() },
            { "GroupBy", groupBy }
        };

        return JsonConvert.SerializeObject(historyKey, Formatting.None, JsonSettings);
    }

    public static string KeyForSchedulingPlaylistContent(string key)
    {
        var historyKey = new Dictionary<string, object>
        {
            { "Key", key },
            { "Order", nameof(PlaybackOrder.None) }
        };

        return JsonConvert.SerializeObject(historyKey, Formatting.None, JsonSettings);
    }

    public static string KeyForCollectionKey(CollectionKey collectionKey)
    {
        dynamic key = new
        {
            collectionKey.CollectionType,
            collectionKey.CollectionId,
            collectionKey.MultiCollectionId,
            collectionKey.SmartCollectionId,
            collectionKey.MediaItemId,
            collectionKey.PlaylistId,
            collectionKey.SearchQuery,
            collectionKey.FakeCollectionKey
        };

        return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
    }

    public static string ForBreakContent(DecoBreakContent breakContent)
    {
        dynamic key = new
        {
            DecoBreakContentId = breakContent.Id,
            PlaybackOrder = PlaybackOrder.Shuffle,
            breakContent.CollectionType,
            breakContent.CollectionId,
            breakContent.MultiCollectionId,
            breakContent.SmartCollectionId,
            breakContent.MediaItemId,
            breakContent.PlaylistId
        };

        return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
    }

    public static string ForDefaultFiller(Deco deco)
    {
        dynamic key = new
        {
            DecoId = deco.Id,
            PlaybackOrder = PlaybackOrder.Shuffle,
            CollectionType = deco.DefaultFillerCollectionType,
            CollectionId = deco.DefaultFillerCollectionId,
            MultiCollectionId = deco.DefaultFillerMultiCollectionId,
            SmartCollectionId = deco.DefaultFillerSmartCollectionId
        };

        return JsonConvert.SerializeObject(key, Formatting.None, JsonSettings);
    }

    public static string ForMediaItem(MediaItem mediaItem)
    {
        Details details = mediaItem switch
        {
            Episode e => ForEpisode(e),
            Movie m => ForMovie(m),
            _ => new Details(mediaItem.Id, null, null, null)
        };

        return JsonConvert.SerializeObject(details, Formatting.None, JsonSettings);
    }

    public static void MoveToNextItem(
        List<MediaItem> collectionItems,
        string detailsString,
        IMediaCollectionEnumerator enumerator,
        PlaybackOrder playbackOrder,
        bool current = false)
    {
        if (playbackOrder is PlaybackOrder.Random)
        {
            return;
        }

        Option<MediaItem> maybeMatchedItem = Option<MediaItem>.None;
        var copy = collectionItems.ToList();

        Details details = JsonConvert.DeserializeObject<Details>(detailsString);

        // try for an exact match first
        if (details.MediaItemId != null)
        {
            maybeMatchedItem = collectionItems.Find(mi => mi.Id == details.MediaItemId);
        }

        if (maybeMatchedItem.IsNone && details.SeasonNumber.HasValue && details.EpisodeNumber.HasValue)
        {
            int season = details.SeasonNumber.Value;
            int episode = details.EpisodeNumber.Value;

            maybeMatchedItem = Optional(collectionItems.Find(ci => MatchSeasonAndEpisode(ci, season, episode)));

            if (maybeMatchedItem.IsNone)
            {
                var fakeItem = new Episode
                {
                    Season = new Season { SeasonNumber = season },
                    EpisodeMetadata =
                    [
                        new EpisodeMetadata
                        {
                            EpisodeNumber = episode,
                            ReleaseDate = details.ReleaseDate
                        }
                    ]
                };

                copy.Add(fakeItem);
                maybeMatchedItem = fakeItem;
            }
        }
        else if (maybeMatchedItem.IsNone && playbackOrder is PlaybackOrder.Chronological &&
                 details.ReleaseDate.HasValue)
        {
            maybeMatchedItem = Optional(collectionItems.Find(ci => MatchReleaseDate(ci, details.ReleaseDate.Value)));

            if (maybeMatchedItem.IsNone)
            {
                var fakeItem = new Movie { MovieMetadata = [new MovieMetadata { ReleaseDate = details.ReleaseDate }] };
                copy.Add(fakeItem);
                maybeMatchedItem = fakeItem;
            }
        }

        foreach (MediaItem matchedItem in maybeMatchedItem)
        {
            IComparer<MediaItem> comparer = playbackOrder switch
            {
                PlaybackOrder.Chronological => new ChronologicalMediaComparer(),
                _ => new SeasonEpisodeMediaComparer()
            };

            copy.Sort(comparer);

            var state = new CollectionEnumeratorState
            {
                Seed = enumerator.State.Seed,
                Index = copy.IndexOf(matchedItem)
            };
            enumerator.ResetState(state);

            if (!current)
            {
                enumerator.MoveNext(Option<DateTimeOffset>.None);
            }
        }
    }

    private static bool MatchReleaseDate(MediaItem mediaItem, DateTime releaseDate) =>
        mediaItem switch
        {
            Movie m => m.MovieMetadata.Any(mm => mm.ReleaseDate == releaseDate),
            Episode e => e.EpisodeMetadata.Any(em => em.ReleaseDate == releaseDate),
            //MusicVideo mv => mv.MusicVideoMetadata.Any(mvm => mvm.ReleaseDate == releaseDate),
            OtherVideo ov => ov.OtherVideoMetadata.Any(ovm => ovm.ReleaseDate == releaseDate),
            _ => false
        };

    private static bool MatchSeasonAndEpisode(MediaItem mediaItem, int seasonNumber, int episodeNumber) =>
        mediaItem switch
        {
            Episode e => e.Season.SeasonNumber == seasonNumber &&
                         e.EpisodeMetadata.Any(em => em.EpisodeNumber == episodeNumber),
            _ => false
        };

    private static Details ForEpisode(Episode e)
    {
        int? episodeNumber = null;
        DateTime? releaseDate = null;
        foreach (EpisodeMetadata episodeMetadata in e.EpisodeMetadata.HeadOrNone())
        {
            episodeNumber = episodeMetadata.EpisodeNumber;
            releaseDate = episodeMetadata.ReleaseDate;
        }

        return new Details(e.Id, releaseDate, e.Season.SeasonNumber, episodeNumber);
    }

    private static Details ForMovie(Movie m)
    {
        DateTime? releaseDate = null;
        foreach (MovieMetadata movieMetadata in m.MovieMetadata.HeadOrNone())
        {
            releaseDate = movieMetadata.ReleaseDate;
        }

        return new Details(m.Id, releaseDate, null, null);
    }

    public record Details(int? MediaItemId, DateTime? ReleaseDate, int? SeasonNumber, int? EpisodeNumber);
}
