using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Interop;

namespace MBN_STOCK_WEBVIEW.Playout.Safety;

/// <summary>
/// Process-lifetime authorization captured before WebView2 is created. Gate A is
/// deliberately narrower than normal Live mode: one CONNECT and one claimed
/// PREPARE of the approved 5001 cut are the only requests that can cross the
/// native engine boundary.
/// </summary>
public sealed class PlayoutLaunchAuthorization : ILiveAuthorization, IDisposable
{
    public const string GateACapabilityEnvironmentVariable =
        "MBN_LEGACY_PGM_GATE_A_PREPARE_CAPABILITY";

    public const string GateAPgmProcessIdEnvironmentVariable =
        "MBN_LEGACY_PGM_GATE_A_PGM_PID";

    public const string GateAPgmStartTimeUtcTicksEnvironmentVariable =
        "MBN_LEGACY_PGM_GATE_A_PGM_START_TIME_UTC_TICKS";

    public const string GateAExpiresAtUtcTicksEnvironmentVariable =
        "MBN_LEGACY_PGM_GATE_A_EXPIRES_AT_UTC_TICKS";

    public const string GateAApprovedSceneFilePath =
        @"C:\Users\MD\source\repos\MBN_STOCK_N\MBN_STOCK_N\bin\Debug\Cuts\5001.t2s";

    public const string GateAApprovedSceneSha256 =
        "99CE3B689A42D8C42BEB09A86FA10C2D7C1AEF4F50D324D81276C1A1E4C4D8A7";

    private const int GateAArmed = 0;
    private const int GateAOperatorClaimed = 1;
    private const int GateAEnginePrepareClaimed = 2;
    private const int GateATerminal = 3;
    private const int CapabilityLength = 64;
    private static readonly long MaximumGateALifetimeTicks = TimeSpan.FromMinutes(15).Ticks;
    private static readonly object GateACaptureSync = new();
    private static bool _gateAObservedForProcess;

    private readonly object _gateASync = new();
    private readonly byte[]? _gateACapabilityHash;
    private readonly TimeProvider _timeProvider;
    private readonly long _authorizationCapturedTimestamp;
    private readonly TimeSpan _authorizedLifetime;
    private FileStream? _gateASceneLease;
    private int _gateAState;
    private bool _connectClaimed;
    private bool _capabilityHashCleared;

    private PlayoutLaunchAuthorization(
        bool liveAuthorized,
        byte[]? gateACapabilityHash,
        int? expectedPgmProcessId,
        long? expectedPgmStartTimeUtcTicks,
        long? expiresAtUtcTicks,
        TimeProvider timeProvider,
        long authorizationCapturedTimestamp,
        TimeSpan authorizedLifetime,
        FileStream? gateASceneLease)
    {
        IsAuthorizedForThisLaunch = liveAuthorized;
        _gateACapabilityHash = gateACapabilityHash;
        ExpectedPgmProcessId = expectedPgmProcessId;
        ExpectedPgmStartTimeUtcTicks = expectedPgmStartTimeUtcTicks;
        ExpiresAtUtcTicks = expiresAtUtcTicks;
        _timeProvider = timeProvider;
        _authorizationCapturedTimestamp = authorizationCapturedTimestamp;
        _authorizedLifetime = authorizedLifetime;
        _gateASceneLease = gateASceneLease;
    }

    public bool IsAuthorizedForThisLaunch { get; }

    public bool IsGateA => _gateACapabilityHash is not null;

    public bool AllowsDatabaseWrites => !IsGateA;

    public bool AllowsCompositionMutation => !IsGateA;

    public int? ExpectedPgmProcessId { get; }

    public long? ExpectedPgmStartTimeUtcTicks { get; }

    public long? ExpiresAtUtcTicks { get; }

    public static string GateAApprovedSceneDirectory =>
        Path.GetDirectoryName(GateAApprovedSceneFilePath)!;

    internal bool AllowsAutomaticReconnect => !IsGateA;

