#nullable enable

using System.Collections.ObjectModel;

namespace MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

/// <summary>
/// Immutable runtime-to-catalog coverage result. Missing aliases identify catalog aliases
/// with no router route; unexpected aliases identify routes with no reachable MainForm
/// builder. Conditional aliases may intentionally cover more than one builder.
/// </summary>
public sealed record LegacySceneRuntimeCoverageReport(
    IReadOnlyList<string> ReachableBuilderKeys,
    IReadOnlyList<string> UnreachableBuilderKeys,
    IReadOnlyList<string> ExpectedCutCodes,
    IReadOnlyList<string> RegisteredCutCodes,
    IReadOnlyList<string> MissingCutCodes,
    IReadOnlyList<string> UnexpectedCutCodes,
    IReadOnlyList<string> UncoveredBuilderKeys,
    IReadOnlyDictionary<string, IReadOnlyList<string>> ConditionalAliasOwners)
{
    public bool IsComplete =>
        ReachableBuilderKeys.Count == LegacySceneRuntimeCoverage.ExpectedReachableBuilderCount &&
        ExpectedCutCodes.Count == LegacySceneRuntimeCoverage.ExpectedActiveAliasCount &&
        RegisteredCutCodes.Count == LegacySceneRuntimeCoverage.ExpectedActiveAliasCount &&
        MissingCutCodes.Count == 0 &&
        UnexpectedCutCodes.Count == 0 &&
        UncoveredBuilderKeys.Count == 0;
}

/// <summary>
/// Validates that the application composition root has one closed router entry for every
/// active MainForm cut alias and no others. The catalog owns alias-to-builder semantics:
/// 8018/8032/5032 intentionally cover both s5032 and s8018, while s8086 has no MainForm
/// alias and must never be registered as an active runtime cut.
/// </summary>
public static class LegacySceneRuntimeCoverage
{
    public const int ExpectedReachableBuilderCount = 34;
    public const int ExpectedActiveAliasCount = 45;

    private static readonly RuntimeCatalogContract Contract = CreateContract();

    public static IReadOnlyList<string> ExpectedCutCodes => Contract.ExpectedCutCodes;

    public static LegacySceneRuntimeCoverageReport Analyze(
        LegacySceneDataSourceRouter router)
    {
        ArgumentNullException.ThrowIfNull(router);
        var registered = Array.AsReadOnly(
            router.CutCodes.OrderBy(code => code, StringComparer.Ordinal).ToArray());
        var registeredSet = registered.ToHashSet(StringComparer.Ordinal);
        var expectedSet = Contract.ExpectedCutCodes.ToHashSet(StringComparer.Ordinal);
        var missing = Array.AsReadOnly(
            Contract.ExpectedCutCodes
                .Where(code => !registeredSet.Contains(code))
                .ToArray());
        var unexpected = Array.AsReadOnly(
            registered
                .Where(code => !expectedSet.Contains(code))
                .ToArray());
        var uncovered = Array.AsReadOnly(
            Contract.ReachableDefinitions
                .Where(definition => definition.CutAliases.Any(alias => !registeredSet.Contains(alias)))
                .Select(definition => definition.BuilderKey)
                .OrderBy(key => key, StringComparer.Ordinal)
                .ToArray());

        return new LegacySceneRuntimeCoverageReport(
            Contract.ReachableBuilderKeys,
            Contract.UnreachableBuilderKeys,
            Contract.ExpectedCutCodes,
            registered,
            missing,
            unexpected,
            uncovered,
            Contract.ConditionalAliasOwners);
    }

    public static LegacySceneRuntimeCoverageReport Validate(
        LegacySceneDataSourceRouter router)
    {
        var report = Analyze(router);
        if (!report.IsComplete)
        {
            throw new InvalidOperationException(
                "The legacy scene-data router does not exactly cover the active MainForm aliases.");
        }

        return report;
    }

