using Mbproxy.Bcd;
using Mbproxy.Configuration;
using Mbproxy.Options;
using Mbproxy.Proxy.Cache;
using Mbproxy.Proxy.Multiplexing;
using Mbproxy.Proxy.Supervision;
using Microsoft.Extensions.Options;
using Polly;

namespace Mbproxy.Proxy;

/// <summary>
/// <see cref="BackgroundService"/> that owns all <see cref="PlcListenerSupervisor"/> instances.
///
/// Startup posture (matches design doc "eager, continue on per-port failure"):
/// <list type="number">
///   <item>Enumerate <see cref="MbproxyOptions.Plcs"/> and build one supervisor per PLC.</item>
///   <item>Start all supervisors in parallel. Each supervisor attempts to bind immediately
///         and enters the Polly recovery loop if the bind fails.</item>
///   <item>After all supervisors have completed their first bind attempt (reached
///         <see cref="SupervisorState.Bound"/> or <see cref="SupervisorState.Recovering"/>),
///         log <c>mbproxy.startup.ready</c> with bound/configured counts.</item>
/// </list>
///
/// Phase 06: passes the supervisor dictionary to <see cref="ConfigReconciler.Attach"/>
/// after initial startup so hot-reload changes are applied by the reconciler.
///
/// Stop: cancels all supervisors in parallel with a 5-second hard deadline.
/// </summary>
internal sealed partial class ProxyWorker : BackgroundService
{
    private readonly IOptionsMonitor<MbproxyOptions> _options;
    private readonly IPduPipeline _pipeline;
    private readonly ILogger<ProxyWorker> _logger;
    private readonly ILoggerFactory _loggerFactory;
    private readonly ConfigReconciler _reconciler;

    // Phase 06: supervisors are now managed jointly by ProxyWorker (initial bootstrap)
    // and ConfigReconciler (subsequent hot-reload changes). The dictionary is shared
    // via ConfigReconciler.Attach() after initial startup.
    private readonly Dictionary<string, PlcListenerSupervisor> _supervisors = new(StringComparer.Ordinal);

    /// <summary>
    /// Read-only view of the live supervisor dictionary. Consumed by Phase 07's
    /// <see cref="Admin.StatusSnapshotBuilder"/> to enumerate per-PLC state.
    /// The caller should read this on the status-page path only (not the hot path).
    /// </summary>
    internal IReadOnlyDictionary<string, PlcListenerSupervisor> Supervisors => _supervisors;