    internal bool AllowsSdkDisconnect => !IsGateA;

    internal static PlayoutLaunchAuthorization Unrestricted { get; } =
        new(
            liveAuthorized: false,
            gateACapabilityHash: null,
            expectedPgmProcessId: null,
            expectedPgmStartTimeUtcTicks: null,
            expiresAtUtcTicks: null,
            TimeProvider.System,
            authorizationCapturedTimestamp: 0,
            authorizedLifetime: TimeSpan.Zero,
            gateASceneLease: null);

    /// <summary>
    /// Captures all authorization material once, removes the bearer values from
    /// the process environment, then acquires and hashes the approved cut through
    /// a read-only/non-write-sharing handle. The handle remains owned by this
    /// authorization for the native application lifetime.
    /// </summary>
    public static PlayoutLaunchAuthorization CaptureFromEnvironment() =>
        CaptureFromEnvironment(
            TimeProvider.System,
            GateAApprovedSceneFilePath,
            GateAApprovedSceneSha256);

    internal static PlayoutLaunchAuthorization CaptureFromEnvironment(
        TimeProvider timeProvider,
        string approvedSceneFilePath,
        string approvedSceneSha256)
    {
        ArgumentNullException.ThrowIfNull(timeProvider);
        ArgumentException.ThrowIfNullOrWhiteSpace(approvedSceneFilePath);
        ArgumentException.ThrowIfNullOrWhiteSpace(approvedSceneSha256);

        lock (GateACaptureSync)
        {
            return CaptureFromEnvironmentLocked(
                timeProvider,
                approvedSceneFilePath,
                approvedSceneSha256);
        }
    }

    internal static void ResetGateACaptureLatchForTests()
    {
        lock (GateACaptureSync)
        {
            _gateAObservedForProcess = false;
        }
    }

    private static PlayoutLaunchAuthorization CaptureFromEnvironmentLocked(
        TimeProvider timeProvider,
        string approvedSceneFilePath,
        string approvedSceneSha256)
    {
        var liveAuthorized = PlayoutOptionsLoader.IsLiveAuthorizedForThisLaunch();
        var capability = Environment.GetEnvironmentVariable(
            GateACapabilityEnvironmentVariable,
            EnvironmentVariableTarget.Process);
        var processIdText = Environment.GetEnvironmentVariable(
            GateAPgmProcessIdEnvironmentVariable,
            EnvironmentVariableTarget.Process);
        var startTimeText = Environment.GetEnvironmentVariable(
            GateAPgmStartTimeUtcTicksEnvironmentVariable,
            EnvironmentVariableTarget.Process);
        var expiresAtText = Environment.GetEnvironmentVariable(
            GateAExpiresAtUtcTicksEnvironmentVariable,
            EnvironmentVariableTarget.Process);
        var anyGateAValue = capability is not null ||
            processIdText is not null ||
            startTimeText is not null ||
            expiresAtText is not null;
        var gateAWasAlreadyObserved = _gateAObservedForProcess;
        if (anyGateAValue)
        {
            // Latch before validation, cleanup, or file I/O so even an exception in
            // those paths cannot make a second process-local capture eligible.
            _gateAObservedForProcess = true;
        }

        ClearGateAEnvironment();
        if (gateAWasAlreadyObserved || anyGateAValue)
        {
            ClearLiveAuthorizationEnvironment();
        }

        if (gateAWasAlreadyObserved)
        {
            throw new PlayoutConfigurationException(
                "Gate A launch authorization was already observed by this process.");
        }

        if (!anyGateAValue)
        {
            return new PlayoutLaunchAuthorization(
                liveAuthorized,
                gateACapabilityHash: null,
                expectedPgmProcessId: null,
                expectedPgmStartTimeUtcTicks: null,
                expiresAtUtcTicks: null,
                timeProvider,
                authorizationCapturedTimestamp: 0,
                authorizedLifetime: TimeSpan.Zero,
                gateASceneLease: null);
        }

        // Observing any Gate A launch value permanently consumes the process-wide
        // capture slot before validation or file I/O. A malformed, expired, or
        // otherwise rejected first attempt must never be corrected or re-injected
        // within the same native process.
        // The already-captured Live decision is retained only in the returned native
        // object. Its static bearer was removed above before validation or file I/O.

        var nowUtcTicks = timeProvider.GetUtcNow().UtcTicks;
        var capturedTimestamp = timeProvider.GetTimestamp();
        if (!IsExactCapability(capability) ||
            !int.TryParse(
                processIdText,
                NumberStyles.None,
                CultureInfo.InvariantCulture,
                out var processId) ||
            processId <= 0 ||
            !long.TryParse(
                startTimeText,
                NumberStyles.None,
                CultureInfo.InvariantCulture,
                out var startTimeUtcTicks) ||
            startTimeUtcTicks <= 0 ||
            !long.TryParse(
                expiresAtText,
                NumberStyles.None,
                CultureInfo.InvariantCulture,
                out var expiresAtUtcTicks) ||
            expiresAtUtcTicks <= nowUtcTicks ||
            expiresAtUtcTicks - nowUtcTicks > MaximumGateALifetimeTicks)
        {
            throw new PlayoutConfigurationException(
                "Gate A launch authorization is incomplete, expired, or invalid.");
        }

        var sceneLease = AcquireApprovedSceneLease(
            approvedSceneFilePath,
            approvedSceneSha256);
        return new PlayoutLaunchAuthorization(
            liveAuthorized,
            HashCapability(capability!),
            processId,
            startTimeUtcTicks,
            expiresAtUtcTicks,
            timeProvider,
            capturedTimestamp,
            TimeSpan.FromTicks(expiresAtUtcTicks - nowUtcTicks),
            sceneLease);
    }

