@page "/uns"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@rendermode RenderMode.InteractiveServer
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns
@inject IUnsTreeService Svc

<PageTitle>UNS</PageTitle>

<div class="d-flex justify-content-between align-items-center mb-3">
    <h4 class="mb-0">UNS</h4>
    <span class="text-muted small">Changes apply on the next deployment.</span>
</div>

<section class="panel rise" style="animation-delay:.02s">
    <div class="panel-head d-flex justify-content-between align-items-center flex-wrap gap-2">
        <span>Unified namespace</span>
        <div class="d-flex align-items-center gap-2 flex-wrap">
            <input type="text" class="form-control form-control-sm" style="max-width:240px"
                   placeholder="Filter by name (substring)…"
                   @bind="_filter" @bind:event="oninput" />
            <button type="button" class="btn btn-outline-secondary btn-sm" @onclick="ExpandAll">Expand all</button>
            <button type="button" class="btn btn-outline-secondary btn-sm" @onclick="CollapseAll">Collapse all</button>
            <button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenImportModal">Import equipment CSV</button>
        </div>
    </div>

    @if (_loading)
    {
        <div style="padding:1rem" class="text-muted">
            <span class="spinner-border spinner-border-sm me-1"></span>Loading…
        </div>
    }
    else if (_roots.Count == 0)
    {
        <div style="padding:1rem" class="text-muted">No clusters yet.</div>
    }
    else
    {
        <div style="padding:.5rem 1rem">
            <UnsTree Roots="_roots" Filter="@_filter"
                     OnToggleExpand="ToggleAsync"
                     OnAddChild="HandleAddChild"
                     OnAddVirtualTag="HandleAddVirtualTag"
                     OnEdit="HandleEdit"
                     OnDelete="HandleDelete" />
        </div>
    }
</section>

<AreaModal Visible="_areaModalVisible"
           IsNew="_areaModalIsNew"
           ClusterId="@_areaModalClusterId"
           Existing="_areaModalExisting"
           Clusters="ClusterOptions"
           OnSaved="OnModalSavedAsync"
           OnCancel="CloseModals" />

<LineModal Visible="_lineModalVisible"
           IsNew="_lineModalIsNew"
           UnsAreaId="@_lineModalAreaId"
           Existing="_lineModalExisting"
           Areas="_lineModalAreaOptions"
           OnSaved="OnModalSavedAsync"
           OnCancel="CloseModals" />

<EquipmentModal Visible="_equipmentModalVisible"
                IsNew="_equipmentModalIsNew"
                UnsLineId="@_equipmentModalLineId"
                Existing="_equipmentModalExisting"
                Lines="_equipmentModalLineOptions"
                Drivers="_equipmentModalDriverOptions"
                OnSaved="OnModalSavedAsync"
                OnCancel="CloseModals" />

<TagModal Visible="_tagModalVisible"
          IsNew="_tagModalIsNew"
          EquipmentId="@_tagModalEquipmentId"
          Existing="_tagModalExisting"
          Drivers="_tagModalDriverOptions"
          OnSaved="OnEquipmentChildModalSavedAsync"
          OnCancel="CloseModals" />

<VirtualTagModal Visible="_vtagModalVisible"
                 IsNew="_vtagModalIsNew"
                 EquipmentId="@_vtagModalEquipmentId"
                 Existing="_vtagModalExisting"
                 Scripts="_vtagModalScriptOptions"
                 OnSaved="OnEquipmentChildModalSavedAsync"
                 OnCancel="CloseModals" />

<ImportEquipmentModal Visible="_importModalVisible"
                      OnImported="OnImportedAsync"
                      OnCancel="CloseModals" />

