#nullable enable

using System.Collections.ObjectModel;

namespace MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

public delegate Task<LegacySceneDataPage> LegacySceneDataLoaderDelegate(
    LegacyPlaylistEntry entry,
    int pageIndexZeroBased,
    CancellationToken cancellationToken);

public delegate Task<LegacySceneDataPage> LegacyScenePagePlanDataLoaderDelegate(
    LegacyPlaylistEntry entry,
    CancellationToken cancellationToken);

/// <summary>
/// Native-only route from one or more closed cut aliases to a parameterized DTO loader.
/// The delegate is composed in managed code; it is never selected by a Web-supplied type,
/// method name, object name or file path.
/// </summary>
public sealed class LegacySceneDataSourceRoute
{
    public LegacySceneDataSourceRoute(
        IEnumerable<string> cutCodes,
        LegacySceneDataLoaderDelegate loader,
        LegacyScenePagePlanDataLoaderDelegate? pagePlanLoader = null)
    {
        ArgumentNullException.ThrowIfNull(cutCodes);
        Loader = loader ?? throw new ArgumentNullException(nameof(loader));
        PagePlanLoader = pagePlanLoader;
        var codes = cutCodes.Select(ValidateCutCode).ToArray();
        if (codes.Length == 0 ||
            codes.Distinct(StringComparer.Ordinal).Count() != codes.Length)
        {
            throw new ArgumentException("A scene-data route requires unique cut codes.", nameof(cutCodes));
        }

        CutCodes = Array.AsReadOnly(codes);
    }

    public IReadOnlyList<string> CutCodes { get; }

    internal LegacySceneDataLoaderDelegate Loader { get; }

    internal LegacyScenePagePlanDataLoaderDelegate? PagePlanLoader { get; }

    private static string ValidateCutCode(string value)
    {
        if (string.IsNullOrWhiteSpace(value) || value.Length > 64 ||
            value.Any(character => !char.IsAsciiLetterOrDigit(character)))
        {
            throw new ArgumentException("A scene-data route contains an invalid cut code.");
        }

        return value;
    }
}

/// <summary>
/// Immutable dispatcher used by the app composition root to join scene-specific DB loaders.
/// Unknown cuts fail closed before any query is issued.
/// </summary>
public sealed class LegacySceneDataSourceRouter :
    ILegacySceneDataSource,
    ILegacyScenePagePlanDataSource
{
    private readonly IReadOnlyDictionary<string, LegacySceneDataLoaderDelegate> _loaders;
    private readonly IReadOnlyDictionary<string, LegacyScenePagePlanDataLoaderDelegate> _pagePlanLoaders;

    public LegacySceneDataSourceRouter(IEnumerable<LegacySceneDataSourceRoute> routes)
    {
        ArgumentNullException.ThrowIfNull(routes);
        var loaders = new Dictionary<string, LegacySceneDataLoaderDelegate>(StringComparer.Ordinal);
        var pagePlanLoaders = new Dictionary<string, LegacyScenePagePlanDataLoaderDelegate>(
            StringComparer.Ordinal);
        foreach (var route in routes)
        {
            ArgumentNullException.ThrowIfNull(route);
            foreach (var code in route.CutCodes)
            {
                if (!loaders.TryAdd(code, route.Loader))
                {
                    throw new ArgumentException(
                        "A cut code can have only one scene-data route.",
                        nameof(routes));
                }

                if (route.PagePlanLoader is not null &&
                    !pagePlanLoaders.TryAdd(code, route.PagePlanLoader))
                {
                    throw new ArgumentException(
                        "A cut code can have only one scene page-plan route.",
                        nameof(routes));
                }
            }
        }

        if (loaders.Count == 0)
        {
            throw new ArgumentException("At least one scene-data route is required.", nameof(routes));
        }

        _loaders = new ReadOnlyDictionary<string, LegacySceneDataLoaderDelegate>(loaders);
        _pagePlanLoaders = new ReadOnlyDictionary<string, LegacyScenePagePlanDataLoaderDelegate>(
            pagePlanLoaders);
        CutCodes = Array.AsReadOnly(loaders.Keys.OrderBy(code => code, StringComparer.Ordinal).ToArray());
    }

    public IReadOnlyList<string> CutCodes { get; }

    public Task<LegacySceneDataPage> LoadPageAsync(
        LegacyPlaylistEntry entry,
        int pageIndexZeroBased,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(entry);
        if (pageIndexZeroBased < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(pageIndexZeroBased));
        }

        RejectUnresolvedNamedPlaylistEntry(entry);

        if (!_loaders.TryGetValue(entry.CutCode, out var loader))
        {
            if (string.Equals(
                    entry.CutCode,
                    LegacyPlaylistEntry.UnresolvedCutCode,
                    StringComparison.Ordinal))
            {
                throw new LegacySceneDataException(
                    "이 DB 재생목록 행은 기존 컷 이름으로 장면을 찾을 수 없습니다. 다른 행은 계속 사용할 수 있습니다.");
            }

            throw new LegacySceneDataException(
                "The selected cut does not have a registered scene-data loader.");
        }

        return loader(entry, pageIndexZeroBased, cancellationToken);
    }

    public Task<LegacySceneDataPage> LoadPagePlanDataAsync(
        LegacyPlaylistEntry entry,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(entry);
        RejectUnresolvedNamedPlaylistEntry(entry);
        if (!_pagePlanLoaders.TryGetValue(entry.CutCode, out var loader))
        {
            if (string.Equals(
                    entry.CutCode,
                    LegacyPlaylistEntry.UnresolvedCutCode,
                    StringComparison.Ordinal))
            {
                throw new LegacySceneDataException(
                    "이 DB 재생목록 행은 기존 컷 이름으로 장면을 찾을 수 없습니다. 다른 행은 계속 사용할 수 있습니다.");
            }

            throw new LegacySceneDataException(
                "The selected cut does not have a registered page preflight route.");
        }

        return loader(entry, cancellationToken);
    }

    private static void RejectUnresolvedNamedPlaylistEntry(LegacyPlaylistEntry entry)
    {
        if (string.Equals(
                entry.CutCode,
                LegacyPlaylistEntry.UnresolvedCutCode,
                StringComparison.Ordinal))
        {
            throw new LegacySceneDataException(
                "기존 DB 재생목록 행의 장면 이름을 확인할 수 없습니다. 다른 행은 계속 사용할 수 있습니다.");
        }
    }
}
