using System.Collections.Concurrent;
using System.Globalization;
using System.Text;

namespace MBN_STOCK_WEBVIEW.Infrastructure.Diagnostics;

public enum NativeOperatorLogEvent
{
    Startup = 1,
    Shutdown = 2,
    OracleStateChanged = 3,
    MariaDbStateChanged = 4,
    TornadoStateChanged = 5,
    PlayoutCommandRequested = 6,
    PlayoutCommandCompleted = 7,
    OutcomeUnknown = 8
}

public enum NativeOperatorDatabaseState
{
    NotConfigured = 1,
    Healthy = 2,
    Unhealthy = 3,
    TimedOut = 4
}

public enum NativeOperatorTornadoState
{
    Disabled = 1,
    DryRunReady = 2,
    ProcessMissing = 3,
    Disconnected = 4,
    Connecting = 5,
    Connected = 6,
    Reconnecting = 7,
    Disconnecting = 8,
    Faulted = 9,
    OutcomeUnknown = 10,
    Disposed = 11
}

public enum NativeOperatorPlayoutCommand
{
    Connect = 1,
    Disconnect = 2,
    Prepare = 3,
    TakeIn = 4,
    Next = 5,
    TakeOut = 6,
    UpdateOnAir = 7
}

public enum NativeOperatorPlayoutCompletion
{
    Succeeded = 1,
    Rejected = 2,
    Cancelled = 3,
    Unavailable = 4,
    Failed = 5
}

/// <summary>
/// A closed operator-audit record. It deliberately has no string, path, SQL, scene,
/// exception, or arbitrary numeric field, so untrusted/free-form data cannot enter the log.
/// </summary>
public readonly record struct NativeOperatorLogRecord
{
    private NativeOperatorLogRecord(
        NativeOperatorLogEvent logEvent,
        NativeOperatorDatabaseState? databaseState = null,
        NativeOperatorTornadoState? tornadoState = null,
        NativeOperatorPlayoutCommand? command = null,
        NativeOperatorPlayoutCompletion? completion = null)
    {
        Event = logEvent;
        DatabaseState = databaseState;
        TornadoState = tornadoState;
        Command = command;
        Completion = completion;
    }

    public NativeOperatorLogEvent Event { get; }

    public NativeOperatorDatabaseState? DatabaseState { get; }

    public NativeOperatorTornadoState? TornadoState { get; }

    public NativeOperatorPlayoutCommand? Command { get; }

    public NativeOperatorPlayoutCompletion? Completion { get; }

    public static NativeOperatorLogRecord ForStartup() =>
        new(NativeOperatorLogEvent.Startup);

    public static NativeOperatorLogRecord ForShutdown() =>
        new(NativeOperatorLogEvent.Shutdown);

    public static NativeOperatorLogRecord ForOracleStateChanged(
        NativeOperatorDatabaseState state) =>
        new(NativeOperatorLogEvent.OracleStateChanged, databaseState: state);

    public static NativeOperatorLogRecord ForMariaDbStateChanged(
        NativeOperatorDatabaseState state) =>
        new(NativeOperatorLogEvent.MariaDbStateChanged, databaseState: state);

    public static NativeOperatorLogRecord ForTornadoStateChanged(
        NativeOperatorTornadoState state) =>
        new(NativeOperatorLogEvent.TornadoStateChanged, tornadoState: state);

    public static NativeOperatorLogRecord ForPlayoutCommandRequested(
        NativeOperatorPlayoutCommand command) =>
        new(NativeOperatorLogEvent.PlayoutCommandRequested, command: command);

    public static NativeOperatorLogRecord ForPlayoutCommandCompleted(
        NativeOperatorPlayoutCommand command,
        NativeOperatorPlayoutCompletion completion) =>
        new(
            NativeOperatorLogEvent.PlayoutCommandCompleted,
            command: command,
            completion: completion);

    public static NativeOperatorLogRecord ForOutcomeUnknown(
        NativeOperatorPlayoutCommand command) =>
        new(NativeOperatorLogEvent.OutcomeUnknown, command: command);
}

