using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Json;
using ScadaLink.Commons.Types.Enums;

namespace ScadaLink.CentralUI.Components.Shared;

/// <summary>
/// Round-trip codec for the alarm trigger configuration JSON used by both
/// <see cref="AlarmTriggerEditor"/> (UI editing) and AlarmActor (runtime
/// evaluation). The serialized shape per trigger type:
///   ValueMatch       { attributeName, matchValue }      ("!=X" prefix = not equals)
///   RangeViolation   { attributeName, min, max }
///   RateOfChange     { attributeName, thresholdPerSecond, windowSeconds, direction }
///   HiLo             { attributeName, loLo, lo, hi, hiHi,
///                      loLoPriority, loPriority, hiPriority, hiHiPriority }
///   Expression       { expression }
///
/// All HiLo setpoints and per-setpoint priorities are optional — any subset
/// is valid (e.g., only Hi/HiHi configured for over-temperature protection).
///
/// Parsing also accepts legacy aliases the runtime used to consume
/// (<c>attribute</c>, <c>value</c>, <c>low</c>, <c>high</c>) so older configs
/// survive a round-trip through the editor.
/// </summary>
internal static class AlarmTriggerConfigCodec
{
    /// <summary>
    /// Parses a trigger configuration JSON in the context of the given trigger
    /// type. Returns a model with default values on null/empty/malformed input
    /// or for missing keys — never throws.
    /// </summary>
    internal static AlarmTriggerModel Parse(string? json, AlarmTriggerType type)
    {
        var model = new AlarmTriggerModel();
        if (string.IsNullOrWhiteSpace(json)) return model;

        try
        {
            using var doc = JsonDocument.Parse(json);
            var root = doc.RootElement;

            model.AttributeName =
                root.TryGetProperty("attributeName", out var a) ? a.GetString()
                : root.TryGetProperty("attribute", out var a2) ? a2.GetString()
                : null;

            switch (type)
            {
                case AlarmTriggerType.ValueMatch:
                {
                    var raw = root.TryGetProperty("matchValue", out var mv) ? mv.GetString()
                            : root.TryGetProperty("value", out var mv2) ? mv2.GetString()
                            : null;
                    if (raw != null && raw.StartsWith("!=", StringComparison.Ordinal))
                    {
                        model.NotEquals = true;
                        model.MatchValue = raw[2..];
                    }
                    else
                    {
                        model.MatchValue = raw;
                    }
                    break;
                }
                case AlarmTriggerType.RangeViolation:
                    model.Min = TryReadDouble(root, "min") ?? TryReadDouble(root, "low");
                    model.Max = TryReadDouble(root, "max") ?? TryReadDouble(root, "high");
                    break;

                case AlarmTriggerType.RateOfChange:
                    model.ThresholdPerSecond = TryReadDouble(root, "thresholdPerSecond");
                    model.WindowSeconds = TryReadDouble(root, "windowSeconds");
                    var dir = root.TryGetProperty("direction", out var d) ? d.GetString() : null;
                    model.Direction = NormalizeDirection(dir);
                    break;

                case AlarmTriggerType.HiLo:
                    model.LoLo = TryReadDouble(root, "loLo");
                    model.Lo = TryReadDouble(root, "lo");
                    model.Hi = TryReadDouble(root, "hi");
                    model.HiHi = TryReadDouble(root, "hiHi");
                    model.LoLoPriority = TryReadInt(root, "loLoPriority");
                    model.LoPriority = TryReadInt(root, "loPriority");
                    model.HiPriority = TryReadInt(root, "hiPriority");
                    model.HiHiPriority = TryReadInt(root, "hiHiPriority");
                    model.LoLoDeadband = TryReadDouble(root, "loLoDeadband");
                    model.LoDeadband = TryReadDouble(root, "loDeadband");
                    model.HiDeadband = TryReadDouble(root, "hiDeadband");
                    model.HiHiDeadband = TryReadDouble(root, "hiHiDeadband");
                    model.LoLoMessage = TryReadString(root, "loLoMessage");
                    model.LoMessage = TryReadString(root, "loMessage");
                    model.HiMessage = TryReadString(root, "hiMessage");
                    model.HiHiMessage = TryReadString(root, "hiHiMessage");
                    break;

                case AlarmTriggerType.Expression:
                    model.Expression = TryReadString(root, "expression");
                    break;
            }
        }
        catch (JsonException)
        {
            // Malformed JSON — fall through with default model.
        }

        return model;
    }

