using Polly;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Observability;

namespace ZB.MOM.WW.OtOpcUa.Core.Resilience;

/// <summary>
///     Executes driver-capability calls through a shared Polly pipeline. One invoker per
///     <c>(DriverInstance, IDriver)</c> pair; the underlying <see cref="DriverResiliencePipelineBuilder"/>
///     is process-singleton so all invokers share its cache. Implements
///     <see cref="IDriverCapabilityInvoker"/> so the Polly-free Runtime dispatch layer can consume it
///     through the abstraction (bound per-driver by <see cref="IDriverCapabilityInvokerFactory"/>).
/// </summary>
/// <remarks>
///     The Runtime dispatch layer (<c>DriverInstanceActor</c>) routes its capability calls
///     (<c>IWritable.WriteAsync</c>, <c>ITagDiscovery.DiscoverAsync</c>,
///     <c>ISubscribable.SubscribeAsync/UnsubscribeAsync</c>,
///     <c>IAlarmSource.SubscribeAlarmsAsync/AcknowledgeAsync</c>) through this invoker via the
///     <see cref="IDriverCapabilityInvoker"/> seam. The <c>UnwrappedCapabilityCallAnalyzer</c>
///     (OTOPCUA0001) enforces that every guarded call is wrapped in an <see cref="ExecuteAsync{TResult}"/>
///     / <see cref="ExecuteWriteAsync{TResult}"/> lambda (the interface's methods are registered wrapper
///     homes too, so an interface-typed invoker call suppresses the diagnostic).
/// </remarks>
public sealed class CapabilityInvoker : IDriverCapabilityInvoker
{
    private readonly DriverResiliencePipelineBuilder _builder;
    private readonly string _driverInstanceId;
    private readonly string _driverType;
    private readonly Func<DriverResilienceOptions> _optionsAccessor;
    private readonly DriverResilienceStatusTracker? _statusTracker;
    private readonly long _optionsGeneration;
    private DriverResilienceOptions? _noRetryWriteOptions;

    /// <summary>
    ///     Construct an invoker for one driver instance.
    /// </summary>
    /// <param name="builder">Shared, process-singleton pipeline builder.</param>
    /// <param name="driverInstanceId">The <c>DriverInstance.Id</c> column value.</param>
    /// <param name="optionsAccessor">
    ///     Snapshot accessor for the current resilience options. Invoked per call so Admin-edit +
    ///     pipeline-invalidate can take effect without restarting the invoker.
    /// </param>
    /// <param name="driverType">Driver type name for structured-log enrichment (e.g. <c>"Modbus"</c>).</param>
    /// <param name="statusTracker">Optional resilience-status tracker. When wired, every capability call records start/complete so the AdminUI resilience panel can surface <see cref="ResilienceStatusSnapshot.CurrentInFlight"/> as the in-flight-call-depth proxy, and each successful call resets <see cref="ResilienceStatusSnapshot.ConsecutiveFailures"/> so the counter reflects only an active retry storm.</param>
    /// <param name="optionsGeneration">
    ///     Options epoch for this invoker (01/S-7). Threaded into every pipeline-cache lookup so a lingering
    ///     old child's late re-cache can never poison a newer invoker's pipeline. Defaults to 0 so existing
    ///     callers/tests are unaffected.
    /// </param>
    public CapabilityInvoker(
        DriverResiliencePipelineBuilder builder,
        string driverInstanceId,
        Func<DriverResilienceOptions> optionsAccessor,
        string driverType = "Unknown",
        DriverResilienceStatusTracker? statusTracker = null,
        long optionsGeneration = 0)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(optionsAccessor);

