using System.Security;
using System.Text.Json;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
using MBN_STOCK_WEBVIEW.Playout.Safety;

namespace MBN_STOCK_WEBVIEW.Playout.Configuration;

internal enum DevelopmentLiveBootstrapStatus
{
    Skipped,
    Applied,
    GateABypassed,
    Rejected
}

internal readonly record struct DevelopmentLiveBootstrapResult(
    DevelopmentLiveBootstrapStatus Status,
    string Code)
{
    public bool IsApplied => Status == DevelopmentLiveBootstrapStatus.Applied;

    public bool IsLaunchAllowed =>
        Status is DevelopmentLiveBootstrapStatus.Applied or
            DevelopmentLiveBootstrapStatus.GateABypassed;

    public bool IsRequested =>
        Status is DevelopmentLiveBootstrapStatus.Applied or
            DevelopmentLiveBootstrapStatus.Rejected;

    public string? FailureMessage => Status == DevelopmentLiveBootstrapStatus.Rejected
        ? Code switch
        {
            "development-live-arguments-invalid" =>
                "Live startup was rejected because the launch arguments were not recognized.",
            "development-live-gate-a-inspection-failed" =>
                "Gate A launch authorization could not be inspected.",
            "development-live-authorization-unavailable" =>
                "Live startup authorization is unavailable.",
            "development-live-authorization-invalid" =>
                "Live startup authorization is invalid.",
            "development-live-environment-failed" =>
                "Live startup authorization could not be applied to this process.",
            _ => "Live startup authorization failed."
        }
        : null;
}

internal interface IDevelopmentLiveBootstrapPlatform
{
    byte[] ReadAuthorizationFile(string path, int maximumBytes);

    string? GetProcessEnvironmentVariable(string name);

    void SetProcessEnvironmentVariable(string name, string? value);
}

/// <summary>
/// Arms the existing process-scoped Live and vendor-binary gates for a full-runtime
/// application launch. Both ordinary no-argument launches and the retained exact
/// development-live argument use the same strict local authorization file, which
/// is deliberately kept outside the package and source tree.
/// </summary>
internal static class DevelopmentLiveLaunchBootstrap
{
    internal const string ExactLaunchArgument = "--development-live";
    internal const string ModeEnvironmentVariable = "MBN_STOCK_PLAYOUT_MODE";
    internal const string RequiredMode = "Live";
    internal const int SchemaVersion = 1;
    internal const int MaximumAuthorizationFileBytes = 4 * 1024;

    private const string SchemaVersionProperty = "schemaVersion";
    private const string ModeProperty = "mode";
    private const string AuthorizationProperty = "authorization";
    private const string NativeSha256Property = "nativeSha256";
    private const string InteropSha256Property = "interopSha256";
    private const string InvalidLaunchArgumentsMarker =
        "\0MBN_STOCK_WEBVIEW_INVALID_LAUNCH_ARGUMENTS";

    private static readonly IReadOnlySet<string> AllowedProperties =
        new HashSet<string>(StringComparer.Ordinal)
        {
            SchemaVersionProperty,
            ModeProperty,
            AuthorizationProperty,
            NativeSha256Property,
            InteropSha256Property
        };

    private static readonly string[] ArmedEnvironmentNames =
    [
        InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable,
        InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable,
        ModeEnvironmentVariable,
        PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable
    ];

    private static readonly string[] GateAEnvironmentNames =
    [
        PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable,
        PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable,
        PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable,
        PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable
    ];

