using System;
using System.IO.Pipes;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using MessagePack;
using Serilog;

namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Ipc;

/// <summary>
///     Accepts one client connection at a time on a named pipe with the strict ACL from
///     <see cref="PipeAcl"/>. Verifies the peer SID and the per-process shared secret before
///     any frame is dispatched. Mirrors Driver.Galaxy.Host's PipeServer; the sidecar carries
///     its own copy so the deletion of Galaxy.Host in PR 7.2 leaves the sidecar self-contained.
/// </summary>
public sealed class PipeServer : IDisposable
{
    private readonly string _pipeName;
    private readonly SecurityIdentifier _allowedSid;
    private readonly string _sharedSecret;
    private readonly ILogger _logger;
    private readonly CancellationTokenSource _cts = new();
    private readonly CallerVerifier _verifier;
    private NamedPipeServerStream? _current;

    /// <summary>
    ///     Pluggable caller-verification seam. Default implementation calls
    ///     <see cref="VerifyCaller"/>; tests can substitute one that ignores the pipe ACL
    ///     to exercise the rejection paths.
    /// </summary>
    /// <param name="pipe">The named pipe server stream to verify.</param>
    /// <param name="allowedSid">The allowed security identifier.</param>
    /// <param name="reason">The rejection reason if verification fails.</param>
    internal delegate bool CallerVerifier(NamedPipeServerStream pipe, SecurityIdentifier allowedSid, out string reason);

    /// <summary>Initializes a new instance of the <see cref="PipeServer"/> class.</summary>
    /// <param name="pipeName">The name of the named pipe.</param>
    /// <param name="allowedSid">The security identifier allowed to connect.</param>
    /// <param name="sharedSecret">The shared secret for client authentication.</param>
    /// <param name="logger">The logger for diagnostic messages.</param>
    public PipeServer(string pipeName, SecurityIdentifier allowedSid, string sharedSecret, ILogger logger)
        : this(pipeName, allowedSid, sharedSecret, logger, DefaultVerifier) { }

    /// <summary>Initializes a new instance of the <see cref="PipeServer"/> class with a custom verifier.</summary>
    /// <param name="pipeName">The name of the named pipe.</param>
    /// <param name="allowedSid">The security identifier allowed to connect.</param>
    /// <param name="sharedSecret">The shared secret for client authentication.</param>
    /// <param name="logger">The logger for diagnostic messages.</param>
    /// <param name="verifier">The caller verification delegate.</param>
    internal PipeServer(
        string pipeName, SecurityIdentifier allowedSid, string sharedSecret, ILogger logger,
        CallerVerifier verifier)
    {
        _pipeName = pipeName ?? throw new ArgumentNullException(nameof(pipeName));
        _allowedSid = allowedSid ?? throw new ArgumentNullException(nameof(allowedSid));
        _sharedSecret = sharedSecret ?? throw new ArgumentNullException(nameof(sharedSecret));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _verifier = verifier ?? throw new ArgumentNullException(nameof(verifier));
    }

    private static bool DefaultVerifier(NamedPipeServerStream pipe, SecurityIdentifier allowedSid, out string reason)
        => VerifyCaller(pipe, allowedSid, out reason);

    /// <summary>
    ///     Accepts one connection, performs Hello handshake, then dispatches frames to
    ///     <paramref name="handler"/> until EOF or cancel. Returns when the client disconnects.
    /// </summary>
    /// <param name="handler">The frame handler to process frames.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    public async Task RunOneConnectionAsync(IFrameHandler handler, CancellationToken ct)
    {
        using var linked = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, ct);
        var acl = PipeAcl.Create(_allowedSid);

        _current = new NamedPipeServerStream(
            _pipeName,
            PipeDirection.InOut,
            maxNumberOfServerInstances: 1,
            PipeTransmissionMode.Byte,
            PipeOptions.Asynchronous,
            inBufferSize: 64 * 1024,
            outBufferSize: 64 * 1024,
            pipeSecurity: acl);

