using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using Mbproxy.Options;
using Mbproxy.Proxy.Multiplexing;
using Polly;

namespace Mbproxy.Proxy;

/// <summary>
/// Owns one <see cref="TcpListener"/> bound to a PLC's configured listen port and one
/// <see cref="PlcMultiplexer"/> that owns the single backend connection to the PLC.
///
/// <para>Every accepted upstream is wrapped in an <see cref="UpstreamPipe"/> and handed
/// to the multiplexer, which TxId-multiplexes them onto a single backend socket — this
/// eliminates the H2-ECOM100's 4-concurrent-client cap from the upstream side.</para>
///
/// <para><see cref="StartAsync"/> binds the socket; <see cref="RunAsync"/> runs until
/// cancelled or the listener faults; <see cref="DisposeAsync"/> tears down both the
/// listener and the multiplexer.</para>
/// </summary>
internal sealed partial class PlcListener : IAsyncDisposable
{
    private readonly PlcOptions _plc;
    private readonly ConnectionOptions _connectionOptions;
    private readonly IPduPipeline _pipeline;
    private readonly ILogger<PlcListener> _listenerLogger;
    private readonly ILogger<PlcMultiplexer> _multiplexerLogger;
    private readonly ILogger _pipeLogger;
    private readonly PerPlcContext? _perPlcContext;
    private readonly ResiliencePipeline? _backendConnectPipeline;
    private readonly Func<ReadCoalescingOptions>? _coalescingOptions;
    // Live keepalive accessor (TCP SO_KEEPALIVE on accepted upstream sockets + the backend
    // heartbeat). Non-null after construction — falls back to the construction-time
    // ConnectionOptions snapshot when no live accessor is supplied.
    private readonly Func<KeepaliveOptions> _keepaliveOptions;

    private TcpListener? _listener;
    private PlcMultiplexer? _multiplexer;
    private bool _disposed;

    // Track active pipe-handling tasks so DisposeAsync can wait for them.
    private readonly ConcurrentDictionary<Guid, Task> _pipeTasks = new();

    /// <summary>
    /// Live collection of active <see cref="UpstreamPipe"/> instances for this listener.
    /// Consumed by the status page to report per-client telemetry. Empty when the
    /// multiplexer has not yet been constructed (e.g., between StopAsync and a fresh start).
    /// </summary>
    public IReadOnlyCollection<UpstreamPipe> ActiveUpstreams
        => _multiplexer?.AttachedPipes ?? Array.Empty<UpstreamPipe>();

    /// <summary>
    /// Exposes the running multiplexer so a hot-reload reseat can swap the per-PLC
    /// context on the live instance. <c>null</c> between StopAsync and a fresh start;
    /// callers must null-check.
    /// </summary>
    internal PlcMultiplexer? Multiplexer => _multiplexer;

    public PlcListener(
        PlcOptions plc,
        ConnectionOptions connectionOptions,
        IPduPipeline pipeline,
        ILogger<PlcListener> listenerLogger,
        ILogger<PlcMultiplexer> multiplexerLogger,
        ILogger pipeLogger,
        PerPlcContext? perPlcContext = null,
        ResiliencePipeline? backendConnectPipeline = null,
        Func<ReadCoalescingOptions>? coalescingOptions = null,
        Func<KeepaliveOptions>? keepaliveOptions = null)
    {
        _plc                    = plc;
        _connectionOptions      = connectionOptions;
        _pipeline               = pipeline;
        _listenerLogger         = listenerLogger;
        _multiplexerLogger      = multiplexerLogger;
        _pipeLogger             = pipeLogger;
        _perPlcContext          = perPlcContext;
        _backendConnectPipeline = backendConnectPipeline;
        _coalescingOptions      = coalescingOptions;
        _keepaliveOptions       = keepaliveOptions ?? (() => _connectionOptions.Keepalive);
    }