    internal static string DefaultPath => Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        "MBN_STOCK_WEBVIEW",
        "Config",
        "playout.development-live.local.json");

    internal static string? ResolveLaunchArguments(
        string? activationArguments,
        IReadOnlyList<string> processArguments)
    {
        ArgumentNullException.ThrowIfNull(processArguments);

        // Packaged WinUI launches can expose an empty LaunchActivatedEventArgs.Arguments
        // even though the full-trust process received the Visual Studio command-line
        // argument. Fall back only to one exact process argument; never concatenate,
        // trim, or ignore additional command-line input.
        if (!string.IsNullOrEmpty(activationArguments))
        {
            return activationArguments;
        }

        return processArguments.Count switch
        {
            <= 1 => null,
            2 => processArguments[1],
            _ => InvalidLaunchArgumentsMarker
        };
    }

    internal static DevelopmentLiveBootstrapResult TryApply(
        string? launchArguments) =>
        TryApply(
            launchArguments,
            isDebugBuild: true,
            DefaultPath,
            new SystemDevelopmentLiveBootstrapPlatform());

    internal static DevelopmentLiveBootstrapResult TryApply(
        string? launchArguments,
        bool isDebugBuild) =>
        TryApply(
            launchArguments,
            isDebugBuild,
            DefaultPath,
            new SystemDevelopmentLiveBootstrapPlatform());

    internal static DevelopmentLiveBootstrapResult TryApply(
        string? launchArguments,
        bool isDebugBuild,
        string authorizationFilePath,
        IDevelopmentLiveBootstrapPlatform platform)
    {
        ArgumentNullException.ThrowIfNull(platform);

        // Retain the isDebugBuild parameter for callers compiled against the
        // previous API. Release and Debug now intentionally share this exact gate.
        _ = isDebugBuild;

        if (!string.IsNullOrEmpty(launchArguments) &&
            !string.Equals(
                launchArguments,
                ExactLaunchArgument,
                StringComparison.Ordinal))
        {
            return Rejected("development-live-arguments-invalid");
        }

        if (!TryHasGateAEnvironment(platform, out var hasGateAEnvironment))
        {
            return Rejected("development-live-gate-a-inspection-failed");
        }

        // Gate A owns and consumes its process-scoped bearer values in
        // PlayoutLaunchAuthorization. Do not read the ordinary authorization file
        // and do not set, clear or roll back any environment value in this branch.
        if (hasGateAEnvironment)
        {
            return new DevelopmentLiveBootstrapResult(
                DevelopmentLiveBootstrapStatus.GateABypassed,
                "development-live-gate-a-bypassed");
        }

        if (string.IsNullOrWhiteSpace(authorizationFilePath))
        {
            return RejectedAndDisarm(
                platform,
                "development-live-authorization-unavailable");
        }

        byte[] json;
        try
        {
            json = platform.ReadAuthorizationFile(
                authorizationFilePath,
                MaximumAuthorizationFileBytes);
        }
        catch (Exception exception) when (IsExpectedFileFailure(exception))
        {
            return RejectedAndDisarm(
                platform,
                "development-live-authorization-unavailable");
        }

        if (!TryParseAuthorization(json, out var nativeSha256, out var interopSha256))
        {
            return RejectedAndDisarm(
                platform,
                "development-live-authorization-invalid");
        }

        return TryApplyEnvironment(platform, nativeSha256, interopSha256)
            ? new DevelopmentLiveBootstrapResult(
                DevelopmentLiveBootstrapStatus.Applied,
                "development-live-applied")
            : RejectedAndDisarm(
                platform,
                "development-live-environment-failed");
    }

    private static bool TryParseAuthorization(
        byte[] json,
        out string nativeSha256,
        out string interopSha256)
    {
        nativeSha256 = string.Empty;
        interopSha256 = string.Empty;
        if (json.Length is 0 or > MaximumAuthorizationFileBytes)
        {
            return false;
        }

        try
        {
            using var document = JsonDocument.Parse(
                json,
                new JsonDocumentOptions
                {
                    AllowTrailingCommas = false,
                    CommentHandling = JsonCommentHandling.Disallow,
                    MaxDepth = 4
                });
            if (document.RootElement.ValueKind != JsonValueKind.Object)
            {
                return false;
            }

            var seen = new HashSet<string>(StringComparer.Ordinal);
            int? schemaVersion = null;
            string? mode = null;
            string? authorization = null;
            string? native = null;
            string? interop = null;
            foreach (var property in document.RootElement.EnumerateObject())
            {
                if (!AllowedProperties.Contains(property.Name) || !seen.Add(property.Name))
                {
                    return false;
                }

                switch (property.Name)
                {
                    case SchemaVersionProperty:
                        if (property.Value.ValueKind != JsonValueKind.Number ||
                            !property.Value.TryGetInt32(out var parsedVersion))
                        {
                            return false;
                        }

                        schemaVersion = parsedVersion;
                        break;
                    case ModeProperty:
                        mode = StrictString(property.Value);
                        break;
                    case AuthorizationProperty:
                        authorization = StrictString(property.Value);
                        break;
                    case NativeSha256Property:
                        native = StrictString(property.Value);
                        break;
                    case InteropSha256Property:
                        interop = StrictString(property.Value);
                        break;
                }
            }

            if (seen.Count != AllowedProperties.Count ||
                schemaVersion != SchemaVersion ||
                !string.Equals(mode, RequiredMode, StringComparison.Ordinal) ||
                !string.Equals(
                    authorization,
                    PlayoutOptionsLoader.RequiredLiveAuthorizationValue,
                    StringComparison.Ordinal) ||
                !IsSha256(native) ||
                !IsSha256(interop))
            {
                return false;
            }

            nativeSha256 = native!.ToUpperInvariant();
            interopSha256 = interop!.ToUpperInvariant();
            return true;
        }
        catch (JsonException)
        {
            return false;
        }
    }

    private static bool TryApplyEnvironment(
        IDevelopmentLiveBootstrapPlatform platform,
        string nativeSha256,
        string interopSha256)
    {
        var originals = new Dictionary<string, string?>(StringComparer.Ordinal);
        try
        {
            foreach (var name in ArmedEnvironmentNames)
            {
                originals.Add(name, platform.GetProcessEnvironmentVariable(name));
            }

            // The authorization value is the final write. A partially applied setup
            // therefore cannot newly satisfy the two-part Live gate.
            platform.SetProcessEnvironmentVariable(
                InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable,
                nativeSha256);
            platform.SetProcessEnvironmentVariable(
                InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable,
                interopSha256);
            platform.SetProcessEnvironmentVariable(ModeEnvironmentVariable, RequiredMode);
            platform.SetProcessEnvironmentVariable(
                PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
                PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
            return true;
        }
        catch (Exception exception) when (IsExpectedEnvironmentFailure(exception))
        {
            for (var index = ArmedEnvironmentNames.Length - 1; index >= 0; index--)
            {
                var name = ArmedEnvironmentNames[index];
                if (!originals.TryGetValue(name, out var original))
                {
                    continue;
                }

                try
                {
                    platform.SetProcessEnvironmentVariable(name, original);
                }
                catch (Exception rollbackException) when (
                    IsExpectedEnvironmentFailure(rollbackException))
                {
                    // The real process environment API is expected to be all-or-nothing
                    // for these bounded names and values. Never turn a rollback failure
                    // into a second startup attempt or expose a value in diagnostics.
                }
            }

            return false;
        }
    }

    private static bool TryHasGateAEnvironment(
        IDevelopmentLiveBootstrapPlatform platform,
        out bool hasGateAEnvironment)
    {
        hasGateAEnvironment = false;
        try
        {
            foreach (var name in GateAEnvironmentNames)
            {
                if (platform.GetProcessEnvironmentVariable(name) is not null)
                {
                    hasGateAEnvironment = true;
                }
            }

            return true;
        }
        catch (Exception exception) when (IsExpectedEnvironmentFailure(exception))
        {
            hasGateAEnvironment = false;
            return false;
        }
    }

    private static DevelopmentLiveBootstrapResult RejectedAndDisarm(
        IDevelopmentLiveBootstrapPlatform platform,
        string code)
    {
        // A required Live request that failed validation must not fall through to
        // inherited process state from an earlier shell or Visual Studio session.
        // The app treats this requested-but-not-applied state as a startup failure.
        for (var index = ArmedEnvironmentNames.Length - 1; index >= 0; index--)
        {
            var name = ArmedEnvironmentNames[index];
            try
            {
                platform.SetProcessEnvironmentVariable(name, null);
            }
            catch (Exception exception) when (IsExpectedEnvironmentFailure(exception))
            {
                // MainWindow's request provenance remains the final fail-closed
                // boundary even if the host denies cleanup of a stale value.
            }
        }

        return Rejected(code);
    }

    private static string? StrictString(JsonElement value) =>
        value.ValueKind == JsonValueKind.String ? value.GetString() : null;

    private static bool IsSha256(string? value)
    {
        if (value is null || value.Length != 64)
        {
            return false;
        }

        foreach (var character in value)
        {
            if (!((character >= '0' && character <= '9') ||
                  (character >= 'A' && character <= 'F') ||
                  (character >= 'a' && character <= 'f')))
            {
                return false;
            }
        }

        return true;
    }

    private static bool IsExpectedFileFailure(Exception exception) =>
        exception is IOException or UnauthorizedAccessException or SecurityException or
            InvalidDataException or ArgumentException or NotSupportedException;

    private static bool IsExpectedEnvironmentFailure(Exception exception) =>
        exception is ArgumentException or SecurityException or InvalidOperationException;

    private static DevelopmentLiveBootstrapResult Rejected(string code) =>
        new(DevelopmentLiveBootstrapStatus.Rejected, code);
}

