@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
@using System.Globalization

@* Structured editor for a template script's trigger. Owns both the trigger-type
   selector and the type-specific configuration, emitting (via Changed) the
   canonical TriggerType string + the TriggerConfiguration JSON that
   ScriptActor.ParseTriggerConfig consumes:
     Interval     { intervalMs }
     ValueChange  { attributeName }
     Conditional  { attributeName, operator, threshold, mode }
     Expression   { expression, mode }
     Call         { }                                                        *@

<div class="border rounded bg-white p-3">

    @* ── Trigger type ──────────────────────────────────────────────────── *@
    <div class="mb-3">
        <label for="script-trigger-type" class="form-label small text-uppercase text-muted fw-semibold mb-1">
            Trigger type
        </label>
        <select id="script-trigger-type" class="form-select form-select-sm"
                @bind="_kindValue" @bind:after="OnKindChanged">
            <option value="None">— none (never runs automatically) —</option>
            <option value="Interval">Interval — run on a fixed timer</option>
            <option value="ValueChange">Value change — run when an attribute changes</option>
            <option value="Conditional">Conditional — run when a condition is met</option>
            <option value="Expression">Expression — run when a boolean expression becomes true</option>
            <option value="Call">Call — run only when invoked by another script</option>
            @if (_kind == ScriptTriggerKind.Unknown)
            {
                <optgroup label="Unrecognized">
                    <option value="Unknown">@_rawType (unknown)</option>
                </optgroup>
            }
        </select>
    </div>

    @* ── Type-specific configuration ───────────────────────────────────── *@
    @switch (_kind)
    {
        case ScriptTriggerKind.Interval:
            @RenderInterval();
            break;
        case ScriptTriggerKind.ValueChange:
            <div class="mb-1">@RenderAttributePicker("Monitored attribute")</div>
            break;
        case ScriptTriggerKind.Conditional:
            @RenderConditional();
            break;
        case ScriptTriggerKind.Expression:
            @RenderExpression();
            break;
        case ScriptTriggerKind.Call:
            <div class="small text-muted">
                No automatic trigger — this script runs only when another script
                invokes it via <code>Instance.CallScript("...")</code>.
            </div>
            break;
        case ScriptTriggerKind.Unknown:
            <div class="alert alert-warning py-2 small mb-0">
                Unrecognized trigger type <code>@_rawType</code>. Its stored
                configuration is shown below and left untouched — pick a known
                trigger type above to reconfigure it.
                <pre class="bg-light border rounded p-2 mt-2 mb-0">@(string.IsNullOrWhiteSpace(TriggerConfig) ? "(empty)" : TriggerConfig)</pre>
            </div>
            break;
    }

    @* ── Fire mode (Conditional + Expression) ──────────────────────────── *@
    @if (_kind is ScriptTriggerKind.Conditional or ScriptTriggerKind.Expression)
    {
        @RenderMode();
    }

    @* ── Hint ──────────────────────────────────────────────────────────── *@
    @if (_kind is ScriptTriggerKind.Interval or ScriptTriggerKind.ValueChange
         or ScriptTriggerKind.Conditional or ScriptTriggerKind.Expression)
    {
        <div class="mt-3 pt-2 border-top small text-muted">@BuildHint()</div>
    }
</div>