        _builder = builder;
        _driverInstanceId = driverInstanceId;
        _driverType = driverType;
        _optionsAccessor = optionsAccessor;
        _statusTracker = statusTracker;
        _optionsGeneration = optionsGeneration;
    }

    /// <inheritdoc />
    public async ValueTask<TResult> ExecuteAsync<TResult>(
        DriverCapability capability,
        string hostName,
        Func<CancellationToken, ValueTask<TResult>> callSite,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(callSite);

        var pipeline = ResolvePipeline(capability, hostName);
        _statusTracker?.RecordCallStart(_driverInstanceId, hostName);
        try
        {
            using (LogContextEnricher.Push(_driverInstanceId, _driverType, capability, LogContextEnricher.NewCorrelationId()))
            {
                var result = await pipeline.ExecuteAsync(callSite, cancellationToken).ConfigureAwait(false);
                // Success resets ConsecutiveFailures so the counter is a true "consecutive" gauge
                // (nonzero only during an active retry storm), not stuck at the last blip's count.
                _statusTracker?.RecordSuccess(_driverInstanceId, hostName, DateTime.UtcNow);
                return result;
            }
        }
        finally
        {
            _statusTracker?.RecordCallComplete(_driverInstanceId, hostName);
        }
    }

    /// <inheritdoc />
    public async ValueTask ExecuteAsync(
        DriverCapability capability,
        string hostName,
        Func<CancellationToken, ValueTask> callSite,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(callSite);

        var pipeline = ResolvePipeline(capability, hostName);
        _statusTracker?.RecordCallStart(_driverInstanceId, hostName);
        try
        {
            using (LogContextEnricher.Push(_driverInstanceId, _driverType, capability, LogContextEnricher.NewCorrelationId()))
            {
                await pipeline.ExecuteAsync(callSite, cancellationToken).ConfigureAwait(false);
                _statusTracker?.RecordSuccess(_driverInstanceId, hostName, DateTime.UtcNow);
            }
        }
        finally
        {
            _statusTracker?.RecordCallComplete(_driverInstanceId, hostName);
        }
    }

    /// <inheritdoc />
    public async ValueTask<TResult> ExecuteWriteAsync<TResult>(
        string hostName,
        bool isIdempotent,
        Func<CancellationToken, ValueTask<TResult>> callSite,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(callSite);

        if (!isIdempotent)
        {
            // Build the no-retry options snapshot ONCE per invoker lifetime (01/P-4). The options are
            // immutable for the invoker's lifetime — a ResilienceConfig change respawns the driver child,
            // which constructs a fresh invoker (#13 respawn-on-change) — so lazily caching the snapshot is
            // safe and removes the per-write allocation the previous once-per-call snapshot incurred. The
            // null-check-assign tolerates a benign first-call race (both threads build an equivalent record).
            var noRetryOptions = _noRetryWriteOptions ??= BuildNoRetryWriteOptions();
            var pipeline = _builder.GetOrCreate(_driverInstanceId, $"{hostName}::non-idempotent", DriverCapability.Write, noRetryOptions, _optionsGeneration);
            // Record in-flight accounting on the CALLER's host (the "::non-idempotent" suffix is a
            // pipeline-cache key, not an operator-facing host) so Admin /hosts sees write depth too.
            _statusTracker?.RecordCallStart(_driverInstanceId, hostName);
            try
            {
                using (LogContextEnricher.Push(_driverInstanceId, _driverType, DriverCapability.Write, LogContextEnricher.NewCorrelationId()))
                {
                    var result = await pipeline.ExecuteAsync(callSite, cancellationToken).ConfigureAwait(false);
                    _statusTracker?.RecordSuccess(_driverInstanceId, hostName, DateTime.UtcNow);
                    return result;
                }
            }
            finally
            {
                _statusTracker?.RecordCallComplete(_driverInstanceId, hostName);
            }
        }

        return await ExecuteAsync(DriverCapability.Write, hostName, callSite, cancellationToken).ConfigureAwait(false);
    }

    private DriverResilienceOptions BuildNoRetryWriteOptions()
    {
        var snapshot = _optionsAccessor();
        return snapshot with
        {
            CapabilityPolicies = new Dictionary<DriverCapability, CapabilityPolicy>
            {
                [DriverCapability.Write] = snapshot.Resolve(DriverCapability.Write) with { RetryCount = 0 },
            },
        };
    }

    private ResiliencePipeline ResolvePipeline(DriverCapability capability, string hostName) =>
        _builder.GetOrCreate(_driverInstanceId, hostName, capability, _optionsAccessor(), _optionsGeneration);
}