internal sealed class SystemDevelopmentLiveBootstrapPlatform :
    IDevelopmentLiveBootstrapPlatform
{
    public byte[] ReadAuthorizationFile(string path, int maximumBytes)
    {
        var fullPath = Path.GetFullPath(path);
        EnsureTrustedLocalPath(fullPath);
        var attributes = File.GetAttributes(fullPath);
        if ((attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0)
        {
            throw new InvalidDataException();
        }

        using var stream = new FileStream(
            fullPath,
            FileMode.Open,
            FileAccess.Read,
            FileShare.Read,
            bufferSize: 4096,
            FileOptions.SequentialScan);
        if (stream.Length is <= 0 || stream.Length > maximumBytes)
        {
            throw new InvalidDataException();
        }

        var bytes = new byte[checked((int)stream.Length)];
        stream.ReadExactly(bytes);
        return bytes;
    }

    public string? GetProcessEnvironmentVariable(string name) =>
        Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process);

    public void SetProcessEnvironmentVariable(string name, string? value) =>
        Environment.SetEnvironmentVariable(
            name,
            value,
            EnvironmentVariableTarget.Process);

    private static void EnsureTrustedLocalPath(string fullPath)
    {
        var localRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)));
        var rootPrefix = localRoot + Path.DirectorySeparatorChar;
        if (!fullPath.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidDataException();
        }

        var directory = Path.GetDirectoryName(fullPath);
        while (!string.IsNullOrEmpty(directory) &&
               !string.Equals(directory, localRoot, StringComparison.OrdinalIgnoreCase))
        {
            if ((File.GetAttributes(directory) & FileAttributes.ReparsePoint) != 0)
            {
                throw new InvalidDataException();
            }

            directory = Path.GetDirectoryName(directory);
        }

        if (!string.Equals(directory, localRoot, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidDataException();
        }
    }
}