public interface INativeOperatorLogWriter
{
    /// <summary>
    /// Attempts one bounded append. No logging failure is allowed to escape to broadcast logic.
    /// </summary>
    bool TryWrite(NativeOperatorLogRecord record);
}

/// <summary>
/// Writes closed native operator events beneath
/// %LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Logs\Log_MMdd.log.
/// </summary>
public sealed class NativeOperatorLogWriter : INativeOperatorLogWriter
{
    internal const int DefaultMaximumFileBytes = 4 * 1024 * 1024;
    internal const int DefaultRetentionDays = 31;
    internal const int MaximumEncodedLineBytes = 512;

    private const string ApplicationDirectoryName = "MBN_STOCK_WEBVIEW";
    private const string LogDirectoryName = "Logs";
    private static readonly UTF8Encoding Utf8 = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
    private static readonly ConcurrentDictionary<string, object> PathGates =
        new(StringComparer.OrdinalIgnoreCase);

    private readonly string _localApplicationDataRoot;
    private readonly TimeProvider _timeProvider;
    private readonly int _maximumFileBytes;
    private readonly int _retentionDays;

    public NativeOperatorLogWriter()
        : this(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            TimeProvider.System,
            DefaultMaximumFileBytes,
            DefaultRetentionDays)
    {
    }

    internal NativeOperatorLogWriter(
        string localApplicationDataRoot,
        TimeProvider timeProvider,
        int maximumFileBytes = DefaultMaximumFileBytes,
        int retentionDays = DefaultRetentionDays)
    {
        _localApplicationDataRoot = localApplicationDataRoot;
        _timeProvider = timeProvider;
        _maximumFileBytes = maximumFileBytes;
        _retentionDays = retentionDays;
    }

    public bool TryWrite(NativeOperatorLogRecord record)
    {
        try
        {
            if (_timeProvider is null ||
                _maximumFileBytes <= 0 ||
                _retentionDays <= 0 ||
                !TryFormatPayload(record, out var payload) ||
                !TryResolvePaths(out var localRoot, out var applicationDirectory, out var logDirectory))
            {
                return false;
            }

            var gate = PathGates.GetOrAdd(logDirectory, static _ => new object());
            lock (gate)
            {
                var localNow = _timeProvider.GetLocalNow();
                if (!TryPrepareDirectories(localRoot, applicationDirectory, logDirectory))
                {
                    return false;
                }

                using var directoryLease = global::MBN_STOCK_WEBVIEW.Infrastructure
                    .TrustedManualDirectory.AcquirePrivateWritableLease(logDirectory);
                if (!TryApplyRetention(logDirectory, localNow.UtcDateTime))
                {
                    return false;
                }

                var fileName = $"Log_{localNow:MMdd}.log";
                var logPath = Path.GetFullPath(Path.Combine(logDirectory, fileName));
                if (!IsImmediateChild(logDirectory, logPath) ||
                    !string.Equals(Path.GetFileName(logPath), fileName, StringComparison.Ordinal))
                {
                    return false;
                }

                var line = $"[{localNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", CultureInfo.InvariantCulture)}] {payload}{Environment.NewLine}";
                var encodedLine = Utf8.GetBytes(line);
                if (encodedLine.Length == 0 || encodedLine.Length > MaximumEncodedLineBytes)
                {
                    return false;
                }