    /// <summary>
    /// Atomically consumes the browser-to-native PREPARE capability. Invalid
    /// values do not consume it. Expiry is checked immediately before the state
    /// transition and permanently closes the gate.
    /// </summary>
    public bool TryClaimGateAPrepare(string? capability)
    {
        if (!IsGateA || !IsExactCapability(capability))
        {
            return false;
        }

        var candidateHash = HashCapability(capability!);
        try
        {
            lock (_gateASync)
            {
                if (TerminateIfExpiredLocked() ||
                    _gateAState != GateAArmed ||
                    !CryptographicOperations.FixedTimeEquals(
                        candidateHash,
                        _gateACapabilityHash!))
                {
                    return false;
                }

                // Re-read native time at the exact state boundary. A claim at the
                // expiry tick is rejected and can never be rearmed.
                if (TerminateIfExpiredLocked())
                {
                    return false;
                }

                _gateAState = GateAOperatorClaimed;
                return true;
            }
        }
        finally
        {
            CryptographicOperations.ZeroMemory(candidateHash);
        }
    }

    /// <summary>
    /// Permanently closes the PREPARE capability after the native request,
    /// regardless of success, rejection, cancellation, timeout, or unknown outcome.
    /// The approved cut lease intentionally remains held until native shutdown.
    /// </summary>
    public void CompleteGateAPrepare()
    {
        if (!IsGateA)
        {
            return;
        }

        lock (_gateASync)
        {
            EnterTerminalLocked();
        }
    }

    internal bool TryClaimEngineCommand(
        PlayoutOperation operation,
        PlayoutCue? cue)
    {
        if (!IsGateA)
        {
            return true;
        }

        lock (_gateASync)
        {
            if (TerminateIfExpiredLocked())
            {
                return false;
            }

            switch (operation)
            {
                case PlayoutOperation.Connect when !_connectClaimed:
                    _connectClaimed = true;
                    return true;
                case PlayoutOperation.Prepare:
                    return TryClaimEnginePrepareLocked(cue);
                default:
                    return false;
            }
        }
    }

