@* Create/edit modal for an equipment-bound virtual tag, wired straight into IUnsTreeService. The host
   page owns visibility and supplies the owning equipment id (create) or the loaded VirtualTagEditDto
   (edit), plus the script list. Virtual tags are always scoped to an equipment (plan decision #2) and
   that binding never moves, so this modal deliberately offers NO equipment selector — the owning
   equipment is fixed. On a successful save it raises OnSaved so the host can refresh the equipment's
   children in place. *@
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
@inject IUnsTreeService Svc

@if (Visible)
{
    <div class="modal-backdrop fade show" style="display:block"></div>
    <div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
        <div class="modal-dialog modal-lg" role="document">
            <div class="modal-content">
                <EditForm Model="_form" OnValidSubmit="SaveAsync" FormName="virtualTagModal">
                    <DataAnnotationsValidator />
                    <div class="modal-header">
                        <h5 class="modal-title">@(IsNew ? "New virtual tag" : "Edit virtual tag")</h5>
                        <button type="button" class="btn-close" aria-label="Close" @onclick="CancelAsync"></button>
                    </div>
                    <div class="modal-body">
                        <div class="row">
                            <div class="col-md-6 mb-3">
                                <label class="form-label" for="vtag-id">VirtualTagId</label>
                                <InputText id="vtag-id" @bind-Value="_form.VirtualTagId" disabled="@(!IsNew)"
                                           class="form-control form-control-sm mono" />
                                <ValidationMessage For="@(() => _form.VirtualTagId)" />
                            </div>
                            <div class="col-md-6 mb-3">
                                <label class="form-label" for="vtag-name">Name</label>
                                <InputText id="vtag-name" @bind-Value="_form.Name" class="form-control form-control-sm" />
                                <ValidationMessage For="@(() => _form.Name)" />
                            </div>
                        </div>
                        <div class="row">
                            <div class="col-md-6 mb-3">
                                <label class="form-label" for="vtag-dtype">DataType</label>
                                <InputSelect id="vtag-dtype" @bind-Value="_form.DataType" class="form-select form-select-sm">
                                    @foreach (var dt in DataTypes)
                                    {
                                        <option value="@dt">@dt</option>
                                    }
                                </InputSelect>
                            </div>
                            <div class="col-md-6 mb-3">
                                <label class="form-label" for="vtag-script">Script</label>
                                <InputSelect id="vtag-script" @bind-Value="_form.ScriptId"
                                             @bind-Value:after="OnScriptChangedAsync" class="form-select form-select-sm">
                                    <option value="">— pick script —</option>
                                    @foreach (var (id, display) in _scripts)
                                    {
                                        <option value="@id">@display</option>
                                    }
                                </InputSelect>
                                <ValidationMessage For="@(() => _form.ScriptId)" />
                            </div>
                        </div>

                        @* No script bound yet → offer a one-click "New script" action that creates a
                           fresh (blank) Script row, binds this virtual tag to it, and expands the inline
                           editor below so the operator can author the body without leaving the modal. *@
                        @if (string.IsNullOrEmpty(_form.ScriptId))
                        {
                            <div class="mb-3">
                                <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 script bound to this virtual 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. This panel saves
                           the SHARED Script row on its OWN concurrency-guarded button — it is deliberately
                           separate from the virtual-tag Create/Save below and never touches _form or closes
                           the modal. *@
                        @if (!string.IsNullOrEmpty(_form.ScriptId) && _scriptLoaded)
                        {
                            <div class="card mb-3">
                                <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 all of them.
                                        </div>
                                        <MonacoEditor @bind-Value="_scriptSource" Height="300px" EquipmentId="@EquipmentId" />
                                        @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>
                        }
                        <div class="row">
                            <div class="col-md-4 mb-3">
                                <label class="form-label">Change-triggered</label>
                                <div class="form-check form-switch">
                                    <InputCheckbox @bind-Value="_form.ChangeTriggered" class="form-check-input" />
                                    <label class="form-check-label">Re-evaluate on dependency change</label>
                                </div>
                            </div>
                            <div class="col-md-4 mb-3">
                                <label class="form-label" for="vtag-timer">TimerIntervalMs (optional)</label>
                                <InputNumber id="vtag-timer" @bind-Value="_form.TimerIntervalMs" class="form-control form-control-sm" />
                                <div class="form-text">Periodic re-evaluation. Null = change-trigger only.</div>
                            </div>
                            <div class="col-md-4 mb-3">
                                <label class="form-label">Historize</label>
                                <div class="form-check form-switch">
                                    <InputCheckbox @bind-Value="_form.Historize" class="form-check-input" />
                                    <label class="form-check-label">Send to HistorianGateway</label>
                                </div>
                            </div>
                        </div>
                        <div class="mb-3">
                            <label class="form-label">Enabled</label>
                            <div class="form-check form-switch">
                                <InputCheckbox @bind-Value="_form.Enabled" class="form-check-input" />
                                <label class="form-check-label">Spawn this virtual tag in deployments</label>
                            </div>
                        </div>

                        @if (!string.IsNullOrWhiteSpace(_error))
                        {
                            <div class="text-danger small mt-2">@_error</div>
                        }
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-outline-secondary" @onclick="CancelAsync" disabled="@(_busy || _scriptSaving)">Cancel</button>
                        <button type="submit" class="btn btn-primary" disabled="@(_busy || _scriptSaving)">
                            @if (_busy) { <span class="spinner-border spinner-border-sm me-1"></span> }
                            @(IsNew ? "Create" : "Save changes")
                        </button>
                    </div>
                </EditForm>
            </div>
        </div>
    </div>
}

@code {
    /// <summary>The OPC UA data types offered for a virtual tag — the same set the TagModal uses.</summary>
    private static readonly string[] DataTypes =
        ["Boolean", "SByte", "Byte", "Int16", "UInt16", "Int32", "UInt32",
         "Int64", "UInt64", "Float", "Double", "String", "DateTime", "Guid", "ByteString"];

    /// <summary>Whether the modal is shown. The host owns this flag.</summary>
    [Parameter] public bool Visible { get; set; }

    /// <summary><c>true</c> to create a new virtual tag; <c>false</c> to edit <see cref="Existing"/>.</summary>
    [Parameter] public bool IsNew { get; set; }

    /// <summary>The owning equipment id the created virtual tag binds to (used only on create).</summary>
    [Parameter] public string? EquipmentId { get; set; }

    /// <summary>The virtual tag being edited, when <see cref="IsNew"/> is <c>false</c>.</summary>
    [Parameter] public VirtualTagEditDto? Existing { get; set; }

    /// <summary>The selectable scripts as <c>(Id, Display)</c> pairs.</summary>
    [Parameter] public IReadOnlyList<(string Id, string Display)> Scripts { get; set; } = Array.Empty<(string, string)>();

    /// <summary>Raised after a successful create/save so the host can refresh the equipment's children and close.</summary>
    [Parameter] public EventCallback OnSaved { get; set; }

    /// <summary>Raised when the user cancels so the host can close.</summary>
    [Parameter] public EventCallback OnCancel { get; set; }

    private FormModel _form = new();
    private bool _busy;
    private string? _error;

    // Working copy of the selectable scripts: the host-supplied list plus any script created inline via
    // "New script", so the freshly-created (and now-bound) script is a real option in the dropdown.
    private List<(string Id, string Display)> _scripts = new();

    // Tracks which open this modal last loaded for, so unrelated Blazor Server re-renders don't
    // rebuild _form / reload the script source and clobber in-progress edits. Null while closed.
    private string? _loadedKey;

    // Inline script-source editor state. Independent of _form and the virtual-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 (shown only when no script is bound yet).
    private bool _scriptCreating;
    private string? _scriptCreateError;

    protected override async Task OnParametersSetAsync()
    {
        if (!Visible)
        {
            _loadedKey = null;   // closed → next open reloads fresh
            return;
        }

        // Guard against unrelated re-renders. In Blazor Server any live-status push re-invokes
        // OnParametersSetAsync; without this the rebuild + script reload below would silently
        // discard whatever the operator has typed. Only rebuild when the modal OPENS or the
        // target entity CHANGES.
        var key = IsNew ? "<new>" : Existing?.VirtualTagId;
        if (key == _loadedKey) return;   // same open, re-render → preserve in-progress form + script edits
        _loadedKey = key;

        // Snapshot the host-supplied script options; "New script" appends to this working copy so the
        // newly-created script is selectable without mutating the parent's parameter.
        _scripts = Scripts.Select(s => (s.Id, s.Display)).ToList();

        // Rebuild the working form whenever the host (re)opens the modal for a fresh target.
        if (IsNew)
        {
            _form = new FormModel();
        }
        else if (Existing is not null)
        {
            _form = new FormModel
            {
                VirtualTagId = Existing.VirtualTagId,
                Name = Existing.Name,
                DataType = Existing.DataType,
                ScriptId = Existing.ScriptId,
                ChangeTriggered = Existing.ChangeTriggered,
                TimerIntervalMs = Existing.TimerIntervalMs,
                Historize = Existing.Historize,
                Enabled = Existing.Enabled,
            };
        }
        _error = null;
        _scriptCreateError = null;
        _scriptExpanded = false;

        // Load the bound script's source for the inline panel when editing an existing tag that
        // already has a script (the open-load path; selection changes go through OnScriptChangedAsync).
        await LoadScriptSourceAsync();
    }

    /// <summary>
    /// Reloads the inline-editor state from the currently selected script. Clears it when no script is
    /// bound. This drives both the open-load and the post-selection refresh, and never touches _form.
    /// </summary>
    private async Task LoadScriptSourceAsync()
    {
        _scriptSaved = false;
        _scriptError = null;

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

        var loaded = await Svc.GetScriptSourceAsync(_form.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(_form.ScriptId);
    }

    /// <summary>Refreshes the inline script panel when the operator picks a different script.</summary>
    private Task OnScriptChangedAsync() => LoadScriptSourceAsync();

    /// <summary>
    /// Creates a brand-new (blank) script, binds this virtual tag to it, and expands the inline editor
    /// so the operator can author the body without leaving the modal. The new script name seeds from the
    /// virtual tag's name when set. This does NOT save the virtual tag — the binding lives in _form until
    /// the operator clicks Create/Save below; only the new Script row is persisted here. On failure the
    /// inline error is surfaced and the binding is left untouched.
    /// </summary>
    private async Task CreateNewScriptAsync()
    {
        _scriptCreating = true;
        _scriptCreateError = null;
        try
        {
            // Trim so the local dropdown label matches what CreateScriptAsync persists (it Trim()s
            // the name server-side) — otherwise stray whitespace makes the in-session label drift
            // from every later modal open.
            var seedName = string.IsNullOrWhiteSpace(_form.Name) ? "New script" : $"{_form.Name.Trim()} 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 new script to the options list BEFORE setting the selected value so the
            // <InputSelect> can resolve the label on first render — if we set ScriptId first the
            // dropdown has no matching <option> yet and shows a blank/stale label. This ordering
            // invariant is unit-tested via VirtualTagModalHelpers.ResolveScriptLabel (the AdminUI
            // has no bUnit harness, so the pure helper proxies the render-cycle contract).
            if (!_scripts.Any(s => s.Id == result.CreatedId))
            {
                _scripts.Add((result.CreatedId, $"{seedName} (CSharp)"));
            }
            // Bind the new script and load its (blank) source so the inline editor renders + expands.
            _form.ScriptId = result.CreatedId;
            await LoadScriptSourceAsync();
            _scriptExpanded = true;
        }
        finally
        {
            _scriptCreating = false;
        }
    }

    /// <summary>
    /// Toggles the inline script-source panel. Clears the stale "Script saved." banner when
    /// (re)expanding so a collapse/expand cycle doesn't re-show a confirmation from an earlier save.
    /// </summary>
    private void ToggleScriptPanel()
    {
        _scriptExpanded = !_scriptExpanded;
        if (_scriptExpanded)
        {
            _scriptSaved = false;
        }
    }

    /// <summary>
    /// Saves the edited script body via its own RowVersion-guarded service call. This is wholly
    /// separate from the virtual-tag save: it does not touch _form, does not raise OnSaved, and does
    /// not close the modal. On success it re-fetches the source so the panel holds a fresh token.
    /// </summary>
    private async Task SaveScriptAsync()
    {
        if (string.IsNullOrEmpty(_form.ScriptId) || !_scriptLoaded)
        {
            return;
        }

        _scriptSaving = true;
        _scriptSaved = false;
        _scriptError = null;
        try
        {
            var result = await Svc.UpdateScriptSourceAsync(_form.ScriptId, _scriptSource, _scriptRowVersion);
            if (result.Ok)
            {
                // Re-fetch to refresh the concurrency token for any subsequent save.
                var reloaded = await Svc.GetScriptSourceAsync(_form.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;
        }
    }

    private async Task SaveAsync()
    {
        _busy = true;
        _error = null;
        try
        {
            var input = new VirtualTagInput(
                _form.VirtualTagId,
                _form.Name,
                _form.DataType,
                _form.ScriptId,
                _form.ChangeTriggered,
                _form.TimerIntervalMs,
                _form.Historize,
                _form.Enabled);

            var result = IsNew
                ? await Svc.CreateVirtualTagAsync(EquipmentId!, input)
                : await Svc.UpdateVirtualTagAsync(Existing!.VirtualTagId, input, Existing.RowVersion);

            if (result.Ok)
            {
                await OnSaved.InvokeAsync();
            }
            else
            {
                _error = result.Error;
            }
        }
        finally
        {
            _busy = false;
        }
    }

    private Task CancelAsync() => OnCancel.InvokeAsync();

    private sealed class FormModel
    {
        [Required, RegularExpression("^[A-Za-z0-9_-]+$")] public string VirtualTagId { get; set; } = "";
        [Required] public string Name { get; set; } = "";
        public string DataType { get; set; } = "Double";
        [Required] public string ScriptId { get; set; } = "";
        public bool ChangeTriggered { get; set; } = true;
        public int? TimerIntervalMs { get; set; }
        public bool Historize { get; set; }
        public bool Enabled { get; set; } = true;
    }
}