        try
        {
            await _current.WaitForConnectionAsync(linked.Token).ConfigureAwait(false);

            using var reader = new FrameReader(_current, leaveOpen: true);
            using var writer = new FrameWriter(_current, leaveOpen: true);

            // First frame must be Hello with the correct shared secret. Reading it before
            // the caller-SID impersonation check satisfies Windows' ERROR_CANNOT_IMPERSONATE
            // rule — ImpersonateNamedPipeClient fails until at least one frame has been read.
            var first = await reader.ReadFrameAsync(linked.Token).ConfigureAwait(false);
            if (first is null || first.Value.Kind != MessageKind.Hello)
            {
                _logger.Warning("Sidecar IPC first frame was not Hello; dropping");
                return;
            }

            if (!_verifier(_current, _allowedSid, out var reason))
            {
                // Driver.Historian.Wonderware-007: send a rejecting HelloAck so the client
                // learns why instead of having to wait for its own read timeout. The reason
                // tag "caller-sid-mismatch" is symmetric with the shared-secret-mismatch and
                // major-version-mismatch acks the two other rejection paths emit below.
                await writer.WriteAsync(MessageKind.HelloAck,
                    new HelloAck { Accepted = false, RejectReason = $"caller-sid-mismatch: {reason}" },
                    linked.Token).ConfigureAwait(false);
                _logger.Warning("Sidecar IPC caller rejected: {Reason}", reason);
                _current.Disconnect();
                return;
            }

            var hello = MessagePackSerializer.Deserialize<Hello>(first.Value.Body);
            if (!string.Equals(hello.SharedSecret, _sharedSecret, StringComparison.Ordinal))
            {
                await writer.WriteAsync(MessageKind.HelloAck,
                    new HelloAck { Accepted = false, RejectReason = "shared-secret-mismatch" },
                    linked.Token).ConfigureAwait(false);
                _logger.Warning("Sidecar IPC Hello rejected: shared-secret-mismatch");
                return;
            }

            if (hello.ProtocolMajor != Hello.CurrentMajor)
            {
                await writer.WriteAsync(MessageKind.HelloAck,
                    new HelloAck { Accepted = false, RejectReason = $"major-version-mismatch-peer={hello.ProtocolMajor}-server={Hello.CurrentMajor}" },
                    linked.Token).ConfigureAwait(false);
                _logger.Warning("Sidecar IPC Hello rejected: major mismatch peer={Peer} server={Server}",
                    hello.ProtocolMajor, Hello.CurrentMajor);
                return;
            }

            await writer.WriteAsync(MessageKind.HelloAck,
                new HelloAck { Accepted = true, HostName = Environment.MachineName },
                linked.Token).ConfigureAwait(false);

            while (!linked.Token.IsCancellationRequested)
            {
                var frame = await reader.ReadFrameAsync(linked.Token).ConfigureAwait(false);
                if (frame is null) break;

                await handler.HandleAsync(frame.Value.Kind, frame.Value.Body, writer, linked.Token).ConfigureAwait(false);
            }
        }
        finally
        {
            _current.Dispose();
            _current = null;
        }
    }

    // Backoff sequence for consecutive RunOneConnection failures: 250 ms → 500 ms →
    // 1 000 ms → 2 000 ms → 4 000 ms → capped at 8 000 ms thereafter.
    private static readonly TimeSpan[] BackoffSteps =
    {
        TimeSpan.FromMilliseconds(250),
        TimeSpan.FromMilliseconds(500),
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(2),
        TimeSpan.FromSeconds(4),
        TimeSpan.FromSeconds(8),
    };

    /// <summary>
    ///     Maximum consecutive failures before the server gives up and lets the process exit
    ///     so the supervisor (NSSM / SCM) can restart the sidecar cleanly.
    /// </summary>
    private const int MaxConsecutiveFailures = 20;

    /// <summary>
    ///     Runs the server continuously, handling one connection at a time. When a connection
    ///     ends (clean or error), waits with exponential backoff before accepting the next.
    ///     If <see cref="MaxConsecutiveFailures"/> consecutive failures occur the method
    ///     throws so the supervisor can restart the sidecar.
    /// </summary>
    /// <param name="handler">The frame handler to process frames.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    public async Task RunAsync(IFrameHandler handler, CancellationToken ct)
    {
        var consecutiveFailures = 0;

        while (!ct.IsCancellationRequested)
        {
            try
            {
                await RunOneConnectionAsync(handler, ct).ConfigureAwait(false);
                consecutiveFailures = 0; // a clean connection (even a short-lived one) resets the counter
            }
            catch (OperationCanceledException) { break; }
            catch (Exception ex)
            {
                consecutiveFailures++;

                if (consecutiveFailures >= MaxConsecutiveFailures)
                {
                    _logger.Fatal(ex,
                        "Sidecar IPC connection loop failed {Count} consecutive times — giving up so supervisor can restart",
                        consecutiveFailures);
                    throw;
                }

                var delay = BackoffSteps[Math.Min(consecutiveFailures - 1, BackoffSteps.Length - 1)];
                _logger.Error(ex,
                    "Sidecar IPC connection loop error (consecutive failure {Count}/{Max}) — retrying in {Delay}",
                    consecutiveFailures, MaxConsecutiveFailures, delay);

                try { await Task.Delay(delay, ct).ConfigureAwait(false); }
                catch (OperationCanceledException) { break; }
            }
        }
    }

    private static bool VerifyCaller(NamedPipeServerStream pipe, SecurityIdentifier allowedSid, out string reason)
    {
        try
        {
            pipe.RunAsClient(() =>
            {
                using var wi = WindowsIdentity.GetCurrent();
                if (wi.User is null)
                    throw new InvalidOperationException("GetCurrent().User is null — cannot verify caller");
                if (wi.User != allowedSid)
                    throw new UnauthorizedAccessException(
                        $"caller SID {wi.User.Value} does not match allowed {allowedSid.Value}");
            });
            reason = string.Empty;
            return true;
        }
        catch (Exception ex) { reason = ex.Message; return false; }
    }

    /// <summary>Disposes the pipe server and cancels any pending operations.</summary>
    public void Dispose()
    {
        _cts.Cancel();
        _current?.Dispose();
        _cts.Dispose();
    }
}

/// <summary>
///     Strategy for handling each post-Hello frame the pipe server reads. Implementations
///     deserialize the body per the <see cref="MessageKind"/>, dispatch to the historian, and
///     write the corresponding reply through the supplied <see cref="FrameWriter"/>.
/// </summary>
public interface IFrameHandler
{
    /// <summary>Handles a frame from the pipe server.</summary>
    /// <param name="kind">The type of message being handled.</param>
    /// <param name="body">The serialized message body.</param>
    /// <param name="writer">The frame writer to send responses.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    Task HandleAsync(MessageKind kind, byte[] body, FrameWriter writer, CancellationToken ct);
}