    /// <summary>
    /// Final native guard invoked on the STA immediately before an SDK dispatch
    /// latch can be claimed. This closes the registration/queue-delay window after
    /// the outer command claim. Expiry or a terminal gate is reported as a safety
    /// exception so the engine quarantines without issuing cleanup commands.
    /// </summary>
    internal void EnsureGateASdkDispatchAuthorized()
    {
        if (!IsGateA)
        {
            return;
        }

        lock (_gateASync)
        {
            if (TerminateIfExpiredLocked() || _gateAState == GateATerminal)
            {
                throw new K3dConnectSafetyException();
            }
        }
    }

    internal bool CanTakeIn => !IsGateA;

    internal string RejectionMessage(PlayoutOperation operation) =>
        IsGateA
            ? $"Gate A does not authorize another {operation} command for this process."
            : "The playout command is not authorized for this launch.";

    internal static PlayoutLaunchAuthorization CreateGateAForTests(
        string capability,
        bool liveAuthorized = true,
        int expectedPgmProcessId = 1,
        long expectedPgmStartTimeUtcTicks = 1,
        TimeProvider? timeProvider = null,
        long? expiresAtUtcTicks = null,
        FileStream? gateASceneLease = null)
    {
        if (!IsExactCapability(capability))
        {
            throw new ArgumentException(
                "A test Gate A capability must be exactly 64 uppercase hexadecimal characters.",
                nameof(capability));
        }

        var clock = timeProvider ?? TimeProvider.System;
        var nowUtcTicks = clock.GetUtcNow().UtcTicks;
        var expiry = expiresAtUtcTicks ??
            nowUtcTicks + MaximumGateALifetimeTicks;
        return new PlayoutLaunchAuthorization(
            liveAuthorized,
            HashCapability(capability),
            expectedPgmProcessId,
            expectedPgmStartTimeUtcTicks,
            expiry,
            clock,
            clock.GetTimestamp(),
            TimeSpan.FromTicks(Math.Max(0, expiry - nowUtcTicks)),
            gateASceneLease);
    }

    internal static bool IsExactGateASceneDirectory(string? directory)
    {
        if (string.IsNullOrWhiteSpace(directory))
        {
            return false;
        }

        try
        {
            return string.Equals(
                Path.TrimEndingDirectorySeparator(Path.GetFullPath(directory)),
                Path.TrimEndingDirectorySeparator(
                    Path.GetFullPath(GateAApprovedSceneDirectory)),
                StringComparison.OrdinalIgnoreCase);
        }
        catch (Exception exception) when (exception is
            ArgumentException or
            NotSupportedException or
            PathTooLongException)
        {
            return false;
        }
    }

    public void Dispose()
    {
        if (IsGateA)
        {
            lock (_gateASync)
            {
                EnterTerminalLocked();
            }
        }

        Interlocked.Exchange(ref _gateASceneLease, null)?.Dispose();
    }

    private bool TryClaimEnginePrepareLocked(PlayoutCue? cue)
    {
        if (_gateAState != GateAOperatorClaimed)
        {
            return false;
        }

        if (!IsExactGateAPrepareCue(cue))
        {
            EnterTerminalLocked();
            return false;
        }

        if (TerminateIfExpiredLocked())
        {
            return false;
        }

        _gateAState = GateAEnginePrepareClaimed;
        return true;
    }

    private bool TerminateIfExpiredLocked()
    {
        if (ExpiresAtUtcTicks is not { } expiry)
        {
            return false;
        }

        var expired = _timeProvider.GetUtcNow().UtcTicks >= expiry;
        if (!expired)
        {
            try
            {
                expired = _timeProvider.GetElapsedTime(
                    _authorizationCapturedTimestamp,
                    _timeProvider.GetTimestamp()) >= _authorizedLifetime;
            }
            catch
            {
                // A broken monotonic clock must never extend live authority.
                expired = true;
            }
        }

        if (!expired)
        {
            return false;
        }

        EnterTerminalLocked();
        return true;
    }

