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

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

/// <summary>
///     Per-tier × per-capability resilience policy configuration for a driver instance.
///     Bound from <c>DriverInstance.ResilienceConfig</c> JSON (nullable column; null = tier defaults).
///     Per <c>docs/v2/plan.md</c>.
/// </summary>
public sealed record DriverResilienceOptions
{
    /// <summary>Tier the owning driver type is registered as; drives the default map.</summary>
    public required DriverTier Tier { get; init; }

    /// <summary>
    ///     Per-capability policy overrides. Capabilities absent from this map fall back to
    ///     <see cref="GetTierDefaults(DriverTier)"/> for the configured <see cref="Tier"/>.
    /// </summary>
    public IReadOnlyDictionary<DriverCapability, CapabilityPolicy> CapabilityPolicies { get; init; }
        = new Dictionary<DriverCapability, CapabilityPolicy>();

    /// <summary>
    ///     Periodic scheduled recycle interval for Tier C out-of-process hosts, in seconds.
    ///     Null (the default) = no scheduled recycle; the driver's Host process keeps running
    ///     indefinitely unless a memory breach or operator action triggers a recycle. Only
    ///     respected for <see cref="DriverTier.C"/>; Tier A/B recycle would tear down every
    ///     OPC UA session, so the loader ignores non-null values for those tiers + logs a
    ///     warning.
    /// </summary>
    public int? RecycleIntervalSeconds { get; init; }

    /// <summary>
    ///     Look up the effective policy for a capability, falling back to tier defaults when no
    ///     override is configured. Never returns null.
    /// </summary>
    /// <param name="capability">The driver capability to resolve the policy for.</param>
    /// <returns>The effective CapabilityPolicy for the specified capability.</returns>
    /// <exception cref="KeyNotFoundException">
    ///     Thrown when neither the override map nor the tier defaults carry an entry for the
    ///     requested capability. The <c>TierDefaults_Cover_EveryCapability</c> invariant test
    ///     in <c>DriverResilienceOptionsTests</c> guarantees every defined enum value is present
    ///     in each tier's table, so this only fires when a caller passes an out-of-range value
    ///     or someone adds a <see cref="DriverCapability"/> member without updating
    ///     <see cref="GetTierDefaults"/>. The message names the missing capability and tier.
    /// </exception>
    public CapabilityPolicy Resolve(DriverCapability capability)
    {
        if (CapabilityPolicies.TryGetValue(capability, out var policy))
            return policy;

        var defaults = GetTierDefaults(Tier);
        if (defaults.TryGetValue(capability, out var fallback))
            return fallback;

        throw new KeyNotFoundException(
            $"No policy defined for capability '{capability}' under tier '{Tier}'. " +
            $"This indicates a {nameof(DriverCapability)} enum value missing from {nameof(GetTierDefaults)} — " +
            "add the capability to every tier's default table.");
    }

