@page "/design/templates/{Id:int}"
@using ZB.MOM.WW.ScadaBridge.Security
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
@using ZB.MOM.WW.ScadaBridge.TemplateEngine
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
@inject ITemplateEngineRepository TemplateEngineRepository
@inject ICentralUiRepository CentralUiRepository
@inject TemplateService TemplateService
@inject ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.ScriptAnalysisService AnalysisService
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
@inject IDialogService Dialog

<div class="container-fluid mt-3">
    <ToastNotification @ref="_toast" />

    <div class="mb-3">
        <button class="btn btn-outline-secondary btn-sm"
                aria-label="Back to Templates"
                @onclick="GoBack">← Templates</button>
    </div>

    @if (_loading)
    {
        <LoadingSpinner IsLoading="true" />
    }
    else if (_loadError != null)
    {
        <div class="alert alert-danger">@_loadError</div>
    }
    else if (_selectedTemplate == null)
    {
        <div class="alert alert-warning">Template not found.</div>
    }
    else
    {
        @RenderTemplateDetail()
    }
</div>

@code {
    [Parameter] public int Id { get; set; }

    private List<Template> _templates = new();
    private Template? _selectedTemplate;
    private List<TemplateAttribute> _attributes = new();
    private List<TemplateAlarm> _alarms = new();
    private List<TemplateScript> _scripts = new();
    private List<TemplateComposition> _compositions = new();

    // Populated only when _selectedTemplate.IsDerived — keyed by member name.
    private Dictionary<string, TemplateAttribute> _baseAttributesByName = new(StringComparer.Ordinal);
    private Dictionary<string, TemplateScript> _baseScriptsByName = new(StringComparer.Ordinal);
    private Template? _baseTemplate;
    private Template? _ownerTemplate;
    private TemplateComposition? _ownerComposition;

    private bool _loading = true;
    private string? _loadError;
    private string _activeTab = "attributes";

    // Edit properties
    private string _editName = string.Empty;
    private string? _editDescription;
    private int _editParentId;

    // Validation
    private bool _validating;
    private Commons.Types.Flattening.ValidationResult? _validationResult;

    // Member add/edit forms. _edit*Id null = adding; non-null = editing that row.
    private bool _showAttrForm;
    private int? _editAttrId;
    private string _attrName = string.Empty;
    private string? _attrValue;
    private DataType _attrDataType;
    private bool _attrIsLocked;
    private string? _attrDataSourceRef;
    private string? _attrFormError;

    private bool _showAlarmForm;
    private int? _editAlarmId;
    private string _alarmName = string.Empty;
    private int _alarmPriority;
    private AlarmTriggerType _alarmTriggerType;
    private string? _alarmTriggerConfig;
    private bool _alarmIsLocked;
    private string? _alarmFormError;

    // Native alarm source bindings (read-only mirror of OPC UA A&C / MxGateway alarms)
    private List<TemplateNativeAlarmSource> _nativeSources = new();
    private List<DataConnection> _alarmCapableConnections = new();
    private bool _showNativeSourceForm;
    private int? _editNativeSourceId;
    private string _nasName = string.Empty;
    private string _nasConnection = string.Empty;
    private string _nasSourceRef = string.Empty;
    private string? _nasFilter;
    private string? _nasDescription;
    private bool _nasIsLocked;
    private string? _nasFormError;

    private bool _showScriptForm;
    private int? _editScriptId;
    private string _scriptName = string.Empty;
    private string _scriptCode = string.Empty;
    private string? _scriptTriggerType;
    private string? _scriptTriggerConfig;
    private string? _scriptMinTimeValue;
    private string _scriptMinTimeUnit = "sec";
    private string? _scriptParameters;
    private string? _scriptReturn;
    private bool _scriptIsLocked;
    // Round-tripped from the loaded script so UI edits preserve a timeout set
    // via Transport import (no authoring control in the UI — scoped out).
    private int? _scriptExecutionTimeoutSeconds;
    private string? _scriptFormError;
    private string _scriptModalTab = "trigger"; // "trigger" | "code" | "parameters" | "return"
    private MonacoEditor? _scriptEditor;
    private IReadOnlyList<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.DiagnosticMarker> _scriptMarkers
        = Array.Empty<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.DiagnosticMarker>();
    private IReadOnlyList<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext> _editorChildren
        = Array.Empty<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext>();

    // Script modal Test Run state.
    private bool _showScriptTestRun;
    private bool _scriptRunning;
    private Dictionary<string, object?> _scriptParamValues = new();
    private ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.SandboxRunResult? _scriptRunResult;
    private CancellationTokenSource? _scriptRunCts;
    private List<Instance> _deployedInstances = new();
    private string _scriptBindInstance = string.Empty;

    /// <summary>
    /// Editor's Parent.* context. Empty for base templates (no owner exists);
    /// exactly one entry for derived templates — the slot-owner resolved from
    /// the template's OwnerCompositionId.
    /// </summary>
    private List<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext> _editorParents
        = new();

    private ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext? ActiveEditorParent =>
        _editorParents.FirstOrDefault();

    private ToastNotification _toast = default!;

    protected override async Task OnParametersSetAsync()
    {
        await LoadAsync();
    }

    private async Task LoadAsync()
    {
        _loading = true;
        _loadError = null;
        try
        {
            _templates = (await TemplateEngineRepository.GetAllTemplatesAsync()).ToList();

            _selectedTemplate = await TemplateEngineRepository.GetTemplateWithChildrenAsync(Id)
                                ?? _templates.FirstOrDefault(t => t.Id == Id);
            if (_selectedTemplate == null) { _loading = false; return; }

            _editName = _selectedTemplate.Name;
            _editDescription = _selectedTemplate.Description;
            _editParentId = _selectedTemplate.ParentTemplateId ?? 0;

            _attributes = (await TemplateEngineRepository.GetAttributesByTemplateIdAsync(Id)).ToList();
            _alarms = (await TemplateEngineRepository.GetAlarmsByTemplateIdAsync(Id)).ToList();
            _nativeSources = (await TemplateEngineRepository.GetNativeAlarmSourcesByTemplateIdAsync(Id)).ToList();
            _alarmCapableConnections = (await CentralUiRepository.GetAllDataConnectionsAsync())
                .Where(c => IsAlarmCapable(c.Protocol))
                .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
                .Select(g => g.First())
                .OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
                .ToList();
            _scripts = (await TemplateEngineRepository.GetScriptsByTemplateIdAsync(Id)).ToList();
            _compositions = (await TemplateEngineRepository.GetCompositionsByTemplateIdAsync(Id)).ToList();

            // Derived-template context: base + slot owner power the override
            // banner, inherited badges, and LockedInDerived rendering.
            _baseAttributesByName = new(StringComparer.Ordinal);
            _baseScriptsByName = new(StringComparer.Ordinal);
            _baseTemplate = null;
            _ownerTemplate = null;
            _ownerComposition = null;
            if (_selectedTemplate.IsDerived && _selectedTemplate.ParentTemplateId.HasValue)
            {
                _baseTemplate = await TemplateEngineRepository.GetTemplateByIdAsync(_selectedTemplate.ParentTemplateId.Value);
                if (_baseTemplate != null)
                {
                    foreach (var a in _baseTemplate.Attributes)
                        _baseAttributesByName[a.Name] = a;
                    foreach (var s in _baseTemplate.Scripts)
                        _baseScriptsByName[s.Name] = s;
                }
                if (_selectedTemplate.OwnerCompositionId.HasValue)
                {
                    foreach (var t in _templates)
                    {
                        var c = t.Compositions.FirstOrDefault(x => x.Id == _selectedTemplate.OwnerCompositionId.Value);
                        if (c != null)
                        {
                            _ownerTemplate = t;
                            _ownerComposition = c;
                            break;
                        }
                    }
                }
            }

            // Editor metadata: child compositions + every parent that
            // composes this template. Powers Attributes["X"] /
            // Children["Y"].Attributes["Z"] / Parent.Attributes["W"]
            // completion + SCADA006 / SCADA007 diagnostics in the Monaco
            // editor.
            _editorChildren = await BuildChildContextsAsync(_compositions);
            _editorParents = await BuildParentContextsAsync(Id);

            // Deployed, running instances of this template — selectable as the
            // bind target for a script Test Run.
            _deployedInstances = (await CentralUiRepository.GetInstancesFilteredAsync(templateId: Id))
                .Where(i => i.State == InstanceState.Enabled)
                .OrderBy(i => i.UniqueName)
                .ToList();

            _validationResult = null;
        }
        catch (Exception ex)
        {
            _loadError = $"Failed to load template: {ex.Message}";
        }
        _loading = false;
    }

    private void GoBack()
    {
        NavigationManager.NavigateTo("/design/templates");
    }

    // CentralUI-024: delegates to the shared helper so the claim type stays
    // resolved through JwtTokenService rather than a duplicated magic string.
    private Task<string> GetCurrentUserAsync()
        => AuthStateProvider.GetCurrentUsernameAsync();

    private RenderFragment RenderTemplateDetail() => __builder =>
    {
        @if (_selectedTemplate!.IsDerived && _baseTemplate != null)
        {
            <div class="alert alert-info py-2 mb-3 d-flex justify-content-between align-items-center">
                <div>
                    <i class="bi bi-diagram-3 me-1"></i>
                    <strong>Derived</strong> from
                    <a href="/design/templates/@_baseTemplate.Id"><code>@_baseTemplate.Name</code></a>
                    @if (_ownerTemplate != null && _ownerComposition != null)
                    {
                        <span class="ms-1">— composed inside <a href="/design/templates/@_ownerTemplate.Id"><code>@QualifiedTemplateName(_ownerTemplate)</code></a> as <code>@_ownerComposition.InstanceName</code>.</span>
                    }
                </div>
            </div>
        }
        <div class="d-flex justify-content-between align-items-center mb-3">
            <div>
                <h4 class="d-inline mb-0">@_selectedTemplate.Name</h4>
                @if (_selectedTemplate.ParentTemplateId.HasValue && !_selectedTemplate.IsDerived)
                {
                    <span class="text-muted ms-2">inherits @(_templates.FirstOrDefault(t => t.Id == _selectedTemplate.ParentTemplateId)?.Name)</span>
                }
                @if (_selectedTemplate.IsDerived)
                {
                    @* Derived templates store a contained name; show the full
                       qualified path as a breadcrumb subtitle. *@
                    <div class="text-muted small font-monospace">@QualifiedTemplateName(_selectedTemplate)</div>
                }
            </div>
            <div>
                <button class="btn btn-outline-info btn-sm me-1" @onclick="RunValidation" disabled="@_validating">
                    @if (_validating)
                    {
                        <span class="spinner-border spinner-border-sm me-1"></span>
                    }
                    Validate
                </button>
                <button class="btn btn-outline-danger btn-sm" @onclick="DeleteTemplate">Delete</button>
            </div>
        </div>

        @* Validation results *@
        @if (_validationResult != null)
        {
            <div class="mb-3">
                @if (_validationResult.Errors.Count > 0)
                {
                    <div class="alert alert-danger py-2">
                        <strong>Validation Errors (@_validationResult.Errors.Count)</strong>
                        <ul class="mb-0 small">
                            @foreach (var err in _validationResult.Errors)
                            {
                                <li class="mb-1">
                                    <strong>@err.Category</strong> @err.Message
                                    @if (err.EntityName != null)
                                    {
                                        <span class="text-muted">(@err.EntityName)</span>
                                    }
                                </li>
                            }
                        </ul>
                    </div>
                }
                @if (_validationResult.Warnings.Count > 0)
                {
                    <div class="alert alert-warning py-2">
                        <strong>Warnings (@_validationResult.Warnings.Count)</strong>
                        <ul class="mb-0 small">
                            @foreach (var warn in _validationResult.Warnings)
                            {
                                <li class="mb-1">
                                    <strong>@warn.Category</strong> <span class="text-muted">@warn.Message</span>
                                </li>
                            }
                        </ul>
                    </div>
                }
                @if (_validationResult.Errors.Count == 0 && _validationResult.Warnings.Count == 0)
                {
                    <div class="alert alert-success py-2">Validation passed with no errors or warnings.</div>
                }
            </div>
        }

        @* Template info edit *@
        <div class="card mb-3">
            <div class="card-header">Template Properties</div>
            <div class="card-body">
                <div class="row g-3">
                    <div class="col-12">
                        <label class="form-label">Name</label>
                        <input type="text" class="form-control" @bind="_editName" />
                    </div>
                    <div class="col-12">
                        <label class="form-label">Description</label>
                        <input type="text" class="form-control" @bind="_editDescription" />
                    </div>
                    @* Parent template is shown in the header above (the "inherits"
                       / "Derived from" line), so it is not repeated here. *@
                    <div class="col-12 text-end">
                        <button class="btn btn-primary" @onclick="UpdateTemplateProperties">Save Properties</button>
                    </div>
                </div>
            </div>
        </div>

        @* Tabs: Attributes, Alarms, Scripts, Compositions *@
        <ul class="nav nav-tabs mb-3" role="tablist">
            <li class="nav-item" role="presentation">
                <button class="nav-link @(_activeTab == "attributes" ? "active" : "")"
                        role="tab"
                        aria-selected="@(_activeTab == "attributes" ? "true" : "false")"
                        aria-controls="tmpl-tab-attributes"
                        @onclick='() => _activeTab = "attributes"'>
                    Attributes <span class="badge bg-secondary">@_attributes.Count</span>
                </button>
            </li>
            <li class="nav-item" role="presentation">
                <button class="nav-link @(_activeTab == "alarms" ? "active" : "")"
                        role="tab"
                        aria-selected="@(_activeTab == "alarms" ? "true" : "false")"
                        aria-controls="tmpl-tab-alarms"
                        @onclick='() => _activeTab = "alarms"'>
                    Alarms <span class="badge bg-secondary">@_alarms.Count</span>
                </button>
            </li>
            <li class="nav-item" role="presentation">
                <button class="nav-link @(_activeTab == "native-alarms" ? "active" : "")"
                        role="tab"
                        aria-selected="@(_activeTab == "native-alarms" ? "true" : "false")"
                        aria-controls="tmpl-tab-native-alarms"
                        @onclick='() => _activeTab = "native-alarms"'>
                    Native Alarms <span class="badge bg-secondary">@_nativeSources.Count</span>
                </button>
            </li>
            <li class="nav-item" role="presentation">
                <button class="nav-link @(_activeTab == "scripts" ? "active" : "")"
                        role="tab"
                        aria-selected="@(_activeTab == "scripts" ? "true" : "false")"
                        aria-controls="tmpl-tab-scripts"
                        @onclick='() => _activeTab = "scripts"'>
                    Scripts <span class="badge bg-secondary">@_scripts.Count</span>
                </button>
            </li>
        </ul>

        @if (_activeTab == "attributes")
        {
            <div role="tabpanel" id="tmpl-tab-attributes">@RenderAttributesTab()</div>
        }
        else if (_activeTab == "native-alarms")
        {
            <div role="tabpanel" id="tmpl-tab-native-alarms">@RenderNativeAlarmsTab()</div>
        }
        else if (_activeTab == "alarms")
        {
            <div role="tabpanel" id="tmpl-tab-alarms">@RenderAlarmsTab()</div>
        }
        else if (_activeTab == "scripts")
        {
            <div role="tabpanel" id="tmpl-tab-scripts">@RenderScriptsTab()</div>
        }
    };

    private async Task DeleteTemplate()
    {
        if (_selectedTemplate == null) return;
        var confirmed = await Dialog.ConfirmAsync(
            "Delete Template",
            $"Delete template '{_selectedTemplate.Name}'? This will fail if instances or child templates reference it.",
            danger: true);
        if (!confirmed) return;

        try
        {
            var user = await GetCurrentUserAsync();
            var result = await TemplateService.DeleteTemplateAsync(_selectedTemplate.Id, user);
            if (result.IsSuccess)
            {
                _toast.ShowSuccess($"Template '{_selectedTemplate.Name}' deleted.");
                NavigationManager.NavigateTo("/design/templates");
            }
            else
            {
                _toast.ShowError(result.Error);
            }
        }
        catch (Exception ex)
        {
            _toast.ShowError($"Delete failed: {ex.Message}");
        }
    }

    private async Task UpdateTemplateProperties()
    {
        if (_selectedTemplate == null) return;
        try
        {
            var user = await GetCurrentUserAsync();
            var result = await TemplateService.UpdateTemplateAsync(
                _selectedTemplate.Id, _editName.Trim(), _editDescription?.Trim(),
                _editParentId == 0 ? null : _editParentId, user);

            if (result.IsSuccess)
            {
                _toast.ShowSuccess("Template properties updated.");
                _selectedTemplate = result.Value;
            }
            else
            {
                _toast.ShowError(result.Error);
            }
        }
        catch (Exception ex)
        {
            _toast.ShowError($"Update failed: {ex.Message}");
        }
    }

    private async Task RunValidation()
    {
        if (_selectedTemplate == null) return;
        _validating = true;
        _validationResult = null;
        try
        {
            var validationService = new ValidationService();
            var flatConfig = new Commons.Types.Flattening.FlattenedConfiguration
            {
                InstanceUniqueName = $"validation-{_selectedTemplate.Name}",
                TemplateId = _selectedTemplate.Id,
                Attributes = _attributes.Select(a => new Commons.Types.Flattening.ResolvedAttribute
                {
                    CanonicalName = a.Name,
                    Value = a.Value,
                    DataType = a.DataType.ToString(),
                    IsLocked = a.IsLocked,
                    DataSourceReference = a.DataSourceReference
                }).ToList(),
                Alarms = _alarms.Select(a => new Commons.Types.Flattening.ResolvedAlarm
                {
                    CanonicalName = a.Name,
                    PriorityLevel = a.PriorityLevel,
                    IsLocked = a.IsLocked,
                    TriggerType = a.TriggerType.ToString(),
                    TriggerConfiguration = a.TriggerConfiguration
                }).ToList(),
                Scripts = _scripts.Select(s => new Commons.Types.Flattening.ResolvedScript
                {
                    CanonicalName = s.Name,
                    Code = s.Code,
                    IsLocked = s.IsLocked,
                    TriggerType = s.TriggerType,
                    TriggerConfiguration = s.TriggerConfiguration,
                    ParameterDefinitions = s.ParameterDefinitions,
                    ReturnDefinition = s.ReturnDefinition
                }).ToList()
            };
            _validationResult = validationService.Validate(flatConfig);

            var collisions = await TemplateService.DetectCollisionsAsync(_selectedTemplate.Id);
            if (collisions.Count > 0)
            {
                var collisionErrors = collisions.Select(c =>
                    Commons.Types.Flattening.ValidationEntry.Error(
                        Commons.Types.Flattening.ValidationCategory.NamingCollision, c)).ToArray();
                var collisionResult = new Commons.Types.Flattening.ValidationResult { Errors = collisionErrors };
                _validationResult = Commons.Types.Flattening.ValidationResult.Merge(_validationResult, collisionResult);
            }
        }
        catch (Exception ex)
        {
            _toast.ShowError($"Validation error: {ex.Message}");
        }
        _validating = false;
    }

    // ---- Attributes Tab ----
    private RenderFragment RenderAttributesTab() => __builder =>
    {
        <div class="d-flex justify-content-between align-items-center mb-2">
            <h5 class="mb-0">Attributes</h5>
            <button class="btn btn-primary btn-sm" @onclick="BeginAddAttribute">Add Attribute</button>
        </div>

        @if (_showAttrForm)
        {
            var editing = _editAttrId.HasValue;
            <div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.4);">
                <div class="modal-dialog modal-dialog-scrollable">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h6 class="modal-title">@(editing ? "Edit Attribute" : "Add Attribute")</h6>
                            <button type="button" class="btn-close" aria-label="Close" @onclick="CancelAttributeForm"></button>
                        </div>
                        <div class="modal-body">
                            <div class="row g-3">
                                <div class="col-12">
                                    <label class="form-label">Name</label>
                                    <input type="text" class="form-control" @bind="_attrName" readonly="@editing" />
                                </div>
                                <div class="col-12">
                                    <label class="form-label">Data Type</label>
                                    <select class="form-select" @bind="_attrDataType">
                                        @foreach (var dt in Enum.GetValues<DataType>())
                                        {
                                            <option value="@dt">@dt</option>
                                        }
                                    </select>
                                </div>
                                <div class="col-12">
                                    <label class="form-label">Value</label>
                                    <input type="text" class="form-control" @bind="_attrValue" />
                                </div>
                                <div class="col-12">
                                    <label class="form-label">Data Source Ref</label>
                                    <input type="text" class="form-control" @bind="_attrDataSourceRef" placeholder="Tag path" />
                                </div>
                                <div class="col-12">
                                    <div class="form-check">
                                        <input class="form-check-input" type="checkbox" @bind="_attrIsLocked" id="attrLocked" />
                                        <label class="form-check-label" for="attrLocked">Locked</label>
                                    </div>
                                </div>
                                @if (_attrFormError != null)
                                {
                                    <div class="col-12"><div class="text-danger small">@_attrFormError</div></div>
                                }
                            </div>
                        </div>
                        <div class="modal-footer">
                            <button class="btn btn-outline-secondary btn-sm" @onclick="CancelAttributeForm">Cancel</button>
                            <button class="btn btn-success btn-sm" @onclick="SaveAttribute">@(editing ? "Save" : "Add")</button>
                        </div>
                    </div>
                </div>
            </div>
        }

        var derived = _selectedTemplate!.IsDerived;
        <table class="table table-sm table-striped">
            <thead class="table-light">
                <tr>
                    <th>Name</th>
                    <th>Type</th>
                    <th>Value</th>
                    <th>Data Source</th>
                    <th>Lock</th>
                    @if (derived)
                    {
                        <th>Source</th>
                    }
                    else
                    {
                        <th title="When true, derived templates may not override this row.">Lock in derived</th>
                    }
                    <th style="width: 60px;">Actions</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var attr in _attributes)
                {
                    _baseAttributesByName.TryGetValue(attr.Name, out var baseAttr);
                    var lockedByBase = derived && baseAttr != null && baseAttr.LockedInDerived;
                    var effectiveValue = (derived && attr.IsInherited && baseAttr != null) ? baseAttr.Value : attr.Value;
                    var effectiveDataSource = (derived && attr.IsInherited && baseAttr != null) ? baseAttr.DataSourceReference : attr.DataSourceReference;
                    <tr>
                        <td>@attr.Name</td>
                        <td><span class="badge bg-light text-dark">@attr.DataType</span></td>
                        <td class="small">@(effectiveValue ?? "—")</td>
                        <td class="small text-muted">@(effectiveDataSource ?? "—")</td>
                        <td>
                            @if (attr.IsLocked)
                            {
                                <span class="badge bg-danger" aria-label="Locked">Locked</span>
                            }
                            else
                            {
                                <span class="badge bg-light text-dark" aria-label="Unlocked">Unlocked</span>
                            }
                        </td>
                        @if (derived)
                        {
                            <td>
                                @if (lockedByBase)
                                {
                                    <span class="badge bg-warning text-dark" title="Locked by base template — cannot override.">🔒 Base-locked</span>
                                }
                                else if (attr.IsInherited)
                                {
                                    <span class="badge bg-secondary">Inherited</span>
                                }
                                else if (baseAttr != null)
                                {
                                    <span class="badge bg-primary">Override</span>
                                }
                                else
                                {
                                    <span class="badge bg-light text-dark">Local</span>
                                }
                            </td>
                        }
                        else
                        {
                            <td>
                                <div class="form-check form-switch">
                                    <input class="form-check-input" type="checkbox" role="switch"
                                           checked="@attr.LockedInDerived"
                                           @onchange="(e) => ToggleAttrLockedInDerived(attr, e)" />
                                </div>
                            </td>
                        }
                        <td>
                            <div class="dropdown">
                                <button class="btn btn-outline-secondary btn-sm py-0 px-1"
                                        data-bs-toggle="dropdown"
                                        aria-expanded="false"
                                        aria-label="@($"More actions for {attr.Name}")">⋮</button>
                                <ul class="dropdown-menu dropdown-menu-end">
                                    @if (derived && baseAttr != null && !lockedByBase)
                                    {
                                        @if (attr.IsInherited)
                                        {
                                            <li>
                                                <button class="dropdown-item" @onclick="() => OverrideAttribute(attr)">Override…</button>
                                            </li>
                                        }
                                        else
                                        {
                                            <li>
                                                <button class="dropdown-item" @onclick="() => RevertAttributeToBase(attr)">Revert to base</button>
                                            </li>
                                        }
                                    }
                                    @if (!(derived && baseAttr != null))
                                    {
                                        <li>
                                            <button class="dropdown-item" @onclick="() => BeginEditAttribute(attr)">Edit…</button>
                                        </li>
                                        <li><hr class="dropdown-divider" /></li>
                                        <li>
                                            <button class="dropdown-item text-danger"
                                                    @onclick="() => DeleteAttribute(attr)">Delete</button>
                                        </li>
                                    }
                                </ul>
                            </div>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    };

    private async Task ToggleAttrLockedInDerived(TemplateAttribute attr, ChangeEventArgs e)
    {
        var newValue = e.Value is bool b && b;
        if (attr.LockedInDerived == newValue) return;
        var user = await GetCurrentUserAsync();
        var proposed = new TemplateAttribute(attr.Name)
        {
            Value = attr.Value,
            DataType = attr.DataType,
            IsLocked = attr.IsLocked,
            Description = attr.Description,
            DataSourceReference = attr.DataSourceReference,
            IsInherited = attr.IsInherited,
            LockedInDerived = newValue,
        };
        var result = await TemplateService.UpdateAttributeAsync(attr.Id, proposed, user);
        if (result.IsSuccess) { attr.LockedInDerived = newValue; _toast.ShowSuccess("Lock-in-derived updated."); }
        else _toast.ShowError(result.Error);
    }

    private async Task OverrideAttribute(TemplateAttribute attr)
    {
        var newVal = await Dialog.PromptAsync(
            $"Override '{attr.Name}'",
            "New value (overrides the base):",
            placeholder: attr.Value ?? "");
        if (newVal == null) return;
        var user = await GetCurrentUserAsync();
        var proposed = new TemplateAttribute(attr.Name)
        {
            Value = newVal,
            DataType = attr.DataType,
            IsLocked = attr.IsLocked,
            Description = attr.Description,
            DataSourceReference = attr.DataSourceReference,
            IsInherited = false,
            LockedInDerived = false,
        };
        var result = await TemplateService.UpdateAttributeAsync(attr.Id, proposed, user);
        if (result.IsSuccess) { _toast.ShowSuccess($"Override saved for '{attr.Name}'."); await LoadAsync(); }
        else _toast.ShowError(result.Error);
    }

    private async Task RevertAttributeToBase(TemplateAttribute attr)
    {
        var confirmed = await Dialog.ConfirmAsync(
            "Revert to base",
            $"Discard the override for '{attr.Name}' and follow the base value again?");
        if (!confirmed) return;
        var user = await GetCurrentUserAsync();
        _baseAttributesByName.TryGetValue(attr.Name, out var baseAttr);
        var proposed = new TemplateAttribute(attr.Name)
        {
            Value = baseAttr?.Value,
            DataType = attr.DataType,
            IsLocked = attr.IsLocked,
            Description = baseAttr?.Description,
            DataSourceReference = baseAttr?.DataSourceReference,
            IsInherited = true,
            LockedInDerived = false,
        };
        var result = await TemplateService.UpdateAttributeAsync(attr.Id, proposed, user);
        if (result.IsSuccess) { _toast.ShowSuccess($"'{attr.Name}' reverted to base."); await LoadAsync(); }
        else _toast.ShowError(result.Error);
    }

    // ---- Alarms Tab ----
    private RenderFragment RenderAlarmsTab() => __builder =>
    {
        <div class="d-flex justify-content-between align-items-center mb-2">
            <h5 class="mb-0">Alarms</h5>
            <button class="btn btn-primary btn-sm" @onclick="BeginAddAlarm">Add Alarm</button>
        </div>

        @if (_showAlarmForm)
        {
            var editing = _editAlarmId.HasValue;
            <div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.4);">
                <div class="modal-dialog modal-dialog-scrollable">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h6 class="modal-title">@(editing ? "Edit Alarm" : "Add Alarm")</h6>
                            <button type="button" class="btn-close" aria-label="Close" @onclick="CancelAlarmForm"></button>
                        </div>
                        <div class="modal-body">
                            <div class="row g-3">
                                <div class="col-12">
                                    <label class="form-label">Name</label>
                                    <input type="text" class="form-control" @bind="_alarmName" readonly="@editing" />
                                </div>
                                <div class="col-12">
                                    <label class="form-label">Trigger Type</label>
                                    <select class="form-select" @bind="_alarmTriggerType" disabled="@editing">
                                        @foreach (var tt in Enum.GetValues<AlarmTriggerType>())
                                        {
                                            <option value="@tt">@tt</option>
                                        }
                                    </select>
                                </div>
                                <div class="col-12">
                                    <label class="form-label">Priority</label>
                                    <input type="number" class="form-control" @bind="_alarmPriority" min="0" max="1000" />
                                </div>
                                <div class="col-12">
                                    <label class="form-label">Trigger Configuration</label>
                                    <AlarmTriggerEditor TriggerType="@_alarmTriggerType"
                                                        Value="@_alarmTriggerConfig"
                                                        ValueChanged="@(v => _alarmTriggerConfig = v)"
                                                        AvailableAttributes="@BuildAlarmAttributeChoices()"
                                                        FallbackPriority="@_alarmPriority" />
                                </div>
                                <div class="col-12">
                                    <div class="form-check">
                                        <input class="form-check-input" type="checkbox" @bind="_alarmIsLocked" id="alarmLocked" />
                                        <label class="form-check-label" for="alarmLocked">Locked</label>
                                    </div>
                                </div>
                                @if (_alarmFormError != null)
                                {
                                    <div class="col-12"><div class="text-danger small">@_alarmFormError</div></div>
                                }
                            </div>
                        </div>
                        <div class="modal-footer">
                            <button class="btn btn-outline-secondary btn-sm" @onclick="CancelAlarmForm">Cancel</button>
                            <button class="btn btn-success btn-sm" @onclick="SaveAlarm">@(editing ? "Save" : "Add")</button>
                        </div>
                    </div>
                </div>
            </div>
        }

        <table class="table table-sm table-striped">
            <thead class="table-light">
                <tr>
                    <th>Name</th>
                    <th>Trigger</th>
                    <th>Priority</th>
                    <th>Config</th>
                    <th>Lock</th>
                    <th style="width: 60px;">Actions</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var alarm in _alarms)
                {
                    <tr>
                        <td>@alarm.Name</td>
                        <td><span class="badge bg-light text-dark">@alarm.TriggerType</span></td>
                        <td>@alarm.PriorityLevel</td>
                        <td class="small text-muted text-truncate" style="max-width: 200px;">@(alarm.TriggerConfiguration ?? "—")</td>
                        <td>
                            @if (alarm.IsLocked)
                            {
                                <span class="badge bg-danger" aria-label="Locked">Locked</span>
                            }
                            else
                            {
                                <span class="badge bg-light text-dark" aria-label="Unlocked">Unlocked</span>
                            }
                        </td>
                        <td>
                            <div class="dropdown">
                                <button class="btn btn-outline-secondary btn-sm py-0 px-1"
                                        data-bs-toggle="dropdown"
                                        aria-expanded="false"
                                        aria-label="@($"More actions for {alarm.Name}")">⋮</button>
                                <ul class="dropdown-menu dropdown-menu-end">
                                    <li>
                                        <button class="dropdown-item" @onclick="() => BeginEditAlarm(alarm)">Edit…</button>
                                    </li>
                                    <li><hr class="dropdown-divider" /></li>
                                    <li>
                                        <button class="dropdown-item text-danger"
                                                @onclick="() => DeleteAlarm(alarm)">Delete</button>
                                    </li>
                                </ul>
                            </div>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    };

    // ---- Native Alarms Tab ----
    private RenderFragment RenderNativeAlarmsTab() => __builder =>
    {
        <div class="d-flex justify-content-between align-items-center mb-2">
            <h5 class="mb-0">Native Alarm Sources</h5>
            <button class="btn btn-primary btn-sm" @onclick="BeginAddNativeSource">Add Source</button>
        </div>
        <p class="text-muted small">
            Read-only mirror of alarms from an OPC UA Alarms &amp; Conditions server or the
            MxAccess Gateway. Discovered at runtime and shown live in the Debug View — no ack-back.
        </p>

        @if (_showNativeSourceForm)
        {
            var editing = _editNativeSourceId.HasValue;
            <div class="modal d-block" tabindex="-1" style="background: rgba(0,0,0,.5);">
                <div class="modal-dialog">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h6 class="modal-title">@(editing ? "Edit Native Alarm Source" : "Add Native Alarm Source")</h6>
                            <button type="button" class="btn-close" aria-label="Close" @onclick="CancelNativeSourceForm"></button>
                        </div>
                        <div class="modal-body">
                            <div class="row g-2">
                                <div class="col-12">
                                    <label class="form-label">Name</label>
                                    <input type="text" class="form-control" @bind="_nasName" readonly="@editing" />
                                </div>
                                <div class="col-12">
                                    <label class="form-label">Connection</label>
                                    <select class="form-select" @bind="_nasConnection">
                                        <option value="">— select an alarm-capable connection —</option>
                                        @foreach (var c in _alarmCapableConnections)
                                        {
                                            <option value="@c.Name">@c.Name (@c.Protocol)</option>
                                        }
                                    </select>
                                    @if (_alarmCapableConnections.Count == 0)
                                    {
                                        <div class="form-text text-warning">No OPC UA or MxGateway connections defined yet.</div>
                                    }
                                </div>
                                <div class="col-12">
                                    <label class="form-label">Source Reference</label>
                                    <input type="text" class="form-control font-monospace" @bind="_nasSourceRef"
                                           placeholder="OPC UA SourceNode nodeId, or MxAccess object/area" />
                                </div>
                                <div class="col-12">
                                    <label class="form-label">Condition Filter <span class="text-muted">(optional)</span></label>
                                    <input type="text" class="form-control" @bind="_nasFilter"
                                           placeholder="Blank = mirror all conditions under the source" />
                                </div>
                                <div class="col-12">
                                    <label class="form-label">Description <span class="text-muted">(optional)</span></label>
                                    <input type="text" class="form-control" @bind="_nasDescription" />
                                </div>
                                <div class="col-12">
                                    <div class="form-check">
                                        <input class="form-check-input" type="checkbox" @bind="_nasIsLocked" id="nasLocked" />
                                        <label class="form-check-label" for="nasLocked">Locked</label>
                                    </div>
                                </div>
                                @if (_nasFormError != null)
                                {
                                    <div class="col-12"><div class="text-danger small">@_nasFormError</div></div>
                                }
                            </div>
                        </div>
                        <div class="modal-footer">
                            <button class="btn btn-outline-secondary btn-sm" @onclick="CancelNativeSourceForm">Cancel</button>
                            <button class="btn btn-success btn-sm" @onclick="SaveNativeSource">@(editing ? "Save" : "Add")</button>
                        </div>
                    </div>
                </div>
            </div>
        }

        @if (_nativeSources.Count == 0)
        {
            <p class="text-muted">No native alarm sources defined.</p>
        }
        else
        {
            <table class="table table-sm table-striped">
                <thead class="table-light">
                    <tr>
                        <th>Name</th>
                        <th>Connection</th>
                        <th>Source Reference</th>
                        <th>Filter</th>
                        <th>Lock</th>
                        <th style="width: 60px;">Actions</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var src in _nativeSources)
                    {
                        <tr>
                            <td>@src.Name</td>
                            <td><span class="badge bg-light text-dark">@src.ConnectionName</span></td>
                            <td class="small font-monospace text-truncate" style="max-width: 220px;" title="@src.SourceReference">@src.SourceReference</td>
                            <td class="small text-muted">@(string.IsNullOrEmpty(src.ConditionFilter) ? "—" : src.ConditionFilter)</td>
                            <td>
                                @if (src.IsLocked)
                                {
                                    <span class="badge bg-danger" aria-label="Locked">Locked</span>
                                }
                                else
                                {
                                    <span class="badge bg-light text-dark" aria-label="Unlocked">Unlocked</span>
                                }
                            </td>
                            <td>
                                <div class="dropdown">
                                    <button class="btn btn-outline-secondary btn-sm py-0 px-1"
                                            data-bs-toggle="dropdown" aria-expanded="false"
                                            aria-label="@($"More actions for {src.Name}")">⋮</button>
                                    <ul class="dropdown-menu dropdown-menu-end">
                                        <li><button class="dropdown-item" @onclick="() => BeginEditNativeSource(src)">Edit…</button></li>
                                        <li><hr class="dropdown-divider" /></li>
                                        <li><button class="dropdown-item text-danger" @onclick="() => DeleteNativeSource(src)">Delete</button></li>
                                    </ul>
                                </div>
                            </td>
                        </tr>
                    }
                </tbody>
            </table>
        }
    };

    private static bool IsAlarmCapable(string? protocol) =>
        string.Equals(protocol, "OpcUa", StringComparison.OrdinalIgnoreCase)
        || string.Equals(protocol, "MxGateway", StringComparison.OrdinalIgnoreCase);

    private void BeginAddNativeSource()
    {
        _showNativeSourceForm = true;
        _editNativeSourceId = null;
        _nasFormError = null;
        _nasName = string.Empty;
        _nasConnection = string.Empty;
        _nasSourceRef = string.Empty;
        _nasFilter = null;
        _nasDescription = null;
        _nasIsLocked = false;
    }

    private void BeginEditNativeSource(TemplateNativeAlarmSource src)
    {
        _showNativeSourceForm = true;
        _editNativeSourceId = src.Id;
        _nasFormError = null;
        _nasName = src.Name;
        _nasConnection = src.ConnectionName;
        _nasSourceRef = src.SourceReference;
        _nasFilter = src.ConditionFilter;
        _nasDescription = src.Description;
        _nasIsLocked = src.IsLocked;
    }

    private void CancelNativeSourceForm()
    {
        _showNativeSourceForm = false;
        _editNativeSourceId = null;
        _nasFormError = null;
    }

    private async Task SaveNativeSource()
    {
        if (_selectedTemplate == null) return;
        _nasFormError = null;
        if (string.IsNullOrWhiteSpace(_nasName)) { _nasFormError = "Name is required."; return; }
        if (string.IsNullOrWhiteSpace(_nasConnection)) { _nasFormError = "Connection is required."; return; }
        if (string.IsNullOrWhiteSpace(_nasSourceRef)) { _nasFormError = "Source reference is required."; return; }

        if (_editNativeSourceId is int id)
        {
            var existing = await TemplateEngineRepository.GetTemplateNativeAlarmSourceByIdAsync(id);
            if (existing == null) { _nasFormError = "Source no longer exists."; return; }
            existing.ConnectionName = _nasConnection.Trim();
            existing.SourceReference = _nasSourceRef.Trim();
            existing.ConditionFilter = string.IsNullOrWhiteSpace(_nasFilter) ? null : _nasFilter.Trim();
            existing.Description = string.IsNullOrWhiteSpace(_nasDescription) ? null : _nasDescription.Trim();
            existing.IsLocked = _nasIsLocked;
            await TemplateEngineRepository.UpdateTemplateNativeAlarmSourceAsync(existing);
            await TemplateEngineRepository.SaveChangesAsync();
            _showNativeSourceForm = false;
            _editNativeSourceId = null;
            _toast.ShowSuccess($"Native alarm source '{existing.Name}' updated.");
            await LoadAsync();
            return;
        }

        if (_nativeSources.Any(s => string.Equals(s.Name, _nasName.Trim(), StringComparison.OrdinalIgnoreCase)))
        {
            _nasFormError = $"A source named '{_nasName.Trim()}' already exists on this template.";
            return;
        }

        var source = new TemplateNativeAlarmSource(_nasName.Trim())
        {
            TemplateId = _selectedTemplate.Id,
            ConnectionName = _nasConnection.Trim(),
            SourceReference = _nasSourceRef.Trim(),
            ConditionFilter = string.IsNullOrWhiteSpace(_nasFilter) ? null : _nasFilter.Trim(),
            Description = string.IsNullOrWhiteSpace(_nasDescription) ? null : _nasDescription.Trim(),
            IsLocked = _nasIsLocked
        };
        await TemplateEngineRepository.AddTemplateNativeAlarmSourceAsync(source);
        await TemplateEngineRepository.SaveChangesAsync();
        _showNativeSourceForm = false;
        _toast.ShowSuccess($"Native alarm source '{source.Name}' added.");
        await LoadAsync();
    }

    private async Task DeleteNativeSource(TemplateNativeAlarmSource src)
    {
        var confirmed = await Dialog.ConfirmAsync("Delete Native Alarm Source", $"Delete native alarm source '{src.Name}'?", danger: true);
        if (!confirmed) return;
        await TemplateEngineRepository.DeleteTemplateNativeAlarmSourceAsync(src.Id);
        await TemplateEngineRepository.SaveChangesAsync();
        _toast.ShowSuccess($"Native alarm source '{src.Name}' deleted.");
        await LoadAsync();
    }

    // ---- Scripts Tab ----
    private RenderFragment RenderScriptsTab() => __builder =>
    {
        <div class="d-flex justify-content-between align-items-center mb-2">
            <h5 class="mb-0">Scripts</h5>
            <button class="btn btn-primary btn-sm" @onclick="BeginAddScript">Add Script</button>
        </div>

        @if (_showScriptForm)
        {
            var editingScript = _editScriptId.HasValue;
            <div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.4);">
                <div class="modal-dialog modal-dialog-scrollable script-editor-modal">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h6 class="modal-title">@(editingScript ? "Edit Script" : "Add Script")</h6>
                            <button type="button" class="btn-close" aria-label="Close" @onclick="CancelScriptForm"></button>
                        </div>
                        <div class="modal-body">
                            <div class="row g-3 mb-3">
                                <div class="col-12">
                                    <label class="form-label">Name</label>
                                    <input type="text" class="form-control" @bind="_scriptName" readonly="@editingScript" />
                                </div>
                                <div class="col-12">
                                    <div class="form-check">
                                        <input class="form-check-input" type="checkbox" @bind="_scriptIsLocked" id="scriptLocked" />
                                        <label class="form-check-label" for="scriptLocked">Locked</label>
                                    </div>
                                </div>
                            </div>

                            @* Tabs: Trigger, Code, Parameters, Return. All panels stay
                               mounted (toggled via display:none) so Monaco editors and the
                               JSONJoy React island don't tear down on tab switch. *@
                            <ul class="nav nav-tabs" role="tablist">
                                <li class="nav-item" role="presentation">
                                    <button type="button"
                                            class="nav-link @(_scriptModalTab == "trigger" ? "active" : "")"
                                            role="tab"
                                            aria-selected="@(_scriptModalTab == "trigger" ? "true" : "false")"
                                            @onclick='() => _scriptModalTab = "trigger"'>Trigger</button>
                                </li>
                                <li class="nav-item" role="presentation">
                                    <button type="button"
                                            class="nav-link @(_scriptModalTab == "code" ? "active" : "")"
                                            role="tab"
                                            aria-selected="@(_scriptModalTab == "code" ? "true" : "false")"
                                            @onclick='() => _scriptModalTab = "code"'>Code</button>
                                </li>
                                <li class="nav-item" role="presentation">
                                    <button type="button"
                                            class="nav-link @(_scriptModalTab == "parameters" ? "active" : "")"
                                            role="tab"
                                            aria-selected="@(_scriptModalTab == "parameters" ? "true" : "false")"
                                            @onclick='() => _scriptModalTab = "parameters"'>Parameters</button>
                                </li>
                                <li class="nav-item" role="presentation">
                                    <button type="button"
                                            class="nav-link @(_scriptModalTab == "return" ? "active" : "")"
                                            role="tab"
                                            aria-selected="@(_scriptModalTab == "return" ? "true" : "false")"
                                            @onclick='() => _scriptModalTab = "return"'>Return type</button>
                                </li>
                            </ul>
                            <div class="border border-top-0 rounded-bottom p-3">
                                <div style="display: @(_scriptModalTab == "trigger" ? "block" : "none")">
                                    <ScriptTriggerEditor TriggerType="@_scriptTriggerType"
                                                         TriggerConfig="@_scriptTriggerConfig"
                                                         Changed="@OnScriptTriggerChanged"
                                                         AvailableAttributes="@BuildAlarmAttributeChoices()" />
                                    @if (ScriptTriggerConfigCodec.SupportsMinTimeBetweenRuns(_scriptTriggerType))
                                    {
                                        <div class="mt-3">
                                            <label class="form-label">Min time between runs</label>
                                            <div class="row g-2" style="max-width: 420px;">
                                                <div class="col-7">
                                                    <input type="number" min="1" step="1" class="form-control"
                                                           placeholder="(optional)"
                                                           @bind="_scriptMinTimeValue" @bind:event="oninput" />
                                                </div>
                                                <div class="col-5">
                                                    <select class="form-select" @bind="_scriptMinTimeUnit">
                                                        <option value="ms">milliseconds</option>
                                                        <option value="sec">seconds</option>
                                                        <option value="min">minutes</option>
                                                    </select>
                                                </div>
                                            </div>
                                            @if (ScriptTriggerIsWhileTrue())
                                            {
                                                <div class="form-text">
                                                    This is the re-fire interval for the
                                                    <strong>WhileTrue</strong> trigger above.
                                                </div>
                                                @if (DurationInput.Compose(_scriptMinTimeValue, _scriptMinTimeUnit) is null)
                                                {
                                                    <div class="alert alert-warning py-1 px-2 small mt-1 mb-0">
                                                        The WhileTrue trigger has no interval set — the script
                                                        will fire only once. Set a value here to make it re-fire.
                                                    </div>
                                                }
                                            }
                                            else
                                            {
                                                <div class="form-text">
                                                    Optional throttle — skips trigger invocations that fire
                                                    sooner than this.
                                                </div>
                                            }
                                        </div>
                                    }
                                </div>
                                <div style="display: @(_scriptModalTab == "code" ? "block" : "none")">
                                    <MonacoEditor @ref="_scriptEditor" Value="@_scriptCode" ValueChanged="@(v => _scriptCode = v)"
                                                  Language="csharp" Height="360px"
                                                  DeclaredParameters="@ScriptParameterNames.Parse(_scriptParameters)"
                                                  DeclaredParameterShapes="@ScriptParameterNames.ParseShapes(_scriptParameters)"
                                                  SiblingScripts="@(_scripts.Select(s => ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.ScriptShapeParser.Parse(s.Name, s.ParameterDefinitions, s.ReturnDefinition)).ToArray())"
                                                  SelfAttributes="@(_attributes.Select(a => new ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.AttributeShape(a.Name, MapDataType(a.DataType))).ToArray())"
                                                  Children="@_editorChildren"
                                                  Parent="@ActiveEditorParent"
                                                  MarkersChanged="@(m => { _scriptMarkers = m; StateHasChanged(); })" />
                                    <ProblemsPanel Markers="@_scriptMarkers" OnNavigate="@(m => _scriptEditor?.RevealLineAsync(m.StartLineNumber, m.StartColumn) ?? Task.CompletedTask)" />
                                </div>
                                <div style="display: @(_scriptModalTab == "parameters" ? "block" : "none")">
                                    <SchemaBuilder Mode="object"
                                                   Value="@_scriptParameters"
                                                   ValueChanged="@(v => _scriptParameters = v)" />
                                </div>
                                <div style="display: @(_scriptModalTab == "return" ? "block" : "none")">
                                    <SchemaBuilder Mode="value"
                                                   Value="@_scriptReturn"
                                                   ValueChanged="@(v => _scriptReturn = v)" />
                                </div>
                            </div>

                            @if (_scriptFormError != null)
                            {
                                <div class="text-danger small mt-2">@_scriptFormError</div>
                            }

                            @if (_showScriptTestRun)
                            {
                                <div class="card mt-3" id="script-test-run-panel">
                                    <div class="card-header py-2">
                                        <span class="fw-semibold">Test Run <span class="badge bg-warning text-dark ms-1">Real I/O</span></span>
                                    </div>
                                    <div class="alert alert-warning py-1 mb-0 small rounded-0 border-0 border-bottom">
                                        <strong>Heads up:</strong>
                                        runs the script as typed (unsaved edits included) against the supplied
                                        <code>Parameters</code>.
                                        <code>External</code>, <code>Database</code>, and <code>Notify</code> calls fire for real against central's configured systems — real HTTP, real SQL, real emails. Side effects are permanent.
                                        <code>CallShared</code> executes the named shared script (saved version) in the same sandbox.
                                        <code>Instance</code>, <code>Attributes</code>, <code>Children</code>, <code>Parent</code>, and <code>CallScript</code> throw unless a bound instance is selected below — then they route to that live instance (attribute writes are permanent too).
                                    </div>
                                    <div class="card-body">
                                        <div class="mb-3">
                                            <label class="form-label small">Bind to instance <span class="text-muted">(optional)</span></label>
                                            @if (_deployedInstances.Count == 0)
                                            {
                                                <div class="form-text">
                                                    No running instances of this template.
                                                    <code>Instance</code>/<code>Attributes</code>/<code>CallScript</code> will throw.
                                                </div>
                                            }
                                            else
                                            {
                                                <select class="form-select form-select-sm" @bind="_scriptBindInstance">
                                                    <option value="">— None (Instance/Attributes throw) —</option>
                                                    @foreach (var inst in _deployedInstances)
                                                    {
                                                        <option value="@inst.UniqueName">@inst.UniqueName</option>
                                                    }
                                                </select>
                                                <div class="form-text">
                                                    Routes <code>Instance.GetAttribute/SetAttribute</code>,
                                                    <code>Attributes</code>, <code>Children</code>, <code>Parent</code>, and
                                                    <code>CallScript</code> to the selected live instance.
                                                </div>
                                            }
                                        </div>
                                        <div class="mb-3">
                                            <label class="form-label small">Parameter values</label>
                                            <ParameterValueForm ParameterDefinitions="@_scriptParameters"
                                                                Values="_scriptParamValues"
                                                                ValuesChanged="@(v => _scriptParamValues = v)" />
                                        </div>
                                        <div class="d-flex gap-2 align-items-center mb-3">
                                            <button class="btn btn-primary btn-sm" @onclick="RunScriptInSandboxAsync" disabled="@_scriptRunning">
                                                @if (_scriptRunning)
                                                {
                                                    <span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
                                                    <span>Running…</span>
                                                }
                                                else
                                                {
                                                    <span>Run</span>
                                                }
                                            </button>
                                            @if (_scriptRunResult != null)
                                            {
                                                <span class="text-muted small">@_scriptRunResult.DurationMs ms</span>
                                            }
                                        </div>

                                        @if (_scriptRunResult != null)
                                        {
                                            @if (_scriptRunResult.Success)
                                            {
                                                <div class="mb-3">
                                                    <label class="form-label small text-success mb-1">
                                                        Return value <span class="badge bg-light text-dark ms-1">@_scriptRunResult.ReturnTypeName</span>
                                                    </label>
                                                    <pre class="bg-light border rounded p-2 small mb-0 font-monospace" style="white-space: pre-wrap;">@_scriptRunResult.ReturnValueJson</pre>
                                                </div>
                                            }
                                            else
                                            {
                                                <div class="mb-3">
                                                    <label class="form-label small text-danger mb-1">
                                                        <span class="badge bg-danger me-1">@ScriptErrorKindLabel(_scriptRunResult.ErrorKind)</span>
                                                    </label>
                                                    <pre class="border border-danger-subtle rounded p-2 small mb-0 font-monospace text-danger" style="white-space: pre-wrap;">@_scriptRunResult.Error</pre>
                                                    @if (_scriptRunResult.Markers is { Count: > 0 })
                                                    {
                                                        <ul class="small text-danger mt-2 mb-0">
                                                            @foreach (var m in _scriptRunResult.Markers)
                                                            {
                                                                <li>Line @m.StartLineNumber, col @m.StartColumn: @m.Message <code class="ms-1">@m.Code</code></li>
                                                            }
                                                        </ul>
                                                    }
                                                </div>
                                            }

                                            @if (!string.IsNullOrEmpty(_scriptRunResult.ConsoleOutput))
                                            {
                                                <div class="mb-0">
                                                    <label class="form-label small mb-1">Console output</label>
                                                    <pre class="bg-dark text-light rounded p-2 small mb-0 font-monospace" style="white-space: pre-wrap;">@_scriptRunResult.ConsoleOutput</pre>
                                                </div>
                                            }
                                        }
                                    </div>
                                </div>
                            }
                        </div>
                        <div class="modal-footer">
                            <button class="btn btn-outline-primary btn-sm me-auto" @onclick="ToggleScriptTestRunPanel">
                                @(_showScriptTestRun ? "Hide Test Run" : "Test Run")
                            </button>
                            <button class="btn btn-outline-secondary btn-sm" @onclick="CancelScriptForm">Cancel</button>
                            <button class="btn btn-success btn-sm" @onclick="SaveScript">@(editingScript ? "Save" : "Add")</button>
                        </div>
                    </div>
                </div>
            </div>
        }

        var derivedScripts = _selectedTemplate!.IsDerived;
        <table class="table table-sm table-striped">
            <thead class="table-light">
                <tr>
                    <th>Name</th>
                    <th>Trigger</th>
                    <th>Code (preview)</th>
                    <th>Lock</th>
                    @if (derivedScripts)
                    {
                        <th>Source</th>
                    }
                    else
                    {
                        <th title="When true, derived templates may not override this script.">Lock in derived</th>
                    }
                    <th style="width: 60px;">Actions</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var script in _scripts)
                {
                    _baseScriptsByName.TryGetValue(script.Name, out var baseScript);
                    var lockedByBase = derivedScripts && baseScript != null && baseScript.LockedInDerived;
                    var effectiveCode = (derivedScripts && script.IsInherited && baseScript != null) ? baseScript.Code : script.Code;
                    <tr>
                        <td>@script.Name</td>
                        <td class="small">@(script.TriggerType ?? "—")</td>
                        <td class="small text-muted text-truncate font-monospace"
                            style="max-width: 300px;"
                            title="@effectiveCode">@effectiveCode[..Math.Min(80, effectiveCode.Length)]@(effectiveCode.Length > 80 ? "..." : "")</td>
                        <td>
                            @if (script.IsLocked)
                            {
                                <span class="badge bg-danger" aria-label="Locked">Locked</span>
                            }
                            else
                            {
                                <span class="badge bg-light text-dark" aria-label="Unlocked">Unlocked</span>
                            }
                        </td>
                        @if (derivedScripts)
                        {
                            <td>
                                @if (lockedByBase)
                                {
                                    <span class="badge bg-warning text-dark" title="Locked by base template — cannot override.">🔒 Base-locked</span>
                                }
                                else if (script.IsInherited)
                                {
                                    <span class="badge bg-secondary">Inherited</span>
                                }
                                else if (baseScript != null)
                                {
                                    <span class="badge bg-primary">Override</span>
                                }
                                else
                                {
                                    <span class="badge bg-light text-dark">Local</span>
                                }
                            </td>
                        }
                        else
                        {
                            <td>
                                <div class="form-check form-switch">
                                    <input class="form-check-input" type="checkbox" role="switch"
                                           checked="@script.LockedInDerived"
                                           @onchange="(e) => ToggleScriptLockedInDerived(script, e)" />
                                </div>
                            </td>
                        }
                        <td>
                            <div class="dropdown">
                                <button class="btn btn-outline-secondary btn-sm py-0 px-1"
                                        data-bs-toggle="dropdown"
                                        aria-expanded="false"
                                        aria-label="@($"More actions for {script.Name}")">⋮</button>
                                <ul class="dropdown-menu dropdown-menu-end">
                                    @if (derivedScripts && baseScript != null && !lockedByBase && !script.IsInherited)
                                    {
                                        <li>
                                            <button class="dropdown-item" @onclick="() => RevertScriptToBase(script)">Revert to base</button>
                                        </li>
                                    }
                                    @if (!(derivedScripts && baseScript != null))
                                    {
                                        <li>
                                            <button class="dropdown-item" @onclick="() => BeginEditScript(script)">Edit…</button>
                                        </li>
                                        <li><hr class="dropdown-divider" /></li>
                                        <li>
                                            <button class="dropdown-item text-danger"
                                                    @onclick="() => DeleteScript(script)">Delete</button>
                                        </li>
                                    }
                                </ul>
                            </div>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    };

    private async Task ToggleScriptLockedInDerived(TemplateScript script, ChangeEventArgs e)
    {
        var newValue = e.Value is bool b && b;
        if (script.LockedInDerived == newValue) return;
        var user = await GetCurrentUserAsync();
        var proposed = new TemplateScript(script.Name, script.Code)
        {
            IsLocked = script.IsLocked,
            TriggerType = script.TriggerType,
            TriggerConfiguration = script.TriggerConfiguration,
            ParameterDefinitions = script.ParameterDefinitions,
            ReturnDefinition = script.ReturnDefinition,
            MinTimeBetweenRuns = script.MinTimeBetweenRuns,
            IsInherited = script.IsInherited,
            LockedInDerived = newValue,
        };
        var result = await TemplateService.UpdateScriptAsync(script.Id, proposed, user);
        if (result.IsSuccess) { script.LockedInDerived = newValue; _toast.ShowSuccess("Lock-in-derived updated."); }
        else _toast.ShowError(result.Error);
    }

    private async Task RevertScriptToBase(TemplateScript script)
    {
        var confirmed = await Dialog.ConfirmAsync(
            "Revert to base",
            $"Discard the override for script '{script.Name}' and follow the base body again?");
        if (!confirmed) return;
        var user = await GetCurrentUserAsync();
        _baseScriptsByName.TryGetValue(script.Name, out var baseScript);
        var proposed = new TemplateScript(script.Name, baseScript?.Code ?? script.Code)
        {
            IsLocked = script.IsLocked,
            TriggerType = baseScript?.TriggerType ?? script.TriggerType,
            TriggerConfiguration = baseScript?.TriggerConfiguration ?? script.TriggerConfiguration,
            ParameterDefinitions = baseScript?.ParameterDefinitions ?? script.ParameterDefinitions,
            ReturnDefinition = baseScript?.ReturnDefinition ?? script.ReturnDefinition,
            MinTimeBetweenRuns = baseScript?.MinTimeBetweenRuns ?? script.MinTimeBetweenRuns,
            IsInherited = true,
            LockedInDerived = false,
        };
        var result = await TemplateService.UpdateScriptAsync(script.Id, proposed, user);
        if (result.IsSuccess) { _toast.ShowSuccess($"Script '{script.Name}' reverted to base."); await LoadAsync(); }
        else _toast.ShowError(result.Error);
    }

    // ---- CRUD handlers ----

    private void BeginAddAttribute()
    {
        _showAttrForm = true;
        _editAttrId = null;
        _attrFormError = null;
        _attrName = string.Empty;
        _attrValue = null;
        _attrDataType = default;
        _attrIsLocked = false;
        _attrDataSourceRef = null;
    }

    private void BeginEditAttribute(TemplateAttribute attr)
    {
        _showAttrForm = true;
        _editAttrId = attr.Id;
        _attrFormError = null;
        _attrName = attr.Name;
        _attrValue = attr.Value;
        _attrDataType = attr.DataType;
        _attrIsLocked = attr.IsLocked;
        _attrDataSourceRef = attr.DataSourceReference;
    }

    private void CancelAttributeForm()
    {
        _showAttrForm = false;
        _editAttrId = null;
        _attrFormError = null;
    }

    private async Task SaveAttribute()
    {
        if (_selectedTemplate == null) return;
        _attrFormError = null;
        if (string.IsNullOrWhiteSpace(_attrName)) { _attrFormError = "Name is required."; return; }

        var user = await GetCurrentUserAsync();

        if (_editAttrId is int id)
        {
            var existing = _attributes.FirstOrDefault(a => a.Id == id);
            if (existing == null) { _attrFormError = "Attribute no longer exists."; return; }
            var proposed = new TemplateAttribute(existing.Name)
            {
                DataType = _attrDataType,
                Value = _attrValue?.Trim(),
                IsLocked = _attrIsLocked,
                DataSourceReference = _attrDataSourceRef?.Trim(),
                Description = existing.Description,
                IsInherited = existing.IsInherited,
                LockedInDerived = existing.LockedInDerived,
            };
            var result = await TemplateService.UpdateAttributeAsync(id, proposed, user);
            if (result.IsSuccess)
            {
                _showAttrForm = false;
                _editAttrId = null;
                _toast.ShowSuccess($"Attribute '{existing.Name}' updated.");
                await LoadAsync();
            }
            else
            {
                _attrFormError = result.Error;
            }
            return;
        }

        var attr = new TemplateAttribute(_attrName.Trim())
        {
            DataType = _attrDataType,
            Value = _attrValue?.Trim(),
            IsLocked = _attrIsLocked,
            DataSourceReference = _attrDataSourceRef?.Trim()
        };

        var addResult = await TemplateService.AddAttributeAsync(_selectedTemplate.Id, attr, user);
        if (addResult.IsSuccess)
        {
            _showAttrForm = false;
            _toast.ShowSuccess($"Attribute '{_attrName}' added.");
            await LoadAsync();
        }
        else
        {
            _attrFormError = addResult.Error;
        }
    }

    private async Task DeleteAttribute(TemplateAttribute attr)
    {
        var confirmed = await Dialog.ConfirmAsync("Delete Attribute", $"Delete attribute '{attr.Name}'?", danger: true);
        if (!confirmed) return;
        var user = await GetCurrentUserAsync();
        var result = await TemplateService.DeleteAttributeAsync(attr.Id, user);
        if (result.IsSuccess)
        {
            _toast.ShowSuccess($"Attribute '{attr.Name}' deleted.");
            await LoadAsync();
        }
        else
        {
            _toast.ShowError(result.Error);
        }
    }

    private void BeginAddAlarm()
    {
        _showAlarmForm = true;
        _editAlarmId = null;
        _alarmFormError = null;
        _alarmName = string.Empty;
        _alarmPriority = 500;
        _alarmTriggerType = default;
        _alarmTriggerConfig = null;
        _alarmIsLocked = false;
    }

    /// <summary>
    /// Builds the attribute choice list shown in the alarm trigger editor's
    /// picker. Combines direct + inherited attributes (from <c>_attributes</c>)
    /// with composed children's attributes (from <c>_editorChildren</c>,
    /// path-qualified as <c>[ChildInstance].[AttributeName]</c>).
    /// </summary>
    private IReadOnlyList<AlarmAttributeChoice> BuildAlarmAttributeChoices()
    {
        var list = new List<AlarmAttributeChoice>(capacity: _attributes.Count + 8);

        foreach (var a in _attributes)
        {
            list.Add(new AlarmAttributeChoice(
                a.Name,
                MapDataType(a.DataType),
                a.IsInherited ? "Inherited" : "Direct"));
        }

        foreach (var child in _editorChildren)
        {
            foreach (var shape in child.Attributes)
            {
                list.Add(new AlarmAttributeChoice(
                    $"{child.Name}.{shape.Name}",
                    shape.Type,
                    "Composed"));
            }
        }

        return list;
    }

    private void BeginEditAlarm(TemplateAlarm alarm)
    {
        _showAlarmForm = true;
        _editAlarmId = alarm.Id;
        _alarmFormError = null;
        _alarmName = alarm.Name;
        _alarmPriority = alarm.PriorityLevel;
        _alarmTriggerType = alarm.TriggerType;
        _alarmTriggerConfig = alarm.TriggerConfiguration;
        _alarmIsLocked = alarm.IsLocked;
    }

    private void CancelAlarmForm()
    {
        _showAlarmForm = false;
        _editAlarmId = null;
        _alarmFormError = null;
    }

    private async Task SaveAlarm()
    {
        if (_selectedTemplate == null) return;
        _alarmFormError = null;
        if (string.IsNullOrWhiteSpace(_alarmName)) { _alarmFormError = "Name is required."; return; }

        var user = await GetCurrentUserAsync();

        if (_editAlarmId is int id)
        {
            var existing = _alarms.FirstOrDefault(a => a.Id == id);
            if (existing == null) { _alarmFormError = "Alarm no longer exists."; return; }
            var proposed = new TemplateAlarm(existing.Name)
            {
                TriggerType = existing.TriggerType, // fixed
                PriorityLevel = _alarmPriority,
                TriggerConfiguration = _alarmTriggerConfig?.Trim(),
                IsLocked = _alarmIsLocked,
                Description = existing.Description,
                OnTriggerScriptId = existing.OnTriggerScriptId,
            };
            var result = await TemplateService.UpdateAlarmAsync(id, proposed, user);
            if (result.IsSuccess)
            {
                _showAlarmForm = false;
                _editAlarmId = null;
                _toast.ShowSuccess($"Alarm '{existing.Name}' updated.");
                await LoadAsync();
            }
            else
            {
                _alarmFormError = result.Error;
            }
            return;
        }

        var alarm = new TemplateAlarm(_alarmName.Trim())
        {
            TriggerType = _alarmTriggerType,
            PriorityLevel = _alarmPriority,
            TriggerConfiguration = _alarmTriggerConfig?.Trim(),
            IsLocked = _alarmIsLocked
        };

        var addResult = await TemplateService.AddAlarmAsync(_selectedTemplate.Id, alarm, user);
        if (addResult.IsSuccess)
        {
            _showAlarmForm = false;
            _toast.ShowSuccess($"Alarm '{_alarmName}' added.");
            await LoadAsync();
        }
        else
        {
            _alarmFormError = addResult.Error;
        }
    }

    private async Task DeleteAlarm(TemplateAlarm alarm)
    {
        var confirmed = await Dialog.ConfirmAsync("Delete Alarm", $"Delete alarm '{alarm.Name}'?", danger: true);
        if (!confirmed) return;
        var user = await GetCurrentUserAsync();
        var result = await TemplateService.DeleteAlarmAsync(alarm.Id, user);
        if (result.IsSuccess)
        {
            _toast.ShowSuccess($"Alarm '{alarm.Name}' deleted.");
            await LoadAsync();
        }
        else { _toast.ShowError(result.Error); }
    }

    /// <summary>Applies the structured trigger editor's type + config atomically.</summary>
    private void OnScriptTriggerChanged(ScriptTriggerValue v)
    {
        _scriptTriggerType = v.TriggerType;
        _scriptTriggerConfig = v.Config;
    }

    /// <summary>
    /// True when the current script trigger is a WhileTrue Conditional/Expression
    /// trigger — the case where the "Min time between runs" interval is required
    /// (it is the re-fire cadence).
    /// </summary>
    private bool ScriptTriggerIsWhileTrue()
    {
        var kind = ScriptTriggerConfigCodec.ParseKind(_scriptTriggerType);
        return kind is ScriptTriggerKind.Conditional or ScriptTriggerKind.Expression
            && ScriptTriggerConfigCodec.Parse(_scriptTriggerConfig, kind).Mode
                == ScriptTriggerMode.WhileTrue;
    }

    private void BeginAddScript()
    {
        _showScriptForm = true;
        _editScriptId = null;
        _scriptFormError = null;
        _scriptName = string.Empty;
        _scriptCode = string.Empty;
        _scriptTriggerType = null;
        _scriptTriggerConfig = null;
        (_scriptMinTimeValue, _scriptMinTimeUnit) = DurationInput.Split(null);
        _scriptParameters = null;
        _scriptReturn = null;
        _scriptIsLocked = false;
        _scriptExecutionTimeoutSeconds = null;
        _scriptModalTab = "trigger";
        ResetScriptTestRun();
    }

    private void BeginEditScript(TemplateScript script)
    {
        _showScriptForm = true;
        _editScriptId = script.Id;
        _scriptFormError = null;
        _scriptName = script.Name;
        _scriptCode = script.Code;
        _scriptTriggerType = script.TriggerType;
        _scriptTriggerConfig = script.TriggerConfiguration;
        (_scriptMinTimeValue, _scriptMinTimeUnit) = DurationInput.Split(script.MinTimeBetweenRuns);
        _scriptParameters = script.ParameterDefinitions;
        _scriptReturn = script.ReturnDefinition;
        _scriptIsLocked = script.IsLocked;
        // Preserve any timeout set via Transport import — the UI has no authoring
        // control for this field, so we round-trip the loaded value unchanged.
        _scriptExecutionTimeoutSeconds = script.ExecutionTimeoutSeconds;
        _scriptModalTab = "trigger";
        ResetScriptTestRun();
    }

    private void CancelScriptForm()
    {
        _showScriptForm = false;
        _editScriptId = null;
        _scriptFormError = null;
        ResetScriptTestRun();
    }

    private void ResetScriptTestRun()
    {
        _showScriptTestRun = false;
        _scriptRunning = false;
        _scriptParamValues = new();
        _scriptBindInstance = string.Empty;
        _scriptRunResult = null;
        _scriptRunCts?.Cancel();
        _scriptRunCts = null;
    }

    private void ToggleScriptTestRunPanel() => _showScriptTestRun = !_showScriptTestRun;

    private async Task RunScriptInSandboxAsync()
    {
        _scriptRunCts?.Cancel();
        _scriptRunCts = new CancellationTokenSource();
        _scriptRunning = true;
        _scriptRunResult = null;
        StateHasChanged();

        try
        {
            var jsonParams = _scriptParamValues.ToDictionary(
                kv => kv.Key,
                kv => System.Text.Json.JsonSerializer.SerializeToElement(kv.Value));
            var request = new ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.SandboxRunRequest(
                _scriptCode, jsonParams, TimeoutSeconds: null,
                BindInstanceUniqueName: string.IsNullOrEmpty(_scriptBindInstance) ? null : _scriptBindInstance);
            _scriptRunResult = await AnalysisService.RunInSandboxAsync(request, _scriptRunCts.Token);
        }
        catch (OperationCanceledException) { /* superseded by next Run click */ }
        catch (Exception ex)
        {
            _scriptRunResult = new ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.SandboxRunResult(
                Success: false,
                ReturnValueJson: null,
                ReturnTypeName: null,
                ConsoleOutput: "",
                Error: $"Unexpected: {ex.GetType().Name}: {ex.Message}",
                ErrorKind: ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.SandboxErrorKind.RuntimeError,
                DurationMs: 0,
                Markers: null);
        }
        finally
        {
            _scriptRunning = false;
            StateHasChanged();
        }
    }

    private static string ScriptErrorKindLabel(ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.SandboxErrorKind kind) => kind switch
    {
        ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.SandboxErrorKind.CompileError => "Compile error",
        ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.SandboxErrorKind.SandboxLimitation => "Sandbox limitation",
        ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.SandboxErrorKind.RuntimeError => "Runtime error",
        ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.SandboxErrorKind.Timeout => "Timeout",
        _ => "Error"
    };

    private async Task SaveScript()
    {
        if (_selectedTemplate == null) return;
        _scriptFormError = null;
        if (string.IsNullOrWhiteSpace(_scriptName)) { _scriptFormError = "Name is required."; return; }
        if (string.IsNullOrWhiteSpace(_scriptCode)) { _scriptFormError = "Code is required."; return; }

        var user = await GetCurrentUserAsync();

        if (_editScriptId is int id)
        {
            var existing = _scripts.FirstOrDefault(s => s.Id == id);
            if (existing == null) { _scriptFormError = "Script no longer exists."; return; }
            var proposed = new TemplateScript(existing.Name, _scriptCode)
            {
                TriggerType = _scriptTriggerType?.Trim(),
                TriggerConfiguration = _scriptTriggerConfig?.Trim(),
                ParameterDefinitions = _scriptParameters,
                ReturnDefinition = _scriptReturn,
                IsLocked = _scriptIsLocked,
                MinTimeBetweenRuns = DurationInput.Compose(_scriptMinTimeValue, _scriptMinTimeUnit),
                // Round-trip the loaded value — no UI control, so preserve
                // any timeout set via Transport import unchanged.
                ExecutionTimeoutSeconds = _scriptExecutionTimeoutSeconds,
                IsInherited = existing.IsInherited,
                LockedInDerived = existing.LockedInDerived,
            };
            var result = await TemplateService.UpdateScriptAsync(id, proposed, user);
            if (result.IsSuccess)
            {
                _showScriptForm = false;
                _editScriptId = null;
                _toast.ShowSuccess($"Script '{existing.Name}' updated.");
                await LoadAsync();
            }
            else
            {
                _scriptFormError = result.Error;
            }
            return;
        }

        var script = new TemplateScript(_scriptName.Trim(), _scriptCode)
        {
            TriggerType = _scriptTriggerType?.Trim(),
            TriggerConfiguration = _scriptTriggerConfig?.Trim(),
            ParameterDefinitions = _scriptParameters,
            ReturnDefinition = _scriptReturn,
            IsLocked = _scriptIsLocked,
            MinTimeBetweenRuns = DurationInput.Compose(_scriptMinTimeValue, _scriptMinTimeUnit)
        };

        var addResult = await TemplateService.AddScriptAsync(_selectedTemplate.Id, script, user);
        if (addResult.IsSuccess)
        {
            _showScriptForm = false;
            _toast.ShowSuccess($"Script '{_scriptName}' added.");
            await LoadAsync();
        }
        else
        {
            _scriptFormError = addResult.Error;
        }
    }

    private async Task DeleteScript(TemplateScript script)
    {
        var confirmed = await Dialog.ConfirmAsync("Delete Script", $"Delete script '{script.Name}'?", danger: true);
        if (!confirmed) return;
        var user = await GetCurrentUserAsync();
        var result = await TemplateService.DeleteScriptAsync(script.Id, user);
        if (result.IsSuccess)
        {
            _toast.ShowSuccess($"Script '{script.Name}' deleted.");
            await LoadAsync();
        }
        else { _toast.ShowError(result.Error); }
    }

    /// <summary>
    /// Computes a template's qualified (hierarchical) name from the loaded
    /// template set — the stored name for a base template, the dotted
    /// owner-chain path for a composition-derived one.
    /// </summary>
    private string QualifiedTemplateName(Template template)
    {
        var byId = _templates.ToDictionary(t => t.Id);
        var compById = _templates.SelectMany(t => t.Compositions).ToDictionary(c => c.Id);
        return TemplateNaming.QualifiedName(template, byId, compById);
    }

    // ---- Editor metadata builders ----

    private async Task<IReadOnlyList<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext>> BuildChildContextsAsync(
        IReadOnlyList<ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.TemplateComposition> comps)
    {
        var result = new List<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext>();
        foreach (var comp in comps)
        {
            var composed = await TemplateEngineRepository.GetTemplateWithChildrenAsync(comp.ComposedTemplateId);
            if (composed == null) continue;
            result.Add(BuildCompositionContext(comp.InstanceName, composed));
        }
        return result;
    }

    private async Task<List<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext>> BuildParentContextsAsync(int templateId)
    {
        // Post derive-on-compose: only derived templates have a parent context,
        // and exactly one — the template that owns their composition slot.
        // Base templates suppress Parent.* assistance.
        if (_selectedTemplate?.IsDerived != true || _ownerTemplate == null)
            return new List<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext>();

        // Resolve the owner with eager-loaded members so the context has shapes.
        var owner = await TemplateEngineRepository.GetTemplateByIdAsync(_ownerTemplate.Id);
        if (owner == null)
            return new List<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext>();

        return new List<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext>
        {
            BuildCompositionContext(owner.Name, owner)
        };
    }

    private static ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext BuildCompositionContext(
        string label,
        ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates.Template t)
    {
        var attrs = t.Attributes
            .Select(a => new ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.AttributeShape(a.Name, MapDataType(a.DataType)))
            .ToList();
        var scripts = t.Scripts
            .Select(s => ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.ScriptShapeParser.Parse(
                s.Name, s.ParameterDefinitions, s.ReturnDefinition))
            .ToList();
        return new ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.CompositionContext(label, attrs, scripts);
    }

    private static string MapDataType(ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.DataType dt) => dt switch
    {
        ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.DataType.Boolean => "Boolean",
        ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.DataType.Int32 => "Integer",
        ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.DataType.Float => "Float",
        ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.DataType.Double => "Float",
        ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.DataType.String => "String",
        ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.DataType.DateTime => "String",
        ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.DataType.Binary => "Object",
        _ => "Object"
    };
}
