@* Typed TagConfig editor for the Calculation pseudo-driver. Authors the calc binding —
   scriptId + change/timer triggers — reusing the VirtualTagModal's script dropdown, "New
   script" action, and inline Monaco source panel (all backed by the same IUnsTreeService
   script seams). Dispatched from RawTagModal / RawManualTagEntryModal via TagConfigEditorMap,
   so it takes the same (ConfigJson/ConfigJsonChanged/DriverType/GetDriverConfigJson) parameter
   shape every typed editor takes; DriverType/GetDriverConfigJson are accepted for dispatch
   uniformity but unused (a calc tag has no address to browse). *@
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors
@inject IUnsTreeService Svc

<div class="row g-2">
    <div class="col-md-6">
        <label class="form-label">Script</label>
        <select class="form-select form-select-sm" value="@_m.ScriptId" @onchange="OnScriptChangedAsync">
            <option value="">— pick script —</option>
            @foreach (var (id, display) in _scripts)
            {
                <option value="@id">@display</option>
            }
        </select>
    </div>
    <div class="col-md-3">
        <label class="form-label">Change-triggered</label>
        <div class="form-check form-switch">
            <input class="form-check-input" type="checkbox" id="calc-change-@_uid"
                   checked="@_m.ChangeTriggered"
                   @onchange="@(e => Update(() => _m.ChangeTriggered = e.Value is true))" />
            <label class="form-check-label small" for="calc-change-@_uid">On dependency change</label>
        </div>
    </div>
    <div class="col-md-3">
        <label class="form-label">Timer (ms)</label>
        <input type="number" min="50" step="1" class="form-control form-control-sm"
               value="@_m.TimerIntervalMs"
               @onchange="@(e => Update(() => _m.TimerIntervalMs = ParseTimer(e.Value)))" />
        <div class="form-text">Blank = change-trigger only. Min 50 ms.</div>
    </div>
</div>

@* No script bound yet → offer a one-click "New script" that creates a blank Script row, binds
   it, and expands the inline editor so the operator can author the body without leaving. *@
@if (string.IsNullOrEmpty(_m.ScriptId))
{
    <div class="mt-2">
        <button type="button" class="btn btn-outline-primary btn-sm"
                @onclick="CreateNewScriptAsync" disabled="@_scriptCreating">
            @if (_scriptCreating) { <span class="spinner-border spinner-border-sm me-1"></span> }
            New script
        </button>
        <span class="form-text ms-2">Creates a blank C# script bound to this calc tag.</span>
        @if (!string.IsNullOrWhiteSpace(_scriptCreateError))
        {
            <div class="text-danger small mt-2">@_scriptCreateError</div>
        }
    </div>
}

@* Inline script-source editor — shown only when a script is bound. Saves the SHARED Script row on
   its own concurrency-guarded button; independent of the enclosing tag save. *@
@if (!string.IsNullOrEmpty(_m.ScriptId) && _scriptLoaded)
{
    <div class="card mt-2">
        <div class="card-header py-2 d-flex justify-content-between align-items-center">
            <span class="fw-semibold">Script source</span>
            <button type="button" class="btn btn-link btn-sm p-0 text-decoration-none"
                    @onclick="ToggleScriptPanel">
                @(_scriptExpanded ? "Hide" : "Edit source")
            </button>
        </div>
        @if (_scriptExpanded)
        {
            <div class="card-body">
                <div class="alert alert-warning py-2 small mb-2">
                    Editing shared script "<span class="mono">@_scriptName</span>" — used by
                    @_scriptUsageCount virtual tag(s). Changes affect every tag using it.
                </div>
                <MonacoEditor @bind-Value="_scriptSource" Height="300px" />
                @if (!string.IsNullOrWhiteSpace(_scriptError))
                {
                    <div class="text-danger small mt-2">@_scriptError</div>
                }
                else if (_scriptSaved)
                {
                    <div class="text-success small mt-2">Script saved.</div>
                }
                <div class="mt-2">
                    <button type="button" class="btn btn-outline-primary btn-sm"
                            @onclick="SaveScriptAsync" disabled="@_scriptSaving">
                        @if (_scriptSaving) { <span class="spinner-border spinner-border-sm me-1"></span> }
                        Save script
                    </button>
                </div>
            </div>
        }
    </div>
}