@if (_confirmNode is not null)
{
    <div class="modal-backdrop fade show" style="display:block"></div>
    <div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Delete @_confirmNode.Kind</h5>
                    <button type="button" class="btn-close" aria-label="Close" @onclick="CloseModals"></button>
                </div>
                <div class="modal-body">
                    <p>Delete <span class="mono">@_confirmNode.DisplayName</span>? This cannot be undone.</p>
                    @if (!string.IsNullOrWhiteSpace(_confirmError))
                    {
                        <div class="text-danger small mt-2">@_confirmError</div>
                    }
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-outline-secondary" @onclick="CloseModals" disabled="@_confirmBusy">Cancel</button>
                    <button type="button" class="btn btn-danger" @onclick="ConfirmDeleteAsync" disabled="@_confirmBusy">
                        @if (_confirmBusy) { <span class="spinner-border spinner-border-sm me-1"></span> }
                        Delete
                    </button>
                </div>
            </div>
        </div>
    </div>
}

@code {
    private IReadOnlyList<UnsNode> _roots = Array.Empty<UnsNode>();
    private string? _filter;
    private bool _loading = true;

    // Guards the async modal openers (HandleAddChild/HandleAddVirtualTag/HandleEdit) so a rapid
    // double-action can't race two service loads into the same modal state.
    private bool _modalBusy;

    // --- Area modal state ---
    private bool _areaModalVisible;
    private bool _areaModalIsNew;
    private string? _areaModalClusterId;
    private AreaEditDto? _areaModalExisting;

    // --- Line modal state ---
    private bool _lineModalVisible;
    private bool _lineModalIsNew;
    private string? _lineModalAreaId;
    private LineEditDto? _lineModalExisting;
    private IReadOnlyList<(string Id, string Display)> _lineModalAreaOptions = Array.Empty<(string, string)>();

    // --- Equipment modal state ---
    private bool _equipmentModalVisible;
    private bool _equipmentModalIsNew;
    private string? _equipmentModalLineId;
    private EquipmentEditDto? _equipmentModalExisting;
    private IReadOnlyList<(string Id, string Display)> _equipmentModalLineOptions = Array.Empty<(string, string)>();
    private IReadOnlyList<(string Id, string Display)> _equipmentModalDriverOptions = Array.Empty<(string, string)>();

    // --- Tag modal state ---
    private bool _tagModalVisible;
    private bool _tagModalIsNew;
    private string? _tagModalEquipmentId;
    private TagEditDto? _tagModalExisting;
    private IReadOnlyList<(string Id, string Display, string DriverType)> _tagModalDriverOptions = Array.Empty<(string, string, string)>();

    // --- Virtual-tag modal state ---
    private bool _vtagModalVisible;
    private bool _vtagModalIsNew;
    private string? _vtagModalEquipmentId;
    private VirtualTagEditDto? _vtagModalExisting;
    private IReadOnlyList<(string Id, string Display)> _vtagModalScriptOptions = Array.Empty<(string, string)>();

    // --- Import-equipment-CSV modal state ---
    private bool _importModalVisible;

    // --- Owning equipment to refresh in place after a tag/virtual-tag mutation ---
    private string? _childRefreshEquipmentId;

    // --- Delete-confirm state ---
    private UnsNode? _confirmNode;
    private bool _confirmBusy;
    private string? _confirmError;

    /// <summary>The served-by cluster options for the AreaModal, derived from the loaded tree.</summary>
    private IReadOnlyList<(string Id, string Display)> ClusterOptions =>
        _roots
            .SelectMany(ent => ent.Children)
            .Where(n => n.Kind == UnsNodeKind.Cluster && n.EntityId is not null)
            .Select(n => (n.EntityId!, n.DisplayName))
            .ToList();

    protected override async Task OnInitializedAsync()
    {
        _roots = await Svc.LoadStructureAsync();
        _loading = false;
    }

    /// <summary>Returns the <c>(Id, Display)</c> area options inside a single cluster, for the line picker.</summary>
    private IReadOnlyList<(string Id, string Display)> AreaOptionsForCluster(string? clusterId) =>
        _roots
            .SelectMany(ent => ent.Children)
            .Where(c => c.Kind == UnsNodeKind.Cluster && c.ClusterId == clusterId)
            .SelectMany(c => c.Children)
            .Where(a => a.Kind == UnsNodeKind.Area && a.EntityId is not null)
            .Select(a => (a.EntityId!, a.DisplayName))
            .ToList();

    /// <summary>Returns the <c>(Id, Display)</c> line options inside a single cluster, for the equipment picker.</summary>
    private IReadOnlyList<(string Id, string Display)> LinesForCluster(string? clusterId) =>
        _roots
            .SelectMany(ent => ent.Children)
            .Where(c => c.Kind == UnsNodeKind.Cluster && c.ClusterId == clusterId)
            .SelectMany(c => c.Children)
            .Where(a => a.Kind == UnsNodeKind.Area)
            .SelectMany(a => a.Children)
            .Where(l => l.Kind == UnsNodeKind.Line && l.EntityId is not null)
            .Select(l => (l.EntityId!, l.DisplayName))
            .ToList();

    /// <summary>
    /// Toggles a node's expansion. For equipment nodes whose children have not yet
    /// been loaded, lazily fetches the tag/virtual-tag leaves on first expand.
    /// </summary>
    private async Task ToggleAsync(UnsNode node)
    {
        if (node.Loading) return;

        node.Expanded = !node.Expanded;

        if (node.Kind == UnsNodeKind.Equipment && node.Expanded && !node.Loaded)
        {
            node.Error = null;
            node.Loading = true;
            StateHasChanged();
            try
            {
                var kids = await Svc.LoadEquipmentChildrenAsync(node.EntityId!);
                node.Children.Clear();
                node.Children.AddRange(kids);
                node.Loaded = true;
            }
            catch (Exception ex)
            {
                node.Error = ex.Message;
            }
            finally
            {
                node.Loading = false;
                StateHasChanged();
            }
        }
    }

    /// <summary>
    /// Opens the create modal for a node's primary child: a cluster gets a new area; an area gets a
    /// new line scoped to its cluster; a line gets a new equipment scoped to its cluster; an equipment
    /// gets a new tag scoped to its candidate drivers.
    /// </summary>
    private async Task HandleAddChild(UnsNode node)
    {
        if (_modalBusy) { return; }
        _modalBusy = true;
        try
        {
            CloseModals();
            switch (node.Kind)
            {
                case UnsNodeKind.Cluster:
                    _areaModalIsNew = true;
                    _areaModalExisting = null;
                    _areaModalClusterId = node.ClusterId ?? node.EntityId;
                    _areaModalVisible = true;
                    break;

                case UnsNodeKind.Area:
                    _lineModalIsNew = true;
                    _lineModalExisting = null;
                    _lineModalAreaId = node.EntityId;
                    _lineModalAreaOptions = AreaOptionsForCluster(node.ClusterId);
                    _lineModalVisible = true;
                    break;

                case UnsNodeKind.Line:
                    _equipmentModalIsNew = true;
                    _equipmentModalExisting = null;
                    _equipmentModalLineId = node.EntityId;
                    _equipmentModalLineOptions = LinesForCluster(node.ClusterId);
                    _equipmentModalDriverOptions = await Svc.LoadDriversForClusterAsync(node.ClusterId!);
                    _equipmentModalVisible = true;
                    break;

                case UnsNodeKind.Equipment:
                    _tagModalIsNew = true;
                    _tagModalExisting = null;
                    _tagModalEquipmentId = node.EntityId;
                    _childRefreshEquipmentId = node.EntityId;
                    _tagModalDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(node.EntityId!);
                    _tagModalVisible = true;
                    break;
            }
        }
        finally
        {
            _modalBusy = false;
        }
    }

    /// <summary>Opens the create modal for a new virtual tag scoped to the clicked equipment.</summary>
    private async Task HandleAddVirtualTag(UnsNode node)
    {
        if (_modalBusy) { return; }
        _modalBusy = true;
        try
        {
            CloseModals();
            _vtagModalIsNew = true;
            _vtagModalExisting = null;
            _vtagModalEquipmentId = node.EntityId;
            _childRefreshEquipmentId = node.EntityId;
            _vtagModalScriptOptions = await Svc.LoadScriptsAsync();
            _vtagModalVisible = true;
        }
        finally
        {
            _modalBusy = false;
        }
    }

    /// <summary>
    /// Opens the edit modal for the clicked node, loading the entity first to prefill the form and
    /// capture its RowVersion. Tag/VirtualTag edits also stash the owning equipment id so a successful
    /// save can refresh just that equipment's children in place.
    /// </summary>
    private async Task HandleEdit(UnsNode node)
    {
        if (_modalBusy) { return; }
        _modalBusy = true;
        try
        {
            CloseModals();
            switch (node.Kind)
            {
                case UnsNodeKind.Area:
                    var area = await Svc.LoadAreaAsync(node.EntityId!);
                    if (area is null) { return; }
                    _areaModalIsNew = false;
                    _areaModalExisting = area;
                    _areaModalClusterId = area.ClusterId;
                    _areaModalVisible = true;
                    break;

                case UnsNodeKind.Line:
                    var line = await Svc.LoadLineAsync(node.EntityId!);
                    if (line is null) { return; }
                    _lineModalIsNew = false;
                    _lineModalExisting = line;
                    _lineModalAreaId = line.UnsAreaId;
                    _lineModalAreaOptions = AreaOptionsForCluster(node.ClusterId);
                    _lineModalVisible = true;
                    break;

                case UnsNodeKind.Equipment:
                    var equipment = await Svc.LoadEquipmentAsync(node.EntityId!);
                    if (equipment is null) { return; }
                    _equipmentModalIsNew = false;
                    _equipmentModalExisting = equipment;
                    _equipmentModalLineId = equipment.UnsLineId;
                    _equipmentModalLineOptions = LinesForCluster(node.ClusterId);
                    _equipmentModalDriverOptions = await Svc.LoadDriversForClusterAsync(node.ClusterId!);
                    _equipmentModalVisible = true;
                    break;

                case UnsNodeKind.Tag:
                    var tag = await Svc.LoadTagAsync(node.EntityId!);
                    if (tag is null) { return; }
                    _tagModalIsNew = false;
                    _tagModalExisting = tag;
                    _tagModalEquipmentId = tag.EquipmentId;
                    _childRefreshEquipmentId = tag.EquipmentId;
                    _tagModalDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(tag.EquipmentId);
                    _tagModalVisible = true;
                    break;

                case UnsNodeKind.VirtualTag:
                    var vtag = await Svc.LoadVirtualTagAsync(node.EntityId!);
                    if (vtag is null) { return; }
                    _vtagModalIsNew = false;
                    _vtagModalExisting = vtag;
                    _vtagModalEquipmentId = vtag.EquipmentId;
                    _childRefreshEquipmentId = vtag.EquipmentId;
                    _vtagModalScriptOptions = await Svc.LoadScriptsAsync();
                    _vtagModalVisible = true;
                    break;
            }
        }
        finally
        {
            _modalBusy = false;
        }
    }

    /// <summary>Opens the bulk equipment-CSV import modal.</summary>
    private void OpenImportModal()
    {
        CloseModals();
        _importModalVisible = true;
    }

    /// <summary>
    /// Handles the import modal's close after a run. A bulk import can add equipment across many
    /// lines and clusters, so the whole structural tree is reloaded rather than refreshed in place.
    /// </summary>
    private async Task OnImportedAsync()
    {
        _roots = await Svc.LoadStructureAsync();
        CloseModals();
        StateHasChanged();
    }

    /// <summary>Opens the delete-confirm modal for a node, stashing it as the pending target.</summary>
    private void HandleDelete(UnsNode node)
    {
        CloseModals();
        _confirmNode = node;
    }

    /// <summary>
    /// Performs the pending delete. Loads the entity's RowVersion first, then dispatches on Kind.
    /// Area/Line/Equipment trigger a full structural reload on success; Tag/VirtualTag refresh only the
    /// owning equipment's children in place so the rest of the user's expansion is preserved.
    /// </summary>
    private async Task ConfirmDeleteAsync()
    {
        if (_confirmNode is null) { return; }

        _confirmBusy = true;
        _confirmError = null;
        try
        {
            var node = _confirmNode;
            UnsMutationResult result;

            // Tag/VirtualTag deletes refresh just the owning equipment's children rather than the
            // whole tree, so they're handled separately from the structural Area/Line/Equipment path.
            switch (node.Kind)
            {
                case UnsNodeKind.Tag:
                    var tag = await Svc.LoadTagAsync(node.EntityId!);
                    if (tag is null) { await ReloadAndCloseAsync(); return; }
                    result = await Svc.DeleteTagAsync(node.EntityId!, tag.RowVersion);
                    if (result.Ok)
                    {
                        await RefreshEquipmentChildrenAsync(tag.EquipmentId);
                        CloseModals();
                        StateHasChanged();
                    }
                    else
                    {
                        _confirmError = result.Error;
                    }
                    return;

                case UnsNodeKind.VirtualTag:
                    var vtag = await Svc.LoadVirtualTagAsync(node.EntityId!);
                    if (vtag is null) { await ReloadAndCloseAsync(); return; }
                    result = await Svc.DeleteVirtualTagAsync(node.EntityId!, vtag.RowVersion);
                    if (result.Ok)
                    {
                        await RefreshEquipmentChildrenAsync(vtag.EquipmentId);
                        CloseModals();
                        StateHasChanged();
                    }
                    else
                    {
                        _confirmError = result.Error;
                    }
                    return;

                case UnsNodeKind.Area:
                    var area = await Svc.LoadAreaAsync(node.EntityId!);
                    if (area is null) { await ReloadAndCloseAsync(); return; }
                    result = await Svc.DeleteAreaAsync(node.EntityId!, area.RowVersion);
                    break;

                case UnsNodeKind.Line:
                    var line = await Svc.LoadLineAsync(node.EntityId!);
                    if (line is null) { await ReloadAndCloseAsync(); return; }
                    result = await Svc.DeleteLineAsync(node.EntityId!, line.RowVersion);
                    break;

                case UnsNodeKind.Equipment:
                    var equipment = await Svc.LoadEquipmentAsync(node.EntityId!);
                    if (equipment is null) { await ReloadAndCloseAsync(); return; }
                    result = await Svc.DeleteEquipmentAsync(node.EntityId!, equipment.RowVersion);
                    break;

                default:
                    // Enterprise/Cluster have no delete button, so this branch is unreachable in practice.
                    result = new UnsMutationResult(false, "Delete for this node kind is not yet available.");
                    break;
            }

            if (result.Ok)
            {
                await ReloadAndCloseAsync();
            }
            else
            {
                _confirmError = result.Error;
            }
        }
        finally
        {
            _confirmBusy = false;
        }
    }

    /// <summary>Reloads the tree after a successful modal save and closes any open modal.</summary>
    private async Task OnModalSavedAsync()
    {
        _roots = await Svc.LoadStructureAsync();
        CloseModals();
        StateHasChanged();
    }

    /// <summary>
    /// Handles a successful Tag/VirtualTag modal save by refreshing only the owning equipment's children
    /// in place — never a full structural reload, which would collapse the user's expansion.
    /// </summary>
    private async Task OnEquipmentChildModalSavedAsync()
    {
        if (_childRefreshEquipmentId is not null)
        {
            await RefreshEquipmentChildrenAsync(_childRefreshEquipmentId);
        }
        CloseModals();
        StateHasChanged();
    }

    /// <summary>
    /// Reloads a single equipment node's tag/virtual-tag children in place, leaving the rest of the tree
    /// (and the user's expansion) untouched. Falls back to a full structural reload only if the node
    /// can no longer be found in the current tree. Either branch only mutates state — the caller is
    /// responsible for calling StateHasChanged() afterwards (every current caller does).
    /// </summary>
    private async Task RefreshEquipmentChildrenAsync(string equipmentId)
    {
        var node = FindEquipmentNode(equipmentId);
        if (node is null)
        {
            // Fallback: the equipment node is no longer in the current tree — reload the whole structure.
            _roots = await Svc.LoadStructureAsync();
            return;
        }

        var kids = await Svc.LoadEquipmentChildrenAsync(equipmentId);
        node.Children.Clear();
        node.Children.AddRange(kids);
        node.ChildCount = node.Children.Count;
        node.Loaded = true;
        node.Expanded = true;
    }

    /// <summary>Recursively walks the current tree for the Equipment node with the given id, or null.</summary>
    private UnsNode? FindEquipmentNode(string equipmentId)
    {
        foreach (var root in _roots)
        {
            var found = FindEquipmentNode(root, equipmentId);
            if (found is not null) { return found; }
        }
        return null;
    }

    private static UnsNode? FindEquipmentNode(UnsNode node, string equipmentId)
    {
        if (node.Kind == UnsNodeKind.Equipment && node.EntityId == equipmentId)
        {
            return node;
        }

        foreach (var child in node.Children)
        {
            var found = FindEquipmentNode(child, equipmentId);
            if (found is not null) { return found; }
        }
        return null;
    }

    /// <summary>Reloads the tree after a successful delete and closes the confirm modal.</summary>
    private async Task ReloadAndCloseAsync()
    {
        _roots = await Svc.LoadStructureAsync();
        CloseModals();
        StateHasChanged();
    }

    /// <summary>Closes every modal and clears its transient state.</summary>
    private void CloseModals()
    {
        _areaModalVisible = false;
        _areaModalIsNew = false;
        _areaModalClusterId = null;
        _areaModalExisting = null;
        _lineModalVisible = false;
        _lineModalIsNew = false;
        _lineModalAreaId = null;
        _lineModalExisting = null;
        _lineModalAreaOptions = Array.Empty<(string, string)>();
        _equipmentModalVisible = false;
        _equipmentModalIsNew = false;
        _equipmentModalLineId = null;
        _equipmentModalExisting = null;
        _equipmentModalLineOptions = Array.Empty<(string, string)>();
        _equipmentModalDriverOptions = Array.Empty<(string, string)>();
        _tagModalVisible = false;
        _tagModalIsNew = false;
        _tagModalEquipmentId = null;
        _tagModalExisting = null;
        _tagModalDriverOptions = Array.Empty<(string, string, string)>();
        _vtagModalVisible = false;
        _vtagModalIsNew = false;
        _vtagModalEquipmentId = null;
        _vtagModalExisting = null;
        _vtagModalScriptOptions = Array.Empty<(string, string)>();
        _importModalVisible = false;
        _childRefreshEquipmentId = null;
        _confirmNode = null;
        _confirmError = null;
    }

    /// <summary>
    /// Expands every structural node (Enterprise/Cluster/Area/Line). Equipment nodes
    /// are intentionally left collapsed because expanding them would trigger lazy loads.
    /// </summary>
    private void ExpandAll()
    {
        foreach (var root in _roots)
        {
            ExpandStructural(root);
        }
    }

    private static void ExpandStructural(UnsNode node)
    {
        if (node.Kind is UnsNodeKind.Enterprise or UnsNodeKind.Cluster
            or UnsNodeKind.Area or UnsNodeKind.Line)
        {
            node.Expanded = true;
        }

        foreach (var child in node.Children)
        {
            ExpandStructural(child);
        }
    }

    /// <summary>Collapses every node in the tree.</summary>
    private void CollapseAll()
    {
        foreach (var root in _roots)
        {
            CollapseNode(root);
        }
    }

    private static void CollapseNode(UnsNode node)
    {
        node.Expanded = false;
        foreach (var child in node.Children)
        {
            CollapseNode(child);
        }
    }
}
