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

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers;

/// <summary>
///     Mutable, all-nullable form model for the driver resilience override. Binds the typed fields in
///     DriverResilienceSection; null/blank = "use the driver's tier default", so a blank form serializes
///     back to null (preserving DriverInstance.ResilienceConfig = null). Emits / reads the exact override
///     JSON shape DriverResilienceOptionsParser consumes.
///     <para>
///     Round-trip is <b>non-lossy</b> (04/C-7): the stored JSON is kept as a <see cref="JsonObject"/> bag,
///     and <see cref="ToJson"/> overlays only the known fields onto that bag — so unknown top-level keys,
///     unknown capability entries, and unknown per-policy fields all survive a load→save, mirroring the
///     driver tag editors' preserve-unknown discipline (<c>TagConfigJson</c>). A stored blob that could not
///     be parsed sets <see cref="ParseFailed"/> and is <b>never</b> overwritten — <see cref="ToJson"/>
///     returns the original text verbatim.
///     </para>
/// </summary>
public sealed class ResilienceFormModel
{
    public static readonly string[] Capabilities =
        ["Read", "Write", "Discover", "Subscribe", "Probe", "AlarmSubscribe", "AlarmAcknowledge", "HistoryRead"];

    /// <summary>Gets or sets the driver recycle-interval override, in seconds; null = use the tier default.</summary>
    public int? RecycleIntervalSeconds { get; set; }

    // capability name -> (timeout, retry, breaker), each nullable.
    /// <summary>Gets or sets the per-capability resilience overrides, keyed by capability name.</summary>
    public Dictionary<string, CapabilityRow> Policies { get; set; } =
        Capabilities.ToDictionary(c => c, _ => new CapabilityRow(), StringComparer.OrdinalIgnoreCase);

    /// <summary>
    ///     True when the stored ResilienceConfig could not be parsed as JSON. The section locks editing and
    ///     <see cref="ToJson"/> returns <see cref="RawStoredJson"/> unchanged so an unrelated keystroke can
    ///     never silently overwrite an unreadable column.
    /// </summary>
    public bool ParseFailed { get; private set; }

    /// <summary>The original stored text when <see cref="ParseFailed"/> — returned verbatim by <see cref="ToJson"/>.</summary>
    public string? RawStoredJson { get; private set; }

    private JsonObject _bag = new();

    /// <summary>Per-capability timeout/retry/breaker override row; null fields fall back to the tier default.</summary>
    public sealed class CapabilityRow
    {
        /// <summary>Gets or sets the timeout override, in seconds; null = use the tier default.</summary>
        public int? TimeoutSeconds { get; set; }
        /// <summary>Gets or sets the retry-count override; null = use the tier default.</summary>
        public int? RetryCount { get; set; }
        /// <summary>Gets or sets the circuit-breaker failure-threshold override; null = use the tier default.</summary>
        public int? BreakerFailureThreshold { get; set; }
        /// <summary>Gets a value indicating whether all fields in this row are unset.</summary>
        public bool IsEmpty => TimeoutSeconds is null && RetryCount is null && BreakerFailureThreshold is null;
    }

    /// <summary>
    ///     Range-validate the per-capability overrides against <see cref="ResiliencePolicyRanges"/> and return
    ///     one human-readable message per violation (naming capability + field + legal range). This is
    ///     authoring-time <b>feedback</b>, not the enforcement layer — the parser's clamp is the authoritative
    ///     guard, so the form still emits. Mirrors the driver tag editors' <c>Validate()</c> idiom (01/S-6).
    /// </summary>
    /// <returns>A list of range-violation messages; empty when every override is in range or blank.</returns>
    public IReadOnlyList<string> Validate()
    {
        var msgs = new List<string>();
        foreach (var (cap, row) in Policies)
        {
            if (row.TimeoutSeconds is { } t &&
                (t < ResiliencePolicyRanges.MinTimeoutSeconds || t > ResiliencePolicyRanges.MaxTimeoutSeconds))
                msgs.Add($"{cap} timeout must be between {ResiliencePolicyRanges.MinTimeoutSeconds} and " +
                         $"{ResiliencePolicyRanges.MaxTimeoutSeconds} seconds (got {t}).");

            if (row.RetryCount is { } r &&
                (r < ResiliencePolicyRanges.MinRetryCount || r > ResiliencePolicyRanges.MaxRetryCount))
                msgs.Add($"{cap} retries must be between {ResiliencePolicyRanges.MinRetryCount} and " +
                         $"{ResiliencePolicyRanges.MaxRetryCount} (got {r}).");

            if (row.BreakerFailureThreshold is { } b &&
                (b < ResiliencePolicyRanges.MinBreakerFailureThreshold || b == 1 ||
                 b > ResiliencePolicyRanges.MaxBreakerFailureThreshold))
                msgs.Add($"{cap} breaker threshold must be 0 (off) or between " +
                         $"{ResiliencePolicyRanges.MinEnabledBreakerFailureThreshold} and " +
                         $"{ResiliencePolicyRanges.MaxBreakerFailureThreshold} (got {b}).");
        }
        return msgs;
    }