@code {
    // ── Parameters ─────────────────────────────────────────────────────────

    [Parameter] public string? TriggerType { get; set; }
    [Parameter] public string? TriggerConfig { get; set; }

    /// <summary>Raised whenever the type or config changes — emits both atomically.</summary>
    [Parameter] public EventCallback<ScriptTriggerValue> Changed { get; set; }

    /// <summary>Flattened attribute list (direct + inherited + composed) for the picker.</summary>
    [Parameter] public IReadOnlyList<AlarmAttributeChoice> AvailableAttributes { get; set; } =
        Array.Empty<AlarmAttributeChoice>();

    // ── Internal state ─────────────────────────────────────────────────────

    private ScriptTriggerKind _kind;
    private string _kindValue = "None";
    private string? _rawType;
    private ScriptTriggerModel _model = new();

    // Last type/config seen on Parameters — distinguishes an external change
    // (re-parse) from this component's own echo (skip).
    private bool _seen;
    private string? _lastType;
    private string? _lastConfig;

    // Text mirrors — @bind needs settable backing fields; kept in sync with the
    // model so blank inputs round-trip blank rather than as 0.
    private string _attributeName = string.Empty;
    private string _operator = ">";
    private string? _thresholdText;
    private string? _intervalText;
    private string _intervalUnit = "ms";

    // ── Parse / serialize lifecycle ────────────────────────────────────────

    protected override void OnParametersSet()
    {
        if (_seen && _lastType == TriggerType && _lastConfig == TriggerConfig) return;
        _seen = true;
        _lastType = TriggerType;
        _lastConfig = TriggerConfig;

        _rawType = TriggerType;
        _kind = ScriptTriggerConfigCodec.ParseKind(TriggerType);
        _kindValue = _kind.ToString();
        _model = ScriptTriggerConfigCodec.Parse(TriggerConfig, _kind);
        SyncMirrors();
    }

    private void SyncMirrors()
    {
        _attributeName = _model.AttributeName ?? string.Empty;
        _operator = _model.Operator;
        _thresholdText = _model.Threshold?.ToString("R", CultureInfo.InvariantCulture);
        (_intervalText, _intervalUnit) = SplitInterval(_model.IntervalMs);
    }

    /// <summary>Chooses the largest whole unit (min/sec/ms) that represents the period exactly.</summary>
    private static (string?, string) SplitInterval(long? ms)
    {
        if (ms is not { } v) return (null, "ms");
        if (v >= 60000 && v % 60000 == 0) return ((v / 60000).ToString(CultureInfo.InvariantCulture), "min");
        if (v >= 1000 && v % 1000 == 0) return ((v / 1000).ToString(CultureInfo.InvariantCulture), "sec");
        return (v.ToString(CultureInfo.InvariantCulture), "ms");
    }

    private static long UnitFactor(string unit) => unit switch
    {
        "min" => 60000,
        "sec" => 1000,
        _ => 1
    };

    /// <summary>Serializes the current model and raises <see cref="Changed"/> once.</summary>
    private async Task Emit()
    {
        var type = ScriptTriggerConfigCodec.KindToString(_kind);
        var config = ScriptTriggerConfigCodec.Serialize(_model, _kind);
        _lastType = type;
        _lastConfig = config;
        await Changed.InvokeAsync(new ScriptTriggerValue(type, config));
    }

    // ── Trigger type ───────────────────────────────────────────────────────

    private async Task OnKindChanged()
    {
        if (!Enum.TryParse<ScriptTriggerKind>(_kindValue, out var newKind)
            || newKind == ScriptTriggerKind.Unknown)
        {
            _kindValue = _kind.ToString();
            return;
        }

        // Carry the attribute name across a ValueChange <-> Conditional switch.
        var preservedAttr = _model.AttributeName;
        _kind = newKind;
        _model = new ScriptTriggerModel();
        if (newKind is ScriptTriggerKind.ValueChange or ScriptTriggerKind.Conditional)
            _model.AttributeName = preservedAttr;
        SyncMirrors();
        await Emit();
    }

    // ── Interval ───────────────────────────────────────────────────────────

    private RenderFragment RenderInterval() => __builder =>
    {
        <div class="row g-2 align-items-end" style="max-width: 420px;">
            <div class="col-7">
                <label class="form-label small text-uppercase text-muted fw-semibold mb-1">Run every</label>
                <input type="number" min="1" step="1" class="form-control form-control-sm"
                       placeholder="period"
                       @bind="_intervalText" @bind:event="oninput" @bind:after="OnIntervalChanged" />
            </div>
            <div class="col-5">
                <select class="form-select form-select-sm"
                        @bind="_intervalUnit" @bind:after="OnIntervalChanged">
                    <option value="ms">milliseconds</option>
                    <option value="sec">seconds</option>
                    <option value="min">minutes</option>
                </select>
            </div>
        </div>
    };

    private async Task OnIntervalChanged()
    {
        _model.IntervalMs =
            long.TryParse(_intervalText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) && n > 0
                ? n * UnitFactor(_intervalUnit)
                : null;
        await Emit();
    }

    // ── Conditional ────────────────────────────────────────────────────────

    private RenderFragment RenderConditional() => __builder =>
    {
        <div class="mb-2">@RenderAttributePicker("Monitored attribute")</div>
        <div class="row g-2 align-items-end">
            <div class="col-md-5">
                <label class="form-label small text-uppercase text-muted fw-semibold mb-1">Operator</label>
                <select class="form-select form-select-sm"
                        @bind="_operator" @bind:after="OnOperatorChanged">
                    @foreach (var op in ScriptTriggerConfigCodec.Operators)
                    {
                        <option value="@op">@op — @OperatorLabel(op)</option>
                    }
                </select>
            </div>
            <div class="col-md-7">
                <label class="form-label small text-uppercase text-muted fw-semibold mb-1">Threshold</label>
                <input type="number" step="any" class="form-control form-control-sm"
                       placeholder="numeric value"
                       @bind="_thresholdText" @bind:event="oninput" @bind:after="OnThresholdChanged" />
            </div>
        </div>
    };

    private async Task OnOperatorChanged()
    {
        _model.Operator = ScriptTriggerConfigCodec.NormalizeOperator(_operator);
        await Emit();
    }

    private async Task OnThresholdChanged()
    {
        _model.Threshold =
            double.TryParse(_thresholdText, NumberStyles.Float, CultureInfo.InvariantCulture, out var v)
                ? v
                : null;
        await Emit();
    }

    // ── Expression ─────────────────────────────────────────────────────────

    private RenderFragment RenderExpression() => __builder =>
    {
        <label class="form-label small text-uppercase text-muted fw-semibold mb-1">Trigger expression</label>
        <MonacoEditor Height="120px"
                      Language="csharp"
                      ScriptKind="ScriptAnalysis.ScriptKind.Template"
                      ShowToolbar="false"
                      Value="@(_model.Expression ?? string.Empty)"
                      ValueChanged="OnExpressionChanged"
                      SelfAttributes="@TriggerAttributeMapper.SelfAttributes(AvailableAttributes)"
                      Children="@TriggerAttributeMapper.Children(AvailableAttributes)" />
        <div class="form-text">
            A boolean C# expression — e.g. <code>Attributes["Temperature"] &gt; 80</code>.
        </div>
    };

    private async Task OnExpressionChanged(string value)
    {
        _model.Expression = value;
        await Emit();
    }

    // ── Fire mode (Conditional + Expression) ───────────────────────────────

    private RenderFragment RenderMode() => __builder =>
    {
        <div class="mt-3">
            <label for="script-trigger-mode" class="form-label small text-uppercase text-muted fw-semibold mb-1">
                Fire mode
            </label>
            <select id="script-trigger-mode" class="form-select form-select-sm"
                    style="max-width: 420px;"
                    @bind="_model.Mode" @bind:after="OnModeChanged">
                <option value="@ScriptTriggerMode.OnTrue">
                    Once — when the condition becomes true
                </option>
                <option value="@ScriptTriggerMode.WhileTrue">
                    Repeatedly — while the condition stays true
                </option>
            </select>
            @if (_model.Mode == ScriptTriggerMode.WhileTrue)
            {
                <div class="form-text">
                    Re-runs on a timer while the condition holds, at the script's
                    <strong>Min time between runs</strong> interval — set that field below
                    the script editor, or the trigger fires only once.
                </div>
            }
        </div>
    };

    private async Task OnModeChanged() => await Emit();

    // ── Attribute picker (ValueChange + Conditional) ───────────────────────

    private RenderFragment RenderAttributePicker(string label) => __builder =>
    {
        <label class="form-label small text-uppercase text-muted fw-semibold mb-1">@label</label>
        <select class="form-select form-select-sm"
                @bind="_attributeName" @bind:after="OnAttributeChanged">
            <option value="">— select attribute —</option>
            @{
                var groups = AvailableAttributes
                    .GroupBy(c => c.Source)
                    .OrderBy(g => SourceOrder(g.Key))
                    .ToList();
            }
            @foreach (var grp in groups)
            {
                <optgroup label="@grp.Key">
                    @foreach (var choice in grp.OrderBy(c => c.CanonicalName, StringComparer.Ordinal))
                    {
                        <option value="@choice.CanonicalName">@choice.CanonicalName &nbsp;(@choice.DataType)</option>
                    }
                </optgroup>
            }
            @* Keep a saved-but-missing attribute selectable so it stays visible. *@
            @if (!string.IsNullOrEmpty(_attributeName)
                 && !AvailableAttributes.Any(c => string.Equals(c.CanonicalName, _attributeName, StringComparison.Ordinal)))
            {
                <optgroup label="Unknown">
                    <option value="@_attributeName">@_attributeName (not on this template)</option>
                </optgroup>
            }
        </select>
    };

    private async Task OnAttributeChanged()
    {
        _model.AttributeName = _attributeName;
        await Emit();
    }

    private static int SourceOrder(string source) => source switch
    {
        "Direct" => 0,
        "Inherited" => 1,
        "Composed" => 2,
        _ => 3
    };

    // ── Hint ───────────────────────────────────────────────────────────────

    private string BuildHint()
    {
        var attr = string.IsNullOrWhiteSpace(_model.AttributeName)
            ? "the selected attribute"
            : $"\"{_model.AttributeName}\"";

        return _kind switch
        {
            ScriptTriggerKind.Interval =>
                _model.IntervalMs is { } ms
                    ? $"Runs every {_intervalText} {UnitLabel(_intervalUnit)} ({ms} ms)."
                    : "Runs on a fixed timer — set the period above.",

            ScriptTriggerKind.ValueChange =>
                $"Runs whenever {attr} changes value.",

            ScriptTriggerKind.Conditional =>
                _model.Threshold is { } t
                    ? (_model.Mode == ScriptTriggerMode.WhileTrue
                        ? $"Re-runs while {attr} {_model.Operator} {t.ToString("0.###", CultureInfo.InvariantCulture)}."
                        : $"Runs when {attr} changes, if {attr} {_model.Operator} {t.ToString("0.###", CultureInfo.InvariantCulture)}.")
                    : $"Runs when {attr} changes and meets the configured condition — set a threshold above.",

            ScriptTriggerKind.Expression =>
                _model.Mode == ScriptTriggerMode.WhileTrue
                    ? "Re-runs while this expression stays true."
                    : "Runs once each time this expression becomes true.",

            _ => string.Empty
        };
    }

    private static string UnitLabel(string unit) => unit switch
    {
        "min" => "minute(s)",
        "sec" => "second(s)",
        _ => "millisecond(s)"
    };

    private static string OperatorLabel(string op) => op switch
    {
        ">" => "greater than",
        ">=" => "at least",
        "<" => "less than",
        "<=" => "at most",
        "==" => "equals",
        "!=" => "not equal",
        _ => ""
    };
}