    public ProxyWorker(
        IOptionsMonitor<MbproxyOptions> options,
        IPduPipeline pipeline,
        ILogger<ProxyWorker> logger,
        ILoggerFactory loggerFactory,
        ConfigReconciler reconciler)
    {
        _options       = options;
        _pipeline      = pipeline;
        _logger        = logger;
        _loggerFactory = loggerFactory;
        _reconciler    = reconciler;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var opts = _options.CurrentValue;
        int plcsConfigured = opts.Plcs.Count;

        // ── 1. Build per-PLC BCD tag maps ────────────────────────────────────────────
        var plcContexts = new Dictionary<string, PerPlcContext>(opts.Plcs.Count, StringComparer.Ordinal);

        foreach (var plc in opts.Plcs)
        {
            var result = BcdTagMapBuilder.Build(opts.BcdTags, plc.BcdTags, plc.DefaultCacheTtlMs);

            foreach (var warn in result.Warnings)
                _logger.LogWarning("[{Plc}] BCD tag map warning: {Message}", plc.Name, warn.Message);

            if (result.Errors.Count > 0)
            {
                foreach (var err in result.Errors)
                    _logger.LogError("[{Plc}] BCD tag map error ({Kind}): {Message}",
                        plc.Name, err.Kind, err.Message);

                _logger.LogError("Skipping listener for PLC '{Plc}' due to BCD tag map errors.", plc.Name);
                continue;
            }

            // Phase 11 — construct a per-PLC response cache only when at least one
            // resolved tag opts in (CacheTtlMs > 0). Skipping cache construction for a
            // PLC with no cacheable tags keeps the no-cache path free of the eviction
            // timer and the per-call resolution cost, preserving "default behaviour =
            // Phase 10 unchanged" when no operator has opted any tag in.
            var cache = HasAnyCacheableTag(result.Map)
                ? new ResponseCache(opts.Cache.MaxEntriesPerPlc, opts.Cache.EvictionIntervalMs)
                : null;

            plcContexts[plc.Name] = new PerPlcContext
            {
                PlcName  = plc.Name,
                TagMap   = result.Map,
                Counters = new ProxyCounters(),
                Logger   = _loggerFactory.CreateLogger($"Mbproxy.Proxy.BcdRewriter.{plc.Name}"),
                Cache    = cache,
            };
        }

        // ── 2. Build Polly pipelines once ─────────────────────────────────────────────
        // Both pipelines are built from ResilienceOptions and reused across all PLCs.
        var resilienceOpts  = opts.Resilience;
        var backendPipeline = PolicyFactory.BuildBackendConnect(
            resilienceOpts.BackendConnect,
            _loggerFactory.CreateLogger("Mbproxy.Proxy.BackendConnect"));

        // ── 3. Build supervisors ──────────────────────────────────────────────────────
        foreach (var plc in opts.Plcs)
        {
            if (!plcContexts.TryGetValue(plc.Name, out var perPlcContext))
                continue; // BCD map failed — skip this PLC.

            // Each supervisor gets its own recovery pipeline (with its own logger scope).
            var recoveryPipeline = PolicyFactory.BuildListenerRecovery(
                resilienceOpts.ListenerRecovery,
                _loggerFactory.CreateLogger($"Mbproxy.Proxy.ListenerRecovery.{plc.Name}"));

            // Phase 10 — give the supervisor a live accessor for ReadCoalescingOptions
            // so a hot-reload of `Mbproxy.Resilience.ReadCoalescing.Enabled` propagates
            // to the multiplexer's per-PDU coalescing decision.
            Func<ReadCoalescingOptions> coalescingAccessor =
                () => _options.CurrentValue.Resilience.ReadCoalescing;

            var supervisor = new PlcListenerSupervisor(
                plc,
                opts.Connection,
                _pipeline,
                _loggerFactory.CreateLogger<PlcListener>(),
                _loggerFactory.CreateLogger<PlcMultiplexer>(),
                _loggerFactory.CreateLogger($"Mbproxy.Proxy.UpstreamPipe.{plc.Name}"),
                perPlcContext,
                recoveryPipeline,
                _loggerFactory.CreateLogger<PlcListenerSupervisor>(),
                backendPipeline,
                coalescingAccessor);

            _supervisors[plc.Name] = supervisor;
        }

        // ── Phase 06: wire reconciler BEFORE starting supervisors ─────────────────
        // Attach hands the reconciler the authoritative supervisor dictionary and the
        // initial options snapshot. The reconciler won't process OnChange events until
        // after this call — the brief window between Attach and first supervisor start
        // is safe because the channel signal only enqueues; apply runs asynchronously.
        _reconciler.Attach(_supervisors, opts);

        if (_supervisors.Count == 0)
        {
            LogStartupReady(_logger, 0, plcsConfigured);
            await Task.Delay(Timeout.Infinite, stoppingToken).ConfigureAwait(false);
            return;
        }

        // ── 4. Start all supervisors in parallel ──────────────────────────────────────
        var startTasks = _supervisors.Values
            .Select(s => s.StartAsync(stoppingToken))
            .ToArray();
        await Task.WhenAll(startTasks).ConfigureAwait(false);

        // ── 5. Wait for every supervisor to complete its first bind attempt ───────────
        // "Ready" = every supervisor has transitioned out of Stopped (i.e. reached
        // Bound or Recovering from its first attempt).
        using var readyCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
        using var readyLinked = CancellationTokenSource.CreateLinkedTokenSource(
            readyCts.Token, stoppingToken);

        var waitTasks = _supervisors.Values
            .Select(s => s.WaitForInitialBindAttemptAsync(readyLinked.Token))
            .ToArray();

        try
        {
            await Task.WhenAll(waitTasks).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            // Either the 30 s deadline fired or the service is stopping.
        }

        int boundCount = _supervisors.Values.Count(s => s.Snapshot().State == SupervisorState.Bound);
        LogStartupReady(_logger, boundCount, plcsConfigured);

        // ── 6. Keep the worker alive until the host signals stop ─────────────────────
        // Supervisors run their own background loops; ExecuteAsync just waits.
        await Task.Delay(Timeout.Infinite, stoppingToken).ConfigureAwait(false);
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        // Cancel ExecuteAsync first.
        await base.StopAsync(cancellationToken).ConfigureAwait(false);

        // Stop all supervisors in parallel with a 5-second hard deadline.
        using var stopCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        using var linked  = CancellationTokenSource.CreateLinkedTokenSource(
            stopCts.Token, cancellationToken);

        var stopTasks = _supervisors.Values
            .Select(s => s.StopAsync(linked.Token))
            .ToArray();

        try
        {
            await Task.WhenAll(stopTasks).ConfigureAwait(false);
        }
        catch
        {
            // Best effort — don't let individual supervisor failures block shutdown.
        }

        foreach (var supervisor in _supervisors.Values)
            await supervisor.DisposeAsync().ConfigureAwait(false);

        _supervisors.Clear();
    }

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

    /// <summary>
    /// Phase 11 — returns <c>true</c> when at least one BcdTag in the resolved map has a
    /// positive <see cref="BcdTag.CacheTtlMs"/>. A PLC with no cacheable tags skips the
    /// <see cref="Mbproxy.Proxy.Cache.ResponseCache"/> entirely (no eviction timer, no
    /// per-call cache resolution cost), so the default-OFF deployment is byte-identical
    /// to a Phase-10 deployment.
    /// </summary>
    private static bool HasAnyCacheableTag(BcdTagMap map)
    {
        foreach (var t in map.All)
            if (t.CacheTtlMs > 0) return true;
        return false;
    }

    [LoggerMessage(EventId = 1, EventName = "mbproxy.startup.ready",
        Level = LogLevel.Information,
        Message = "mbproxy service ready — ListenersBound={ListenersBound} PlcsConfigured={PlcsConfigured}")]
    private static partial void LogStartupReady(ILogger logger, int listenersBound, int plcsConfigured);

    [LoggerMessage(EventId = 21, EventName = "mbproxy.startup.bind.failed",
        Level = LogLevel.Error,
        Message = "Failed to bind listener: Plc={Plc} Port={Port} Reason={Reason}")]
    private static partial void LogBindFailed(ILogger logger, string plc, int port, string reason);
}
