using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Json;

namespace ScadaLink.CentralUI.Components.Shared;

/// <summary>
/// Which kind of trigger a template script has. <see cref="None"/> is no
/// trigger; <see cref="Unknown"/> is a stored trigger-type string the runtime
/// does not recognize (preserved as-is by the editor).
/// </summary>
internal enum ScriptTriggerKind { None, Interval, ValueChange, Conditional, Call, Expression, Unknown }

/// <summary>A script's trigger as the editor emits it: a type string + config JSON.</summary>
public sealed record ScriptTriggerValue(string? TriggerType, string? Config);

/// <summary>Parsed/editable view of a script trigger's configuration.</summary>
internal sealed class ScriptTriggerModel
{
    /// <summary>Interval period in milliseconds.</summary>
    public long? IntervalMs { get; set; }

    /// <summary>Monitored attribute (ValueChange + Conditional).</summary>
    public string? AttributeName { get; set; }

    /// <summary>Comparison operator (Conditional) — one of <see cref="ScriptTriggerConfigCodec.Operators"/>.</summary>
    public string Operator { get; set; } = ">";

    /// <summary>Comparison threshold (Conditional).</summary>
    public double? Threshold { get; set; }

    /// <summary>Boolean C# expression (Expression).</summary>
    public string? Expression { get; set; }
}

/// <summary>
/// Round-trip codec for a template script's <c>TriggerType</c> +
/// <c>TriggerConfiguration</c>, shared by <see cref="ScriptTriggerEditor"/> (UI
/// editing) and consumed at the site by <c>ScriptActor.ParseTriggerConfig</c>.
/// Serialized config shapes:
///   Interval     { intervalMs }
///   ValueChange  { attributeName }
///   Conditional  { attributeName, operator, threshold }
///   Call         { }
///   Expression   { expression }
///
/// Parsing also accepts the legacy aliases <c>attribute</c> and <c>value</c> so
/// older configs survive a round-trip through the editor.
/// </summary>
internal static class ScriptTriggerConfigCodec
{
    /// <summary>The six comparison operators <c>ScriptActor.EvaluateCondition</c> accepts.</summary>
    internal static readonly string[] Operators = { ">", ">=", "<", "<=", "==", "!=" };

    /// <summary>Classifies a raw <c>TriggerType</c> string (case-insensitive).</summary>
    internal static ScriptTriggerKind ParseKind(string? triggerType)
    {
        if (string.IsNullOrWhiteSpace(triggerType)) return ScriptTriggerKind.None;
        return triggerType.Trim().ToLowerInvariant() switch
        {
            "interval" => ScriptTriggerKind.Interval,
            "valuechange" => ScriptTriggerKind.ValueChange,
            "conditional" => ScriptTriggerKind.Conditional,
            "call" => ScriptTriggerKind.Call,
            "expression" => ScriptTriggerKind.Expression,
            _ => ScriptTriggerKind.Unknown
        };
    }

    /// <summary>Canonical <c>TriggerType</c> string for a kind; null for None/Unknown.</summary>
    internal static string? KindToString(ScriptTriggerKind kind) => kind switch
    {
        ScriptTriggerKind.Interval => "Interval",
        ScriptTriggerKind.ValueChange => "ValueChange",
        ScriptTriggerKind.Conditional => "Conditional",
        ScriptTriggerKind.Call => "Call",
        ScriptTriggerKind.Expression => "Expression",
        _ => null
    };

    /// <summary>
    /// Parses a trigger configuration JSON in the context of the given kind.
    /// Returns a model with default values on null/empty/malformed input or for
    /// missing keys — never throws.
    /// </summary>
    internal static ScriptTriggerModel Parse(string? json, ScriptTriggerKind kind)
    {
        var model = new ScriptTriggerModel();
        if (string.IsNullOrWhiteSpace(json)) return model;

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

            switch (kind)
            {
                case ScriptTriggerKind.Interval:
                    model.IntervalMs = TryReadLong(root, "intervalMs");
                    break;

                case ScriptTriggerKind.ValueChange:
                    model.AttributeName = TryReadAttributeName(root);
                    break;

                case ScriptTriggerKind.Conditional:
                    model.AttributeName = TryReadAttributeName(root);
                    var op = root.TryGetProperty("operator", out var o) ? o.GetString() : null;
                    model.Operator = NormalizeOperator(op);
                    model.Threshold = TryReadDouble(root, "threshold") ?? TryReadDouble(root, "value");
                    break;

                case ScriptTriggerKind.Expression:
                    model.Expression = root.TryGetProperty("expression", out var e) ? e.GetString() : null;
                    break;
            }
        }
        catch (JsonException)
        {
            // Malformed JSON — fall through with default model.
        }

        return model;
    }

    /// <summary>
    /// Serializes the model to the JSON shape <c>ScriptActor.ParseTriggerConfig</c>
    /// expects. Returns null for None/Unknown (no structured config to emit).
    /// </summary>
    internal static string? Serialize(ScriptTriggerModel model, ScriptTriggerKind kind)
    {
        if (kind is ScriptTriggerKind.None or ScriptTriggerKind.Unknown) return null;

        using var stream = new MemoryStream();
        using (var w = new Utf8JsonWriter(stream))
        {
            w.WriteStartObject();
            switch (kind)
            {
                case ScriptTriggerKind.Interval:
                    if (model.IntervalMs.HasValue)
                        w.WriteNumber("intervalMs", model.IntervalMs.Value);
                    break;

                case ScriptTriggerKind.ValueChange:
                    w.WriteString("attributeName", model.AttributeName ?? "");
                    break;

                case ScriptTriggerKind.Conditional:
                    w.WriteString("attributeName", model.AttributeName ?? "");
                    w.WriteString("operator", model.Operator);
                    if (model.Threshold.HasValue)
                        w.WriteNumber("threshold", model.Threshold.Value);
                    break;

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

                // Call → empty object.
            }
            w.WriteEndObject();
        }
        return Encoding.UTF8.GetString(stream.ToArray());
    }

    /// <summary>Returns <paramref name="raw"/> if it is a recognized operator, else "&gt;".</summary>
    internal static string NormalizeOperator(string? raw)
    {
        var op = raw?.Trim();
        return op != null && Array.IndexOf(Operators, op) >= 0 ? op : ">";
    }

    private static string? TryReadAttributeName(JsonElement root) =>
        root.TryGetProperty("attributeName", out var a) ? a.GetString()
        : root.TryGetProperty("attribute", out var a2) ? a2.GetString()
        : null;

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

    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
        };
    }
}