    /// <summary>
    ///     Per-tier per-capability default policy table, per the Phase 6.1
    ///     Stream A.2 specification. The parser enforces no-retry on
    ///     <see cref="DriverCapability.Write"/> and <see cref="DriverCapability.AlarmAcknowledge"/>
    ///     as an invariant (R2-02/S-8) — a JSON retryCount override on those capabilities is forced back
    ///     to 0; per-tag opt-in via <see cref="WriteIdempotentAttribute"/> is the future relaxation.
    /// </summary>
    /// <param name="tier">The driver tier to get defaults for.</param>
    /// <returns>The default policy dictionary for the specified tier.</returns>
    public static IReadOnlyDictionary<DriverCapability, CapabilityPolicy> GetTierDefaults(DriverTier tier) =>
        tier switch
        {
            DriverTier.A => new Dictionary<DriverCapability, CapabilityPolicy>
            {
                [DriverCapability.Read]             = new(TimeoutSeconds: 2,  RetryCount: 3, BreakerFailureThreshold: 5),
                [DriverCapability.Write]            = new(TimeoutSeconds: 2,  RetryCount: 0, BreakerFailureThreshold: 5),
                [DriverCapability.Discover]         = new(TimeoutSeconds: 30, RetryCount: 2, BreakerFailureThreshold: 3),
                [DriverCapability.Subscribe]        = new(TimeoutSeconds: 5,  RetryCount: 3, BreakerFailureThreshold: 5),
                [DriverCapability.Probe]            = new(TimeoutSeconds: 2,  RetryCount: 3, BreakerFailureThreshold: 5),
                [DriverCapability.AlarmSubscribe]   = new(TimeoutSeconds: 5,  RetryCount: 3, BreakerFailureThreshold: 5),
                [DriverCapability.AlarmAcknowledge] = new(TimeoutSeconds: 5,  RetryCount: 0, BreakerFailureThreshold: 5),
                [DriverCapability.HistoryRead]      = new(TimeoutSeconds: 30, RetryCount: 2, BreakerFailureThreshold: 5),
            },
            DriverTier.B => new Dictionary<DriverCapability, CapabilityPolicy>
            {
                [DriverCapability.Read]             = new(TimeoutSeconds: 4,  RetryCount: 3, BreakerFailureThreshold: 5),
                [DriverCapability.Write]            = new(TimeoutSeconds: 4,  RetryCount: 0, BreakerFailureThreshold: 5),
                [DriverCapability.Discover]         = new(TimeoutSeconds: 60, RetryCount: 2, BreakerFailureThreshold: 3),
                [DriverCapability.Subscribe]        = new(TimeoutSeconds: 8,  RetryCount: 3, BreakerFailureThreshold: 5),
                [DriverCapability.Probe]            = new(TimeoutSeconds: 4,  RetryCount: 3, BreakerFailureThreshold: 5),
                [DriverCapability.AlarmSubscribe]   = new(TimeoutSeconds: 8,  RetryCount: 3, BreakerFailureThreshold: 5),
                [DriverCapability.AlarmAcknowledge] = new(TimeoutSeconds: 8,  RetryCount: 0, BreakerFailureThreshold: 5),
                [DriverCapability.HistoryRead]      = new(TimeoutSeconds: 60, RetryCount: 2, BreakerFailureThreshold: 5),
            },
            DriverTier.C => new Dictionary<DriverCapability, CapabilityPolicy>
            {
                [DriverCapability.Read]             = new(TimeoutSeconds: 10,  RetryCount: 1, BreakerFailureThreshold: 0),
                [DriverCapability.Write]            = new(TimeoutSeconds: 10,  RetryCount: 0, BreakerFailureThreshold: 0),
                [DriverCapability.Discover]         = new(TimeoutSeconds: 120, RetryCount: 1, BreakerFailureThreshold: 0),
                [DriverCapability.Subscribe]        = new(TimeoutSeconds: 15,  RetryCount: 1, BreakerFailureThreshold: 0),
                [DriverCapability.Probe]            = new(TimeoutSeconds: 10,  RetryCount: 1, BreakerFailureThreshold: 0),
                [DriverCapability.AlarmSubscribe]   = new(TimeoutSeconds: 15,  RetryCount: 1, BreakerFailureThreshold: 0),
                [DriverCapability.AlarmAcknowledge] = new(TimeoutSeconds: 15,  RetryCount: 0, BreakerFailureThreshold: 0),
                [DriverCapability.HistoryRead]      = new(TimeoutSeconds: 120, RetryCount: 1, BreakerFailureThreshold: 0),
            },
            _ => throw new ArgumentOutOfRangeException(nameof(tier), tier, $"No default policy table defined for tier {tier}."),
        };
}

/// <summary>Policy for one capability on one driver instance.</summary>
/// <param name="TimeoutSeconds">Per-call timeout (wraps the inner Polly execution).</param>
/// <param name="RetryCount">Number of retry attempts after the first failure; zero = no retry.</param>
/// <param name="BreakerFailureThreshold">
///     Consecutive-failure count that opens the circuit breaker; zero = no breaker
///     (Tier C uses the supervisor's process-level breaker instead).
/// </param>
public sealed record CapabilityPolicy(int TimeoutSeconds, int RetryCount, int BreakerFailureThreshold);