                return TryAppend(logDirectory, logPath, encodedLine);
            }
        }
        catch
        {
            // Audit logging is best effort and must never interrupt playout or database work.
            return false;
        }
    }

    private bool TryResolvePaths(
        out string localRoot,
        out string applicationDirectory,
        out string logDirectory)
    {
        localRoot = string.Empty;
        applicationDirectory = string.Empty;
        logDirectory = string.Empty;

        if (string.IsNullOrWhiteSpace(_localApplicationDataRoot) ||
            !Path.IsPathFullyQualified(_localApplicationDataRoot))
        {
            return false;
        }

        localRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(_localApplicationDataRoot));
        applicationDirectory = Path.GetFullPath(Path.Combine(localRoot, ApplicationDirectoryName));
        logDirectory = Path.GetFullPath(Path.Combine(applicationDirectory, LogDirectoryName));
        return IsImmediateChild(localRoot, applicationDirectory) &&
            IsImmediateChild(applicationDirectory, logDirectory);
    }

    private static bool TryPrepareDirectories(
        string localRoot,
        string applicationDirectory,
        string logDirectory)
    {
        if (!Directory.Exists(localRoot) ||
            !TryGetSafeDirectoryAttributes(localRoot))
        {
            return false;
        }

        var prepared = global::MBN_STOCK_WEBVIEW.Infrastructure.TrustedManualDirectory
            .PreparePrivateWritableDirectory(logDirectory);
        if (!string.Equals(
                Path.TrimEndingDirectorySeparator(prepared),
                Path.TrimEndingDirectorySeparator(logDirectory),
                StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        // The lease acquired by the caller pins the chain after this validation.
        return TryGetSafeDirectoryAttributes(localRoot) &&
            TryGetSafeDirectoryAttributes(applicationDirectory) &&
            TryGetSafeDirectoryAttributes(logDirectory);
    }

    private bool TryApplyRetention(string logDirectory, DateTime utcNow)
    {
        var cutoff = utcNow.AddDays(-_retentionDays);
        foreach (var entry in Directory.EnumerateFileSystemEntries(
                     logDirectory,
                     "Log_????.log",
                     SearchOption.TopDirectoryOnly))
        {
            var fullPath = Path.GetFullPath(entry);
            var fileName = Path.GetFileName(fullPath);
            if (!IsImmediateChild(logDirectory, fullPath) ||
                !IsLogFileName(fileName))
            {
                return false;
            }

            var attributes = File.GetAttributes(fullPath);
            if (!IsSafeFileAttributes(attributes))
            {
                return false;
            }

            if (File.GetLastWriteTimeUtc(fullPath) < cutoff)
            {
                File.Delete(fullPath);
            }
        }

        return TryGetSafeDirectoryAttributes(logDirectory);
    }

    private bool TryAppend(string logDirectory, string logPath, byte[] encodedLine)
    {
        if (File.Exists(logPath) && !TryGetSafeFileAttributes(logPath))
        {
            return false;
        }

        using var stream = new FileStream(
            logPath,
            FileMode.OpenOrCreate,
            FileAccess.Write,
            FileShare.Read,
            bufferSize: 4096,
            FileOptions.SequentialScan);

        if (!string.Equals(Path.GetFullPath(stream.Name), logPath, StringComparison.OrdinalIgnoreCase) ||
            !TryGetSafeDirectoryAttributes(logDirectory) ||
            !TryGetSafeFileAttributes(logPath) ||
            stream.Length > _maximumFileBytes - encodedLine.Length)
        {
            return false;
        }

        global::MBN_STOCK_WEBVIEW.Infrastructure.TrustedDirectoryLease
            .EnsureRegularFileHandle(
                stream.SafeFileHandle,
                logPath,
                _maximumFileBytes,
                allowEmpty: true);

        stream.Seek(0, SeekOrigin.End);
        stream.Write(encodedLine, 0, encodedLine.Length);
        stream.Flush(flushToDisk: false);
        return true;
    }

    private static bool TryFormatPayload(
        NativeOperatorLogRecord record,
        out string payload)
    {
        payload = string.Empty;
        switch (record.Event)
        {
            case NativeOperatorLogEvent.Startup
                when HasNoDetails(record):
                payload = "event=Startup";
                return true;
            case NativeOperatorLogEvent.Shutdown
                when HasNoDetails(record):
                payload = "event=Shutdown";
                return true;
            case NativeOperatorLogEvent.OracleStateChanged
                when record.DatabaseState is { } oracleState && HasOnlyDatabaseState(record):
                return TryDatabaseStateToken(oracleState, out payload, "OracleStateChanged");
            case NativeOperatorLogEvent.MariaDbStateChanged
                when record.DatabaseState is { } mariaDbState && HasOnlyDatabaseState(record):
                return TryDatabaseStateToken(mariaDbState, out payload, "MariaDbStateChanged");
            case NativeOperatorLogEvent.TornadoStateChanged
                when record.TornadoState is { } tornadoState && HasOnlyTornadoState(record):
                return TryTornadoStateToken(tornadoState, out payload);
            case NativeOperatorLogEvent.PlayoutCommandRequested
                when record.Command is { } requestedCommand && HasOnlyCommand(record):
                return TryCommandToken(requestedCommand, out payload, "PlayoutCommandRequested");
            case NativeOperatorLogEvent.PlayoutCommandCompleted
                when record.Command is { } completedCommand &&
                     record.Completion is { } completion &&
                     HasOnlyCommandAndCompletion(record):
                return TryCompletionToken(completedCommand, completion, out payload);
            case NativeOperatorLogEvent.OutcomeUnknown
                when record.Command is { } unknownCommand && HasOnlyCommand(record):
                return TryCommandToken(unknownCommand, out payload, "OutcomeUnknown");
            default:
                return false;
        }
    }

    private static bool HasNoDetails(NativeOperatorLogRecord record) =>
        record.DatabaseState is null &&
        record.TornadoState is null &&
        record.Command is null &&
        record.Completion is null;

    private static bool HasOnlyDatabaseState(NativeOperatorLogRecord record) =>
        record.DatabaseState is not null &&
        record.TornadoState is null &&
        record.Command is null &&
        record.Completion is null;

    private static bool HasOnlyTornadoState(NativeOperatorLogRecord record) =>
        record.DatabaseState is null &&
        record.TornadoState is not null &&
        record.Command is null &&
        record.Completion is null;

    private static bool HasOnlyCommand(NativeOperatorLogRecord record) =>
        record.DatabaseState is null &&
        record.TornadoState is null &&
        record.Command is not null &&
        record.Completion is null;

    private static bool HasOnlyCommandAndCompletion(NativeOperatorLogRecord record) =>
        record.DatabaseState is null &&
        record.TornadoState is null &&
        record.Command is not null &&
        record.Completion is not null;

    private static bool TryDatabaseStateToken(
        NativeOperatorDatabaseState state,
        out string payload,
        string eventName)
    {
        var stateToken = state switch
        {
            NativeOperatorDatabaseState.NotConfigured => "NotConfigured",
            NativeOperatorDatabaseState.Healthy => "Healthy",
            NativeOperatorDatabaseState.Unhealthy => "Unhealthy",
            NativeOperatorDatabaseState.TimedOut => "TimedOut",
            _ => null
        };
        payload = stateToken is null ? string.Empty : $"event={eventName} state={stateToken}";
        return stateToken is not null;
    }

    private static bool TryTornadoStateToken(
        NativeOperatorTornadoState state,
        out string payload)
    {
        var stateToken = state switch
        {
            NativeOperatorTornadoState.Disabled => "Disabled",
            NativeOperatorTornadoState.DryRunReady => "DryRunReady",
            NativeOperatorTornadoState.ProcessMissing => "ProcessMissing",
            NativeOperatorTornadoState.Disconnected => "Disconnected",
            NativeOperatorTornadoState.Connecting => "Connecting",
            NativeOperatorTornadoState.Connected => "Connected",
            NativeOperatorTornadoState.Reconnecting => "Reconnecting",
            NativeOperatorTornadoState.Disconnecting => "Disconnecting",
            NativeOperatorTornadoState.Faulted => "Faulted",
            NativeOperatorTornadoState.OutcomeUnknown => "OutcomeUnknown",
            NativeOperatorTornadoState.Disposed => "Disposed",
            _ => null
        };
        payload = stateToken is null
            ? string.Empty
            : $"event=TornadoStateChanged state={stateToken}";
        return stateToken is not null;
    }

    private static bool TryCommandToken(
        NativeOperatorPlayoutCommand command,
        out string payload,
        string eventName)
    {
        var commandToken = CommandToken(command);
        payload = commandToken is null
            ? string.Empty
            : $"event={eventName} command={commandToken}";
        return commandToken is not null;
    }

    private static bool TryCompletionToken(
        NativeOperatorPlayoutCommand command,
        NativeOperatorPlayoutCompletion completion,
        out string payload)
    {
        var commandToken = CommandToken(command);
        var completionToken = completion switch
        {
            NativeOperatorPlayoutCompletion.Succeeded => "Succeeded",
            NativeOperatorPlayoutCompletion.Rejected => "Rejected",
            NativeOperatorPlayoutCompletion.Cancelled => "Cancelled",
            NativeOperatorPlayoutCompletion.Unavailable => "Unavailable",
            NativeOperatorPlayoutCompletion.Failed => "Failed",
            _ => null
        };
        payload = commandToken is null || completionToken is null
            ? string.Empty
            : $"event=PlayoutCommandCompleted command={commandToken} result={completionToken}";
        return commandToken is not null && completionToken is not null;
    }

    private static string? CommandToken(NativeOperatorPlayoutCommand command) =>
        command switch
        {
            NativeOperatorPlayoutCommand.Connect => "Connect",
            NativeOperatorPlayoutCommand.Disconnect => "Disconnect",
            NativeOperatorPlayoutCommand.Prepare => "Prepare",
            NativeOperatorPlayoutCommand.TakeIn => "TakeIn",
            NativeOperatorPlayoutCommand.Next => "Next",
            NativeOperatorPlayoutCommand.TakeOut => "TakeOut",
            NativeOperatorPlayoutCommand.UpdateOnAir => "UpdateOnAir",
            _ => null
        };

    private static bool TryGetSafeDirectoryAttributes(string path)
    {
        try
        {
            return Directory.Exists(path) &&
                IsSafeDirectoryAttributes(File.GetAttributes(path));
        }
        catch
        {
            return false;
        }
    }

    private static bool TryGetSafeFileAttributes(string path)
    {
        try
        {
            return File.Exists(path) &&
                IsSafeFileAttributes(File.GetAttributes(path));
        }
        catch
        {
            return false;
        }
    }

    internal static bool IsSafeDirectoryAttributes(FileAttributes attributes) =>
        attributes.HasFlag(FileAttributes.Directory) &&
        !attributes.HasFlag(FileAttributes.ReparsePoint) &&
        !attributes.HasFlag(FileAttributes.Device);

    internal static bool IsSafeFileAttributes(FileAttributes attributes) =>
        !attributes.HasFlag(FileAttributes.Directory) &&
        !attributes.HasFlag(FileAttributes.ReparsePoint) &&
        !attributes.HasFlag(FileAttributes.Device);

    private static bool IsImmediateChild(string parent, string candidate)
    {
        var expectedParent = Path.TrimEndingDirectorySeparator(Path.GetFullPath(parent));
        var actualParent = Path.GetDirectoryName(Path.GetFullPath(candidate));
        return string.Equals(expectedParent, actualParent, StringComparison.OrdinalIgnoreCase);
    }

    private static bool IsLogFileName(string fileName) =>
        fileName.Length == 12 &&
        fileName.StartsWith("Log_", StringComparison.Ordinal) &&
        fileName.EndsWith(".log", StringComparison.Ordinal) &&
        fileName.AsSpan(4, 4).IndexOfAnyExceptInRange('0', '9') < 0;
}