    /// <summary>
    /// Binds the listen socket. Throws <see cref="SocketException"/> on bind failure;
    /// the caller (<see cref="Supervision.PlcListenerSupervisor"/>) catches and logs
    /// <c>mbproxy.startup.bind.failed</c>.
    /// </summary>
    public void StartAsync()
    {
        var endpoint = new IPEndPoint(IPAddress.Any, _plc.ListenPort);
        _listener = new TcpListener(endpoint);
        _listener.Start();
        LogBound(_listenerLogger, _plc.Name, _plc.ListenPort);

        // The multiplexer needs a PerPlcContext to share the BCD tag map and counters
        // with the pipeline. If the caller (typically a test) didn't supply one,
        // construct a minimal context that exposes only the PlcName so the multiplexer
        // + a noop/passthrough pipeline still round-trip frames correctly.
        var ctx = _perPlcContext ?? new PerPlcContext
        {
            PlcName = _plc.Name,
            Logger  = _pipeLogger,
        };
        _multiplexer = new PlcMultiplexer(
            _plc,
            _connectionOptions,
            _pipeline,
            ctx,
            _multiplexerLogger,
            _backendConnectPipeline,
            _coalescingOptions,
            _keepaliveOptions);
    }

    /// <summary>
    /// Runs the accept loop until <paramref name="ct"/> is cancelled or the listener
    /// faults. On accept, wraps the socket in an <see cref="UpstreamPipe"/> and attaches
    /// it to the multiplexer.
    /// </summary>
    public async Task RunAsync(CancellationToken ct)
    {
        if (_listener is null)
            throw new InvalidOperationException("StartAsync must be called before RunAsync.");

        if (_multiplexer is null)
            throw new InvalidOperationException("StartAsync must construct the multiplexer before RunAsync.");

        try
        {
            while (!ct.IsCancellationRequested)
            {
                Socket upstream = await _listener.AcceptSocketAsync(ct).ConfigureAwait(false);

                var pipe = new UpstreamPipe(upstream, _plc.Name, _pipeLogger, _keepaliveOptions());
                var pipeTask = Task.Run(async () =>
                {
                    try
                    {
                        await _multiplexer.StartPipeAsync(pipe, ct).ConfigureAwait(false);
                    }
                    finally
                    {
                        await pipe.DisposeAsync().ConfigureAwait(false);
                    }
                }, CancellationToken.None);

                _pipeTasks[pipe.Id] = pipeTask;
                _ = pipeTask.ContinueWith(prev => _pipeTasks.TryRemove(pipe.Id, out _), TaskScheduler.Default);
            }
        }
        catch (OperationCanceledException)
        {
            // Normal shutdown.
        }
        catch (Exception ex)
        {
            // Listener faulted — log and return. The supervisor will restart.
            LogListenerFaulted(_listenerLogger, _plc.Name, _plc.ListenPort, ex.Message);
        }
    }

    // ── IAsyncDisposable ──────────────────────────────────────────────────────────────────

    public async ValueTask DisposeAsync()
    {
        if (_disposed) return;
        _disposed = true;

        _listener?.Stop();

        if (_multiplexer is not null)
        {
            await _multiplexer.DisposeAsync().ConfigureAwait(false);
            _multiplexer = null;
        }

        Task[] snapshot = _pipeTasks.Values.ToArray();
        if (snapshot.Length > 0)
        {
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            try
            {
                await Task.WhenAll(snapshot)
                    .WaitAsync(timeout.Token)
                    .ConfigureAwait(false);
            }
            catch
            {
                // Best effort.
            }
        }
    }

    // ── Logging ───────────────────────────────────────────────────────────────────────────

    [LoggerMessage(EventId = 20, EventName = "mbproxy.startup.bind",
        Level = LogLevel.Information, Message = "Listener bound: Plc={Plc} Port={Port}")]
    private static partial void LogBound(ILogger logger, string plc, int port);

    [LoggerMessage(EventId = 22, EventName = "mbproxy.listener.faulted",
        Level = LogLevel.Error, Message = "Listener faulted: Plc={Plc} Port={Port} Reason={Reason}")]
    private static partial void LogListenerFaulted(ILogger logger, string plc, int port, string reason);
}