    private void EnterTerminalLocked()
    {
        _gateAState = GateATerminal;
        if (!_capabilityHashCleared && _gateACapabilityHash is not null)
        {
            CryptographicOperations.ZeroMemory(_gateACapabilityHash);
            _capabilityHashCleared = true;
        }
    }

    private static bool IsExactGateAPrepareCue(PlayoutCue? cue)
    {
        if (cue is null ||
            !string.Equals(cue.SceneName, "5001", StringComparison.Ordinal) ||
            !string.Equals(cue.SceneFile, "5001.t2s", StringComparison.Ordinal) ||
            cue.FadeDuration != 6)
        {
            return false;
        }

        var disabledBackgroundCount = 0;
        foreach (var mutation in cue.Mutations ?? [])
        {
            switch (mutation)
            {
                case PlayoutUseBackground
                {
                    IsEnabled: false,
                    Timing: PlayoutMutationTiming.BeforeTransaction
                }:
                    disabledBackgroundCount++;
                    break;
                case PlayoutUseBackground or
                    PlayoutSetBackgroundTexture or
                    PlayoutSetBackgroundVideo:
                    return false;
            }
        }

        return disabledBackgroundCount == 1;
    }

    private static bool IsExactCapability(string? value) =>
        value is { Length: CapabilityLength } && value.All(static character =>
            character is >= '0' and <= '9' or >= 'A' and <= 'F');

    private static byte[] HashCapability(string capability) =>
        SHA256.HashData(Encoding.ASCII.GetBytes(capability));

    private static FileStream AcquireApprovedSceneLease(
        string sceneFilePath,
        string expectedSha256)
    {
        if (!IsExactCapability(expectedSha256))
        {
            throw new PlayoutConfigurationException(
                "The approved Gate A scene hash is invalid.");
        }

        FileStream? lease = null;
        byte[]? expectedHash = null;
        byte[]? actualHash = null;
        try
        {
            lease = new FileStream(
                Path.GetFullPath(sceneFilePath),
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                bufferSize: 64 * 1024,
                FileOptions.SequentialScan);
            expectedHash = Convert.FromHexString(expectedSha256);
            actualHash = SHA256.HashData(lease);
            lease.Position = 0;
            if (!CryptographicOperations.FixedTimeEquals(actualHash, expectedHash))
            {
                throw new PlayoutConfigurationException(
                    "The approved Gate A scene hash does not match the leased file.");
            }

            return lease;
        }
        catch (PlayoutConfigurationException)
        {
            lease?.Dispose();
            throw;
        }
        catch (Exception exception) when (exception is
            IOException or
            UnauthorizedAccessException or
            ArgumentException or
            NotSupportedException)
        {
            lease?.Dispose();
            throw new PlayoutConfigurationException(
                "The approved Gate A scene could not be leased and verified.",
                exception);
        }
        finally
        {
            if (expectedHash is not null)
            {
                CryptographicOperations.ZeroMemory(expectedHash);
            }

            if (actualHash is not null)
            {
                CryptographicOperations.ZeroMemory(actualHash);
            }
        }
    }

    private static void ClearGateAEnvironment()
    {
        Environment.SetEnvironmentVariable(
            GateACapabilityEnvironmentVariable,
            null,
            EnvironmentVariableTarget.Process);
        Environment.SetEnvironmentVariable(
            GateAPgmProcessIdEnvironmentVariable,
            null,
            EnvironmentVariableTarget.Process);
        Environment.SetEnvironmentVariable(
            GateAPgmStartTimeUtcTicksEnvironmentVariable,
            null,
            EnvironmentVariableTarget.Process);
        Environment.SetEnvironmentVariable(
            GateAExpiresAtUtcTicksEnvironmentVariable,
            null,
            EnvironmentVariableTarget.Process);
    }

    private static void ClearLiveAuthorizationEnvironment() =>
        Environment.SetEnvironmentVariable(
            PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
            null,
            EnvironmentVariableTarget.Process);
}
