using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;

/// <summary>How an attribute change should be routed to a child (script/alarm) actor.</summary>
internal enum TriggerRoutingKind
{
    /// <summary>Reacts only to changes of a single named attribute.</summary>
    MonitoredAttribute,

    /// <summary>Reacts to ANY attribute change (Expression triggers; or fail-open when the
    /// trigger config is unparseable — the child's own gate then discards what it ignores).</summary>
    AllChanges,

    /// <summary>Never reacts to attribute changes (interval / call-only scripts).</summary>
    None
}

/// <summary>
/// Result of classifying a child's trigger for attribute-change routing.
/// </summary>
internal readonly record struct TriggerRoutingDecision(TriggerRoutingKind Kind, string? MonitoredAttribute)
{
    public static readonly TriggerRoutingDecision AllChanges = new(TriggerRoutingKind.AllChanges, null);
    public static readonly TriggerRoutingDecision None = new(TriggerRoutingKind.None, null);

    /// <summary>Creates a decision routing on changes to a single named attribute.</summary>
    /// <param name="attribute">The name of the attribute to monitor.</param>
    /// <returns>A <see cref="TriggerRoutingDecision"/> of kind <see cref="TriggerRoutingKind.MonitoredAttribute"/>.</returns>
    public static TriggerRoutingDecision Monitored(string attribute) => new(TriggerRoutingKind.MonitoredAttribute, attribute);
}

/// <summary>
/// Single source of truth for the monitored attribute a script/alarm trigger reacts to (P2).
/// Shared by <see cref="ScriptActor"/> (attribute-name extraction) and
/// <c>InstanceActor.CreateChildActors</c> (routing-map construction) so both agree on the
/// attribute for a given trigger config.
///
/// NOTE: routing has three outcomes (monitored / all-changes / none), so this returns a
/// <see cref="TriggerRoutingDecision"/> rather than the plan's original bool-out signature —
/// a bool cannot distinguish "hears everything" from "hears nothing".
/// </summary>
internal static class TriggerRouting
{
    /// <summary>
    /// Reads the monitored attribute name from a trigger config, honoring both the
    /// <c>"attributeName"</c> (scripts + alarms) and <c>"attribute"</c> (alarm alias) keys.
    /// Returns false when the JSON is missing, malformed, or carries no attribute name.
    /// </summary>
    /// <param name="triggerConfigJson">The trigger config JSON to read from.</param>
    /// <param name="attribute">When this method returns, the monitored attribute name, or an empty string if not found.</param>
    /// <returns><c>true</c> if an attribute name was found; otherwise <c>false</c>.</returns>
    public static bool TryReadAttributeName(string? triggerConfigJson, out string attribute)
    {
        attribute = "";
        if (string.IsNullOrEmpty(triggerConfigJson)) return false;
        try
        {
            using var doc = JsonDocument.Parse(triggerConfigJson);
            var root = doc.RootElement;
            if (root.TryGetProperty("attributeName", out var a) && a.GetString() is { Length: > 0 } n)
            {
                attribute = n;
                return true;
            }
            if (root.TryGetProperty("attribute", out var a2) && a2.GetString() is { Length: > 0 } n2)
            {
                attribute = n2;
                return true;
            }
            return false;
        }
        catch (JsonException)
        {
            return false;
        }
    }

    /// <summary>
    /// Classifies a script trigger. valuechange/conditional → the named attribute (fail open to
    /// all-changes if unparseable); expression → all-changes; interval/call → none; anything
    /// unknown → all-changes (fail open — a misconfigured script still reacts, its own gate
    /// remains the correctness check).
    /// </summary>
    /// <param name="triggerType">The script's trigger type (e.g. "valuechange", "expression", "interval", "call").</param>
    /// <param name="triggerConfigJson">The trigger config JSON, used to extract the monitored attribute name where applicable.</param>
    /// <returns>The routing decision for the script's trigger.</returns>
    public static TriggerRoutingDecision ForScript(string? triggerType, string? triggerConfigJson)
    {
        switch (triggerType?.ToLowerInvariant())
        {
            case "valuechange":
            case "conditional":
                return TryReadAttributeName(triggerConfigJson, out var attr)
                    ? TriggerRoutingDecision.Monitored(attr)
                    : TriggerRoutingDecision.AllChanges;
            case "expression":
                return TriggerRoutingDecision.AllChanges;
            case "interval":
            case "call":
                return TriggerRoutingDecision.None;
            default:
                return TriggerRoutingDecision.AllChanges;
        }
    }

    /// <summary>
    /// Classifies an alarm trigger. Expression → all-changes; HiLo/ValueMatch/RangeViolation/
    /// RateOfChange → the named attribute (fail open to all-changes if unparseable); an
    /// unknown/unparseable trigger type → all-changes (fail open).
    /// </summary>
    /// <param name="triggerType">The alarm's trigger type (e.g. "Expression", "HiLo", "ValueMatch").</param>
    /// <param name="triggerConfigJson">The trigger config JSON, used to extract the monitored attribute name where applicable.</param>
    /// <returns>The routing decision for the alarm's trigger.</returns>
    public static TriggerRoutingDecision ForAlarm(string? triggerType, string? triggerConfigJson)
    {
        if (Enum.TryParse<AlarmTriggerType>(triggerType, ignoreCase: true, out var tt))
        {
            switch (tt)
            {
                case AlarmTriggerType.Expression:
                    return TriggerRoutingDecision.AllChanges;
                case AlarmTriggerType.HiLo:
                case AlarmTriggerType.ValueMatch:
                case AlarmTriggerType.RangeViolation:
                case AlarmTriggerType.RateOfChange:
                    return TryReadAttributeName(triggerConfigJson, out var attr)
                        ? TriggerRoutingDecision.Monitored(attr)
                        : TriggerRoutingDecision.AllChanges;
            }
        }
        return TriggerRoutingDecision.AllChanges;
    }
}