    /// <summary>
    /// Serializes the model to the JSON shape AlarmActor.ParseEvalConfig
    /// expects. Writes <c>attributeName</c> (canonical key) for the
    /// attribute-bound trigger types and only the keys relevant to the
    /// current trigger type. <c>Expression</c> is not bound to a single
    /// attribute, so <c>attributeName</c> is omitted for it.
    /// </summary>
    internal static string Serialize(AlarmTriggerModel model, AlarmTriggerType type)
    {
        using var stream = new MemoryStream();
        using (var w = new Utf8JsonWriter(stream))
        {
            w.WriteStartObject();
            if (type != AlarmTriggerType.Expression)
                w.WriteString("attributeName", model.AttributeName ?? "");

            switch (type)
            {
                case AlarmTriggerType.ValueMatch:
                    var mv = model.MatchValue ?? "";
                    if (model.NotEquals) mv = "!=" + mv;
                    w.WriteString("matchValue", mv);
                    break;

                case AlarmTriggerType.RangeViolation:
                    if (model.Min.HasValue) w.WriteNumber("min", model.Min.Value);
                    if (model.Max.HasValue) w.WriteNumber("max", model.Max.Value);
                    break;

                case AlarmTriggerType.RateOfChange:
                    if (model.ThresholdPerSecond.HasValue)
                        w.WriteNumber("thresholdPerSecond", model.ThresholdPerSecond.Value);
                    if (model.WindowSeconds.HasValue)
                        w.WriteNumber("windowSeconds", model.WindowSeconds.Value);
                    w.WriteString("direction", model.Direction);
                    break;

                case AlarmTriggerType.HiLo:
                    if (model.LoLo.HasValue) w.WriteNumber("loLo", model.LoLo.Value);
                    if (model.Lo.HasValue) w.WriteNumber("lo", model.Lo.Value);
                    if (model.Hi.HasValue) w.WriteNumber("hi", model.Hi.Value);
                    if (model.HiHi.HasValue) w.WriteNumber("hiHi", model.HiHi.Value);
                    if (model.LoLoPriority.HasValue) w.WriteNumber("loLoPriority", model.LoLoPriority.Value);
                    if (model.LoPriority.HasValue) w.WriteNumber("loPriority", model.LoPriority.Value);
                    if (model.HiPriority.HasValue) w.WriteNumber("hiPriority", model.HiPriority.Value);
                    if (model.HiHiPriority.HasValue) w.WriteNumber("hiHiPriority", model.HiHiPriority.Value);
                    if (model.LoLoDeadband.HasValue) w.WriteNumber("loLoDeadband", model.LoLoDeadband.Value);
                    if (model.LoDeadband.HasValue) w.WriteNumber("loDeadband", model.LoDeadband.Value);
                    if (model.HiDeadband.HasValue) w.WriteNumber("hiDeadband", model.HiDeadband.Value);
                    if (model.HiHiDeadband.HasValue) w.WriteNumber("hiHiDeadband", model.HiHiDeadband.Value);
                    if (!string.IsNullOrEmpty(model.LoLoMessage)) w.WriteString("loLoMessage", model.LoLoMessage);
                    if (!string.IsNullOrEmpty(model.LoMessage)) w.WriteString("loMessage", model.LoMessage);
                    if (!string.IsNullOrEmpty(model.HiMessage)) w.WriteString("hiMessage", model.HiMessage);
                    if (!string.IsNullOrEmpty(model.HiHiMessage)) w.WriteString("hiHiMessage", model.HiHiMessage);
                    break;

                case AlarmTriggerType.Expression:
                    w.WriteString("expression", model.Expression ?? "");
                    break;
            }

            w.WriteEndObject();
        }
        return Encoding.UTF8.GetString(stream.ToArray());
    }

    internal static string NormalizeDirection(string? raw) => raw?.ToLowerInvariant() switch
    {
        "rising" or "up" or "positive" => "rising",
        "falling" or "down" or "negative" => "falling",
        _ => "either"
    };

    private static double? TryReadDouble(JsonElement el, string name)
    {
        if (!el.TryGetProperty(name, out var p)) return null;
        return p.ValueKind switch
        {
            JsonValueKind.Number => p.GetDouble(),
            JsonValueKind.String when double.TryParse(p.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var v) => v,
            _ => null
        };
    }

    private static int? TryReadInt(JsonElement el, string name)
    {
        if (!el.TryGetProperty(name, out var p)) return null;
        return p.ValueKind switch
        {
            JsonValueKind.Number when p.TryGetInt32(out var i) => i,
            JsonValueKind.Number => (int)p.GetDouble(),
            JsonValueKind.String when int.TryParse(p.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) => v,
            _ => null
        };
    }

    private static string? TryReadString(JsonElement el, string name)
    {
        if (!el.TryGetProperty(name, out var p)) return null;
        return p.ValueKind == JsonValueKind.String ? p.GetString() : null;
    }
}

internal sealed class AlarmTriggerModel
{
    public string? AttributeName { get; set; }

    // ValueMatch
    public string? MatchValue { get; set; }
    public bool NotEquals { get; set; }

    // RangeViolation
    public double? Min { get; set; }
    public double? Max { get; set; }

    // RateOfChange
    public double? ThresholdPerSecond { get; set; }
    public double? WindowSeconds { get; set; }
    public string Direction { get; set; } = "either";

    // HiLo — any subset of setpoints may be set; per-setpoint priorities
    // override the alarm-level priority for that band.
    public double? LoLo { get; set; }
    public double? Lo { get; set; }
    public double? Hi { get; set; }
    public double? HiHi { get; set; }
    public int? LoLoPriority { get; set; }
    public int? LoPriority { get; set; }
    public int? HiPriority { get; set; }
    public int? HiHiPriority { get; set; }

    // Hysteresis: optional deactivation deadband per setpoint. Once at the
    // band, the setpoint threshold is relaxed by this amount before the alarm
    // de-escalates. Prevents flapping when the value hovers at the boundary.
    public double? LoLoDeadband { get; set; }
    public double? LoDeadband { get; set; }
    public double? HiDeadband { get; set; }
    public double? HiHiDeadband { get; set; }

    // Per-band operator message. Optional; surfaces on AlarmStateChanged.Message
    // and may be used by notification routing or operator displays.
    public string? LoLoMessage { get; set; }
    public string? LoMessage { get; set; }
    public string? HiMessage { get; set; }
    public string? HiHiMessage { get; set; }

    // Expression — boolean C# expression evaluated on attribute updates.
    public string? Expression { get; set; }
}