@code {
    [Parameter] public string? ConfigJson { get; set; }
    [Parameter] public EventCallback<string> ConfigJsonChanged { get; set; }
    /// <summary>DriverType of the selected driver — accepted for dispatch uniformity; unused here.</summary>
    [Parameter] public string DriverType { get; set; } = "";
    /// <summary>Live accessor for the selected driver's DriverConfig — accepted for dispatch uniformity;
    /// unused (a calc tag has no address to browse).</summary>
    [Parameter] public Func<string> GetDriverConfigJson { get; set; } = () => "{}";

    private CalculationTagConfigModel _m = new();
    private string? _lastConfigJson;
    // Unique suffix so multiple editor instances (e.g. manual-entry grid rows) get distinct control ids.
    private readonly string _uid = Guid.NewGuid().ToString("N")[..8];

    // Script dropdown options — loaded once, plus any inline-created script appended.
    private List<(string Id, string Display)> _scripts = new();

    // Inline script-source editor state (independent of the tag save).
    private string _scriptSource = "";
    private bool _scriptLoaded;
    private byte[] _scriptRowVersion = Array.Empty<byte>();
    private string? _scriptName;
    private int _scriptUsageCount;
    private bool _scriptExpanded;
    private bool _scriptSaving;
    private bool _scriptSaved;
    private string? _scriptError;

    // "New script" action state.
    private bool _scriptCreating;
    private string? _scriptCreateError;

    protected override async Task OnInitializedAsync()
    {
        var scripts = await Svc.LoadScriptsAsync();
        _scripts = scripts.Select(s => (s.ScriptId, s.Display)).ToList();
    }

    // Re-parse only when the incoming JSON actually changes, so an unrelated parent re-render
    // (Blazor Server live-status pushes) can't reset the operator's in-progress edits.
    protected override async Task OnParametersSetAsync()
    {
        if (ConfigJson == _lastConfigJson) { return; }
        _lastConfigJson = ConfigJson;
        _m = CalculationTagConfigModel.FromJson(ConfigJson);
        await LoadScriptSourceAsync();
    }

    private async Task Update(Action apply)
    {
        apply();
        await WriteBackAsync();
    }

    // Push the model back to the host as JSON and pin _lastConfigJson so the echo doesn't re-parse.
    private async Task WriteBackAsync()
    {
        var json = _m.ToJson();
        _lastConfigJson = json;
        await ConfigJsonChanged.InvokeAsync(json);
    }

    private static int? ParseTimer(object? v)
        => int.TryParse(v?.ToString(), out var i) && i > 0 ? i : null;

    /// <summary>Refreshes the inline script panel + writes the new binding back when the operator picks
    /// a different script.</summary>
    private async Task OnScriptChangedAsync(ChangeEventArgs e)
    {
        _m.ScriptId = e.Value?.ToString() ?? "";
        await WriteBackAsync();
        await LoadScriptSourceAsync();
    }

    /// <summary>Loads the bound script's source for the inline panel; clears it when no script is bound.</summary>
    private async Task LoadScriptSourceAsync()
    {
        _scriptSaved = false;
        _scriptError = null;

        if (string.IsNullOrEmpty(_m.ScriptId))
        {
            _scriptSource = "";
            _scriptLoaded = false;
            _scriptRowVersion = Array.Empty<byte>();
            _scriptName = null;
            _scriptUsageCount = 0;
            return;
        }

        var loaded = await Svc.GetScriptSourceAsync(_m.ScriptId);
        if (loaded is null)
        {
            _scriptSource = "";
            _scriptLoaded = false;
            _scriptRowVersion = Array.Empty<byte>();
            _scriptName = null;
            _scriptUsageCount = 0;
            return;
        }

        _scriptSource = loaded.Value.SourceCode;
        _scriptLoaded = true;
        _scriptRowVersion = loaded.Value.RowVersion;
        _scriptName = loaded.Value.Name;
        _scriptUsageCount = await Svc.CountVirtualTagsUsingScriptAsync(_m.ScriptId);
    }

    /// <summary>Creates a blank script, binds this calc tag to it, and expands the inline editor. Persists
    /// only the new Script row — the tag binding lives in the model until the host's save.</summary>
    private async Task CreateNewScriptAsync()
    {
        _scriptCreating = true;
        _scriptCreateError = null;
        try
        {
            const string seedName = "Calc script";
            var result = await Svc.CreateScriptAsync(seedName);
            if (!result.Ok || string.IsNullOrEmpty(result.CreatedId))
            {
                _scriptCreateError = result.Error ?? "Could not create the script.";
                return;
            }

            // Add the option BEFORE binding so the <select> can resolve the label on first render.
            if (!_scripts.Any(s => s.Id == result.CreatedId))
            {
                _scripts.Add((result.CreatedId, $"{seedName} (CSharp)"));
            }
            _m.ScriptId = result.CreatedId;
            await WriteBackAsync();
            await LoadScriptSourceAsync();
            _scriptExpanded = true;
        }
        finally
        {
            _scriptCreating = false;
        }
    }

    /// <summary>Toggles the inline script-source panel, clearing a stale "saved" banner on (re)expand.</summary>
    private void ToggleScriptPanel()
    {
        _scriptExpanded = !_scriptExpanded;
        if (_scriptExpanded) { _scriptSaved = false; }
    }

    /// <summary>Saves the edited script body via its own RowVersion-guarded call. Wholly separate from the
    /// tag save: never touches the model or closes the enclosing modal.</summary>
    private async Task SaveScriptAsync()
    {
        if (string.IsNullOrEmpty(_m.ScriptId) || !_scriptLoaded) { return; }

        _scriptSaving = true;
        _scriptSaved = false;
        _scriptError = null;
        try
        {
            var result = await Svc.UpdateScriptSourceAsync(_m.ScriptId, _scriptSource, _scriptRowVersion);
            if (result.Ok)
            {
                var reloaded = await Svc.GetScriptSourceAsync(_m.ScriptId);
                if (reloaded is not null)
                {
                    _scriptSource = reloaded.Value.SourceCode;
                    _scriptRowVersion = reloaded.Value.RowVersion;
                    _scriptName = reloaded.Value.Name;
                }
                _scriptSaved = true;
            }
            else
            {
                _scriptError = result.Error;
            }
        }
        finally
        {
            _scriptSaving = false;
        }
    }
}
