using System.Text.Json;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

/// <summary>
///     Parses the <c>DriverInstance.ResilienceConfig</c> JSON column into a
///     <see cref="DriverResilienceOptions"/> instance layered on top of the tier defaults.
///     Every key in the JSON is optional; missing keys fall back to the tier defaults from
///     <see cref="DriverResilienceOptions.GetTierDefaults(DriverTier)"/>.
/// </summary>
/// <remarks>
///     <para>Example JSON shape per Phase 6.1 Stream A.2:</para>
///     <code>
///     {
///       "capabilityPolicies": {
///         "Read":  { "timeoutSeconds": 5, "retryCount": 5, "breakerFailureThreshold": 3 },
///         "Write": { "timeoutSeconds": 5, "retryCount": 0, "breakerFailureThreshold": 5 }
///       }
///     }
///     </code>
///
///     <para>Unrecognised keys + values are ignored so future shapes land without a migration.
///     Per-capability overrides are layered on top of tier defaults — a partial policy (only
///     some of TimeoutSeconds/RetryCount/BreakerFailureThreshold) fills in the other fields
///     from the tier default for that capability.</para>
///
///     <para>Parser failures (malformed JSON, type mismatches) fall back to pure tier defaults
///     + surface through an out-parameter diagnostic. Callers may log the diagnostic but should
///     NOT fail driver startup — a misconfigured ResilienceConfig should never brick a
///     working driver.</para>
/// </remarks>
public static class DriverResilienceOptionsParser
{
    private static readonly JsonSerializerOptions JsonOpts = new()
    {
        PropertyNameCaseInsensitive = true,
        AllowTrailingCommas = true,
        ReadCommentHandling = JsonCommentHandling.Skip,
    };

    /// <summary>
    ///     Parse the JSON payload layered on <paramref name="tier"/>'s defaults. Returns the
    ///     effective options; <paramref name="parseDiagnostic"/> is null on success, or a
    ///     human-readable error message when the JSON was malformed (options still returned
    ///     = tier defaults).
    /// </summary>
    /// <param name="tier">The driver tier for default resilience options.</param>
    /// <param name="resilienceConfigJson">The optional JSON configuration string to parse.</param>
    /// <param name="parseDiagnostic">An out parameter containing diagnostic information if parsing fails.</param>
    /// <returns>The effective <see cref="DriverResilienceOptions"/>, layering any parsed overrides on top of the tier defaults.</returns>
    public static DriverResilienceOptions ParseOrDefaults(
        DriverTier tier,
        string? resilienceConfigJson,
        out string? parseDiagnostic)
    {
        parseDiagnostic = null;
        var baseDefaults = DriverResilienceOptions.GetTierDefaults(tier);
        var baseOptions = new DriverResilienceOptions { Tier = tier, CapabilityPolicies = baseDefaults };

        if (string.IsNullOrWhiteSpace(resilienceConfigJson))
            return baseOptions;

        ResilienceConfigShape? shape;
        try
        {
            shape = JsonSerializer.Deserialize<ResilienceConfigShape>(resilienceConfigJson, JsonOpts);
        }
        catch (JsonException ex)
        {
            parseDiagnostic = $"ResilienceConfig JSON malformed; falling back to tier {tier} defaults. Detail: {ex.Message}";
            return baseOptions;
        }

        if (shape is null) return baseOptions;

        var merged = new Dictionary<DriverCapability, CapabilityPolicy>(baseDefaults);
        if (shape.CapabilityPolicies is not null)
        {
            foreach (var (capName, overridePolicy) in shape.CapabilityPolicies)
            {
                if (!Enum.TryParse<DriverCapability>(capName, ignoreCase: true, out var capability))
                {
                    AppendDiagnostic(ref parseDiagnostic, $"Unknown capability '{capName}' in ResilienceConfig; skipped.");
                    continue;
                }

                var basePolicy = merged[capability];
                var mergedPolicy = new CapabilityPolicy(
                    TimeoutSeconds: overridePolicy.TimeoutSeconds ?? basePolicy.TimeoutSeconds,
                    RetryCount: overridePolicy.RetryCount ?? basePolicy.RetryCount,
                    BreakerFailureThreshold: overridePolicy.BreakerFailureThreshold ?? basePolicy.BreakerFailureThreshold);
                merged[capability] = ClampPolicy(capability, mergedPolicy, basePolicy, ref parseDiagnostic);
            }
        }

        // Scheduled recycle is Tier C only — reject a configured interval on Tier A/B as a
        // misconfiguration surface rather than silently honouring it (recycling an in-process
        // driver would kill every OPC UA session + every co-hosted driver).
        int? recycleIntervalSeconds = null;
        if (shape.RecycleIntervalSeconds is int secs)
        {
            if (secs <= 0)
                AppendDiagnostic(ref parseDiagnostic, $"RecycleIntervalSeconds must be positive; got {secs} — ignoring.");
            else if (tier != DriverTier.C)
                AppendDiagnostic(ref parseDiagnostic, $"RecycleIntervalSeconds is Tier C only; Tier {tier} driver will not scheduled-recycle.");
            else
                recycleIntervalSeconds = secs;
        }

        return new DriverResilienceOptions
        {
            Tier = tier,
            CapabilityPolicies = merged,
            RecycleIntervalSeconds = recycleIntervalSeconds,
        };
    }