    /// <summary>
    ///     Parse the override JSON into a form model, preserving every key in an internal bag. Malformed JSON
    ///     sets <see cref="ParseFailed"/> + <see cref="RawStoredJson"/> and yields an otherwise-empty model.
    /// </summary>
    /// <param name="json">The raw resilience-override JSON, or null/blank if there is no override.</param>
    /// <returns>A populated form model.</returns>
    public static ResilienceFormModel FromJson(string? json)
    {
        var model = new ResilienceFormModel();
        if (string.IsNullOrWhiteSpace(json)) return model;

        JsonObject bag;
        try
        {
            bag = JsonNode.Parse(json) as JsonObject ?? new JsonObject();
        }
        catch (JsonException)
        {
            model.ParseFailed = true;
            model.RawStoredJson = json;
            return model;
        }

        model._bag = bag;
        model.RecycleIntervalSeconds = GetIntOrNull(bag, "recycleIntervalSeconds");

        if (bag.TryGetPropertyValue("capabilityPolicies", out var cpNode) && cpNode is JsonObject cp)
        {
            foreach (var (capName, polNode) in cp)
            {
                // Known capability names populate rows (reading only the known per-policy fields — unknown
                // per-policy fields stay untouched in the bag). Unknown capability entries stay in the bag.
                if (polNode is JsonObject pol && model.Policies.TryGetValue(capName, out var row))
                {
                    row.TimeoutSeconds = GetIntOrNull(pol, "timeoutSeconds");
                    row.RetryCount = GetIntOrNull(pol, "retryCount");
                    row.BreakerFailureThreshold = GetIntOrNull(pol, "breakerFailureThreshold");
                }
            }
        }
        return model;
    }

    /// <summary>
    ///     Overlay the model's non-null known values onto the preserved bag and serialize. Returns null when
    ///     the result is empty (blank form = tier defaults). When <see cref="ParseFailed"/>, returns the
    ///     original unparseable text unchanged.
    /// </summary>
    /// <returns>The serialized override JSON, the original text when parse failed, or null when nothing is set.</returns>
    public string? ToJson()
    {
        if (ParseFailed) return RawStoredJson;

        SetOrRemoveInt(_bag, "recycleIntervalSeconds", RecycleIntervalSeconds);

        var cp = _bag.TryGetPropertyValue("capabilityPolicies", out var cpNode) && cpNode is JsonObject existing
            ? existing
            : null;

        foreach (var (cap, row) in Policies)
        {
            // Find this known capability's existing policy object (case-insensitive), else its canonical key.
            var polKey = cap;
            JsonObject? pol = null;
            if (cp is not null)
            {
                foreach (var (k, v) in cp)
                {
                    if (string.Equals(k, cap, StringComparison.OrdinalIgnoreCase) && v is JsonObject vo)
                    {
                        polKey = k;
                        pol = vo;
                        break;
                    }
                }
            }

            if (row.IsEmpty)
            {
                // Clear the known fields; drop the policy entry only if no unknown fields remain.
                if (pol is not null)
                {
                    pol.Remove("timeoutSeconds");
                    pol.Remove("retryCount");
                    pol.Remove("breakerFailureThreshold");
                    if (pol.Count == 0) cp!.Remove(polKey);
                }
            }
            else
            {
                if (cp is null)
                {
                    cp = new JsonObject();
                    _bag["capabilityPolicies"] = cp;
                }
                if (pol is null)
                {
                    pol = new JsonObject();
                    cp[polKey] = pol;
                }
                SetOrRemoveInt(pol, "timeoutSeconds", row.TimeoutSeconds);
                SetOrRemoveInt(pol, "retryCount", row.RetryCount);
                SetOrRemoveInt(pol, "breakerFailureThreshold", row.BreakerFailureThreshold);
            }
        }

        if (cp is not null && cp.Count == 0) _bag.Remove("capabilityPolicies");

        return _bag.Count == 0 ? null : _bag.ToJsonString();
    }

    private static int? GetIntOrNull(JsonObject o, string name)
        => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue<int>(out var i)
            ? i
            : null;

    private static void SetOrRemoveInt(JsonObject o, string name, int? value)
    {
        if (value is null) o.Remove(name);
        else o[name] = JsonValue.Create(value.Value);
    }
}