    private static RuntimeCatalogContract CreateContract()
    {
        var definitions = LegacySceneCatalog.Definitions;
        var reachable = definitions
            .Where(definition => definition.Reachability is
                LegacySceneReachability.MainFormDispatch or
                LegacySceneReachability.ConditionalMainFormDispatch)
            .OrderBy(definition => definition.BuilderKey, StringComparer.Ordinal)
            .ToArray();
        var unreachable = definitions
            .Where(definition => definition.Reachability ==
                LegacySceneReachability.NoMainFormDispatch)
            .OrderBy(definition => definition.BuilderKey, StringComparer.Ordinal)
            .ToArray();
        var aliases = reachable
            .SelectMany(definition => definition.CutAliases)
            .Distinct(StringComparer.Ordinal)
            .OrderBy(alias => alias, StringComparer.Ordinal)
            .ToArray();

        if (reachable.Length != ExpectedReachableBuilderCount ||
            unreachable.Length != 1 ||
            unreachable[0].BuilderKey != "s8086" ||
            unreachable[0].CutAliases.Count != 0 ||
            reachable.Any(definition => definition.CutAliases.Count == 0) ||
            aliases.Length != ExpectedActiveAliasCount ||
            aliases.Contains("8086", StringComparer.Ordinal))
        {
            throw new InvalidOperationException(
                "The legacy scene catalog does not match the runtime coverage baseline.");
        }

        var conditionalDefinitions = reachable
            .Where(definition => definition.Reachability ==
                LegacySceneReachability.ConditionalMainFormDispatch)
            .ToArray();
        if (conditionalDefinitions.Length != 2 ||
            !conditionalDefinitions.Select(definition => definition.BuilderKey)
                .ToHashSet(StringComparer.Ordinal)
                .SetEquals(["s5032", "s8018"]))
        {
            throw new InvalidOperationException(
                "The conditional legacy scene catalog contract is invalid.");
        }

        var aliasOwners = reachable
            .SelectMany(definition => definition.CutAliases.Select(alias =>
                (Alias: alias, definition.BuilderKey)))
            .GroupBy(value => value.Alias, StringComparer.Ordinal)
            .ToDictionary(
                group => group.Key,
                group => (IReadOnlyList<string>)Array.AsReadOnly(
                    group.Select(value => value.BuilderKey)
                        .OrderBy(key => key, StringComparer.Ordinal)
                        .ToArray()),
                StringComparer.Ordinal);
        var sharedAliases = aliasOwners
            .Where(pair => pair.Value.Count > 1)
            .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
        var expectedSharedAliases = new HashSet<string>(
            ["5032", "8018", "8032"],
            StringComparer.Ordinal);
        if (!sharedAliases.Keys.ToHashSet(StringComparer.Ordinal).SetEquals(expectedSharedAliases) ||
            sharedAliases.Values.Any(owners =>
                !owners.ToHashSet(StringComparer.Ordinal).SetEquals(["s5032", "s8018"])))
        {
            throw new InvalidOperationException(
                "The shared conditional cut-alias contract is invalid.");
        }

        return new RuntimeCatalogContract(
            Array.AsReadOnly(reachable),
            Array.AsReadOnly(reachable.Select(definition => definition.BuilderKey).ToArray()),
            Array.AsReadOnly(unreachable.Select(definition => definition.BuilderKey).ToArray()),
            Array.AsReadOnly(aliases),
            new ReadOnlyDictionary<string, IReadOnlyList<string>>(sharedAliases));
    }

    private sealed record RuntimeCatalogContract(
        IReadOnlyList<LegacySceneDefinition> ReachableDefinitions,
        IReadOnlyList<string> ReachableBuilderKeys,
        IReadOnlyList<string> UnreachableBuilderKeys,
        IReadOnlyList<string> ExpectedCutCodes,
        IReadOnlyDictionary<string, IReadOnlyList<string>> ConditionalAliasOwners);
}