    /// <summary>
    ///     Clamp a merged per-capability override to the legal <see cref="ResiliencePolicyRanges"/> so a
    ///     downstream Polly pipeline build can never throw <c>ValidationException</c> (01/S-6). Each clamp
    ///     appends a diagnostic naming the field + what happened. A non-positive timeout snaps to the
    ///     capability's <paramref name="tierDefault"/> (a nonsense value, not merely an extreme — snapping
    ///     to 1 s could be a 30× surprise on a Discover policy); everything else clamps to the nearest legal
    ///     value. Preserves maximum operator intent while never bricking.
    /// </summary>
    private static CapabilityPolicy ClampPolicy(
        DriverCapability capability,
        CapabilityPolicy merged,
        CapabilityPolicy tierDefault,
        ref string? diagnostic)
    {
        var timeout = merged.TimeoutSeconds;
        if (timeout <= 0)
        {
            AppendDiagnostic(ref diagnostic,
                $"{capability}.timeoutSeconds {timeout} is non-positive; using the tier default ({tierDefault.TimeoutSeconds}s).");
            timeout = tierDefault.TimeoutSeconds;
        }
        else if (timeout > ResiliencePolicyRanges.MaxTimeoutSeconds)
        {
            AppendDiagnostic(ref diagnostic,
                $"{capability}.timeoutSeconds {timeout} exceeds the max ({ResiliencePolicyRanges.MaxTimeoutSeconds}s); clamped.");
            timeout = ResiliencePolicyRanges.MaxTimeoutSeconds;
        }

        var retry = merged.RetryCount;
        if (retry < ResiliencePolicyRanges.MinRetryCount)
        {
            AppendDiagnostic(ref diagnostic, $"{capability}.retryCount {retry} is negative; clamped to {ResiliencePolicyRanges.MinRetryCount}.");
            retry = ResiliencePolicyRanges.MinRetryCount;
        }
        else if (retry > ResiliencePolicyRanges.MaxRetryCount)
        {
            AppendDiagnostic(ref diagnostic, $"{capability}.retryCount {retry} exceeds the cap ({ResiliencePolicyRanges.MaxRetryCount}); clamped.");
            retry = ResiliencePolicyRanges.MaxRetryCount;
        }

        // Spec invariant (01/S-8 = 03/S12): Write and AlarmAcknowledge are write-shaped — a duplicate is
        // not harmless (pulse coils, counter increments, Galaxy supervisory writes, alarm acks). Retries are
        // "skipped regardless of tier" per Phase 6.1; the tier defaults are already 0, so only an override can
        // differ — force it back to 0 here so the JSON→options funnel enforces the invariant. When per-tag
        // opt-in via WriteIdempotentAttribute ships, this is the one line to relax.
        if ((capability == DriverCapability.Write || capability == DriverCapability.AlarmAcknowledge) && retry > 0)
        {
            AppendDiagnostic(ref diagnostic,
                $"{capability} never retries (writes are treated as non-idempotent until per-tag opt-in ships); retryCount override ignored.");
            retry = 0;
        }

        var breaker = merged.BreakerFailureThreshold;
        if (breaker < ResiliencePolicyRanges.MinBreakerFailureThreshold)
        {
            AppendDiagnostic(ref diagnostic, $"{capability}.breakerFailureThreshold {breaker} is negative; breaker disabled.");
            breaker = ResiliencePolicyRanges.MinBreakerFailureThreshold;
        }
        else if (breaker == 1)
        {
            AppendDiagnostic(ref diagnostic,
                $"{capability}.breakerFailureThreshold 1 is below Polly's minimum throughput; raised to {ResiliencePolicyRanges.MinEnabledBreakerFailureThreshold} (closest to open-on-first-failure intent).");
            breaker = ResiliencePolicyRanges.MinEnabledBreakerFailureThreshold;
        }
        else if (breaker > ResiliencePolicyRanges.MaxBreakerFailureThreshold)
        {
            AppendDiagnostic(ref diagnostic,
                $"{capability}.breakerFailureThreshold {breaker} exceeds the cap ({ResiliencePolicyRanges.MaxBreakerFailureThreshold}); clamped.");
            breaker = ResiliencePolicyRanges.MaxBreakerFailureThreshold;
        }

        return merged with { TimeoutSeconds = timeout, RetryCount = retry, BreakerFailureThreshold = breaker };
    }

    /// <summary>Append a diagnostic to the running channel (joined with "; ") so multiple clamps all surface.</summary>
    private static void AppendDiagnostic(ref string? diagnostic, string message)
        => diagnostic = diagnostic is null ? message : $"{diagnostic}; {message}";

    private sealed class ResilienceConfigShape
    {
        /// <summary>Gets or sets the scheduled recycle interval in seconds.</summary>
        public int? RecycleIntervalSeconds { get; set; }
        /// <summary>Gets or sets the per-capability resilience policies.</summary>
        public Dictionary<string, CapabilityPolicyShape>? CapabilityPolicies { get; set; }
    }

    private sealed class CapabilityPolicyShape
    {
        /// <summary>Gets or sets the operation timeout in seconds.</summary>
        public int? TimeoutSeconds { get; set; }
        /// <summary>Gets or sets the number of retry attempts.</summary>
        public int? RetryCount { get; set; }
        /// <summary>Gets or sets the failure count threshold before the circuit breaker opens.</summary>
        public int? BreakerFailureThreshold { get; set; }
    }
}
