@page "/browse"
@implements IAsyncDisposable
@inject IGalaxyHierarchyCache GalaxyCache
@inject IDashboardLiveDataService LiveData
@inject IDashboardBrowseService BrowseService
@inject IGalaxyDeployNotifier DeployNotifier
@using ZB.MOM.WW.GalaxyRepository
@using ZB.MOM.WW.GalaxyRepository.Grpc

<PageTitle>Dashboard Browse</PageTitle>

<div class="dashboard-page-header">
    <div>
        <h1>Browse</h1>
        <div class="text-secondary">@HeaderLine()</div>
    </div>
    <StatusBadge Text="@GalaxyCache.Current.Status.ToString()" />
</div>

<div class="browse-layout">
    <section class="dashboard-section browse-panel">
        <div class="section-heading">
            <h2>Galaxy Hierarchy</h2>
        </div>
        <input class="form-control form-control-sm browse-search"
               placeholder="Filter attributes by name or reference…"
               @bind="Search"
               @bind:event="oninput" />

        @if (_roots.Count == 0)
        {
            <div class="empty-state">
                No Galaxy hierarchy is cached yet. The hierarchy refreshes from the
                Galaxy Repository in the background — check the Galaxy tab for status.
            </div>
        }
        else if (!string.IsNullOrWhiteSpace(Search))
        {
            @if (_searchMatches.Count == 0)
            {
                <div class="empty-state">No attributes match &ldquo;@Search&rdquo;.</div>
            }
            else
            {
                <div class="browse-tree browse-search-results">
                    @foreach (GalaxyAttribute hit in _searchMatches)
                    {
                        GalaxyAttribute row = hit;
                        <div class="tree-attr"
                             title="@row.FullTagReference"
                             @oncontextmenu:preventDefault="true"
                             @oncontextmenu="@(args => ShowMenu(args, row))"
                             @ondblclick="@(() => AddTagAsync(row.FullTagReference))">
                            <span class="attr-icon">·</span>
                            <span class="attr-name">@row.FullTagReference</span>
                            <span class="attr-type">@FormatType(row)</span>
                            @if (row.IsAlarm)
                            {
                                <span class="attr-flag attr-flag-alarm">alarm</span>
                            }
                            @if (row.IsHistorized)
                            {
                                <span class="attr-flag attr-flag-hist">hist</span>
                            }
                        </div>
                    }
                </div>
                @if (_searchMatches.Count >= SearchResultLimit)
                {
                    <div class="browse-search-note">Showing the first @SearchResultLimit matches — refine the filter.</div>
                }
            }
        }
        else
        {
            @if (!string.IsNullOrEmpty(_staleBanner))
            {
                <div class="alert alert-info browse-stale-banner" role="status"
                     @onclick="ClearStaleBanner">@_staleBanner</div>
            }
            <div class="browse-tree">
                @foreach (DashboardBrowseNode root in _roots)
                {
                    <BrowseTreeNodeView Node="root"
                                        OnAddTag="AddTagAsync"
                                        OnTagContextMenu="OnTagContextMenu"
                                        OnLoadChildren="LoadChildrenAsync" />
                }
            </div>
            <div class="browse-search-note">Double-click a tag, or right-click for the menu.</div>
        }
    </section>

    <section class="dashboard-section browse-panel">
        <div class="section-heading">
            <h2>Subscription Panel</h2>
        </div>
        <div class="sub-panel-meta">
            @if (_subscribed.Count > 0)
            {
                <span>@_subscribed.Count subscribed</span>
                <span>·</span>
                <span>refresh 2s</span>
                @if (_workerPid is int pid)
                {
                    <span>·</span>
                    <span>worker pid @pid</span>
                }
                <button type="button" class="btn btn-outline-secondary btn-sm sub-clear" @onclick="ClearAll">Clear all</button>
            }
        </div>

        @if (!string.IsNullOrWhiteSpace(_readError))
        {
            <div class="alert alert-danger">Live read failed: @_readError</div>
        }

        @if (_subscribed.Count == 0)
        {
            <div class="empty-state">
                No tags subscribed. Right-click a tag in the hierarchy and choose
                <strong>Add to subscription panel</strong> (or double-click it) to watch its
                live value, quality and source timestamp here.
            </div>
        }
        else
        {
            <div class="table-responsive">
                <table class="table table-sm dashboard-table sub-table">
                    <thead>
                        <tr>
                            <th scope="col">Tag</th>
                            <th scope="col">Value</th>
                            <th scope="col">Type</th>
                            <th scope="col">Quality</th>
                            <th scope="col">Updated</th>
                            <th scope="col" class="sub-actions-col"></th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach (string tag in _subscribed)
                        {
                            string key = tag;
                            DashboardTagValue? value = _values.GetValueOrDefault(key);
                            <tr>
                                <td><code>@key</code></td>
                                <td class="sub-value">@(value?.ValueText ?? "…")</td>
                                <td>@(value?.DataType ?? "-")</td>
                                <td>
                                    @if (value is null)
                                    {
                                        <span class="text-secondary">…</span>
                                    }
                                    else
                                    {
                                        <span class="quality-chip @(value.QualityGood ? "quality-good" : "quality-bad")">
                                            @value.Quality
                                        </span>
                                        @if (!string.IsNullOrWhiteSpace(value.Error))
                                        {
                                            <span class="sub-error" title="@value.Error">!</span>
                                        }
                                    }
                                </td>
                                <td title="@TimestampTooltip(value)">@TimestampText(key, value)</td>
                                <td class="sub-actions-col">
                                    <button type="button"
                                            class="btn btn-outline-secondary btn-sm"
                                            @onclick="@(() => RemoveTag(key))">Remove</button>
                                </td>
                            </tr>
                        }
                    </tbody>
                </table>
            </div>
        }
    </section>
</div>

@if (_menuVisible)
{
    <div class="context-menu-overlay"
         @onclick="HideMenu"
         @oncontextmenu:preventDefault="true"
         @oncontextmenu="HideMenu"></div>
    <div class="context-menu" style="left:@(_menuX)px; top:@(_menuY)px;">
        <div class="context-menu-head">@(_menuAttribute?.AttributeName)</div>
        <button type="button" class="context-menu-item" @onclick="AddMenuTagAsync">
            Add to subscription panel
        </button>
    </div>
}

@code {
    private const int SearchResultLimit = 300;

    private List<DashboardBrowseNode> _roots = [];
    private ulong _cacheSequence;
    private string? _staleBanner;
    private CancellationTokenSource _deployCts = new();
    private Task? _deployTask;
    private string _search = string.Empty;
    private IReadOnlyList<GalaxyAttribute> _searchMatches = [];
    private readonly List<string> _subscribed = [];
    private readonly Dictionary<string, DashboardTagValue> _values = new(StringComparer.Ordinal);
    // Per-tag bookkeeping for the Updated column: the signature of the value
    // last seen, and when that value/quality was first observed. Lets the
    // column move only on a real change, not on every 2s poll.
    private readonly Dictionary<string, string> _valueSignature = new(StringComparer.Ordinal);
    private readonly Dictionary<string, DateTimeOffset> _observedChangeAt = new(StringComparer.Ordinal);
    private string? _readError;
    private int? _workerPid;

    private bool _menuVisible;
    private int _menuX;
    private int _menuY;
    private GalaxyAttribute? _menuAttribute;

    private readonly CancellationTokenSource _cts = new();
    private Task? _pollTask;

    /// <inheritdoc />
    protected override void OnInitialized()
    {
        BrowseLevelResult roots = BrowseService.GetRoots(new BrowseFilterArgs());
        _roots = [.. roots.Nodes];
        _cacheSequence = roots.CacheSequence;
        _pollTask = PollLoopAsync();
        _deployTask = SubscribeToDeployEventsAsync();
    }

    private async Task LoadChildrenAsync(DashboardBrowseNode node)
    {
        BrowseLevelResult result = BrowseService.GetChildren(node.Object.GobjectId, new BrowseFilterArgs());
        if (!string.IsNullOrEmpty(result.Error))
        {
            throw new InvalidOperationException(result.Error);
        }

        node.Children.Clear();
        foreach (DashboardBrowseNode child in result.Nodes)
        {
            node.Children.Add(child);
        }

        // First expand interaction also dismisses the stale banner — the user
        // is clearly engaging with the tree, no need to keep nagging.
        _staleBanner = null;
        await InvokeAsync(StateHasChanged);
    }

    private async Task SubscribeToDeployEventsAsync()
    {
        try
        {
            await foreach (GalaxyDeployEventInfo info in DeployNotifier
                .SubscribeAsync(_deployCts.Token)
                .ConfigureAwait(false))
            {
                // First Latest replay echoes the sequence we already projected
                // from — skip those to avoid a spurious "redeployed" banner.
                if (info.Sequence == 0 || (ulong)info.Sequence == _cacheSequence)
                {
                    continue;
                }

                BrowseLevelResult roots = BrowseService.GetRoots(new BrowseFilterArgs());
                _roots = [.. roots.Nodes];
                _cacheSequence = roots.CacheSequence;
                _staleBanner = "Galaxy redeployed — tree refreshed.";
                await InvokeAsync(StateHasChanged);
            }
        }
        catch (OperationCanceledException)
        {
        }
    }

    private void ClearStaleBanner()
    {
        _staleBanner = null;
    }

    private string HeaderLine()
    {
        GalaxyHierarchyCacheEntry entry = GalaxyCache.Current;
        return $"{entry.ObjectCount:N0} objects · {entry.AttributeCount:N0} attributes · "
            + $"{entry.AlarmAttributeCount:N0} alarm attributes";
    }

    private string Search
    {
        get => _search;
        set
        {
            _search = value ?? string.Empty;
            _searchMatches = ComputeSearch(_search);
        }
    }

    private IReadOnlyList<GalaxyAttribute> ComputeSearch(string rawQuery)
    {
        string query = rawQuery.Trim();
        if (query.Length == 0)
        {
            return [];
        }

        List<GalaxyAttribute> matches = [];
        foreach (GalaxyObject galaxyObject in GalaxyCache.Current.Objects)
        {
            foreach (GalaxyAttribute attr in galaxyObject.Attributes)
            {
                if (attr.FullTagReference.Contains(query, StringComparison.OrdinalIgnoreCase)
                    || attr.AttributeName.Contains(query, StringComparison.OrdinalIgnoreCase))
                {
                    matches.Add(attr);
                    if (matches.Count >= SearchResultLimit)
                    {
                        return matches;
                    }
                }
            }
        }

        return matches;
    }

    private static string FormatType(GalaxyAttribute attr)
    {
        string baseType = string.IsNullOrWhiteSpace(attr.DataTypeName) ? "type?" : attr.DataTypeName;
        if (!attr.IsArray)
        {
            return baseType;
        }

        return attr.ArrayDimensionPresent ? $"{baseType}[{attr.ArrayDimension}]" : $"{baseType}[]";
    }

    private Task OnTagContextMenu((MouseEventArgs Event, GalaxyAttribute Attribute) args)
    {
        ShowMenu(args.Event, args.Attribute);
        return Task.CompletedTask;
    }

    private void ShowMenu(MouseEventArgs args, GalaxyAttribute attr)
    {
        _menuAttribute = attr;
        _menuX = (int)args.ClientX;
        _menuY = (int)args.ClientY;
        _menuVisible = true;
    }

    private void HideMenu()
    {
        _menuVisible = false;
        _menuAttribute = null;
    }

    private async Task AddMenuTagAsync()
    {
        GalaxyAttribute? attr = _menuAttribute;
        HideMenu();
        if (attr is not null)
        {
            await AddTagAsync(attr.FullTagReference);
        }
    }

    private async Task AddTagAsync(string fullReference)
    {
        if (string.IsNullOrWhiteSpace(fullReference)
            || _subscribed.Contains(fullReference, StringComparer.Ordinal))
        {
            return;
        }

        _subscribed.Add(fullReference);
        await RefreshValuesAsync();
    }

    private void RemoveTag(string tag)
    {
        _subscribed.Remove(tag);
        _values.Remove(tag);
        _valueSignature.Remove(tag);
        _observedChangeAt.Remove(tag);
    }

    private void ClearAll()
    {
        _subscribed.Clear();
        _values.Clear();
        _valueSignature.Clear();
        _observedChangeAt.Clear();
        _readError = null;
    }

    // The MXAccess source timestamp when the worker supplies one, otherwise the
    // time the dashboard first observed the current value/quality.
    private string TimestampText(string tag, DashboardTagValue? value)
    {
        if (value is null)
        {
            return "…";
        }

        if (value.SourceTimestamp is { } source)
        {
            return DashboardDisplay.DateTime(source);
        }

        return _observedChangeAt.TryGetValue(tag, out DateTimeOffset observed)
            ? DashboardDisplay.DateTime(observed)
            : "-";
    }

    private static string TimestampTooltip(DashboardTagValue? value)
    {
        return value?.SourceTimestamp is not null
            ? "MXAccess source timestamp."
            : "When the dashboard first observed this value — MXAccess did not supply a source timestamp for this tag.";
    }

    private async Task PollLoopAsync()
    {
        try
        {
            using PeriodicTimer timer = new(TimeSpan.FromSeconds(2));
            while (await timer.WaitForNextTickAsync(_cts.Token).ConfigureAwait(false))
            {
                await InvokeAsync(RefreshValuesAsync).ConfigureAwait(false);
            }
        }
        catch (OperationCanceledException)
        {
        }
    }

    private async Task RefreshValuesAsync()
    {
        if (_subscribed.Count == 0)
        {
            return;
        }

        string[] tags = [.. _subscribed];
        DashboardLiveReadResult result = await LiveData.ReadAsync(tags, _cts.Token);
        _readError = result.Error;
        _workerPid = result.WorkerProcessId;
        DateTimeOffset now = DateTimeOffset.UtcNow;
        foreach (DashboardTagValue value in result.Values)
        {
            // Stamp the observed-change time only when the value/quality
            // signature actually changes, so the Updated column does not
            // tick on every poll for a static tag.
            string signature = $"{value.ValueText}{value.Quality}{value.Ok}";
            if (!_valueSignature.TryGetValue(value.TagAddress, out string? previous)
                || previous != signature)
            {
                _valueSignature[value.TagAddress] = signature;
                _observedChangeAt[value.TagAddress] = now;
            }

            _values[value.TagAddress] = value;
        }

        StateHasChanged();
    }

    /// <inheritdoc />
    public async ValueTask DisposeAsync()
    {
        await _cts.CancelAsync();
        await _deployCts.CancelAsync();
        if (_pollTask is not null)
        {
            try
            {
                await _pollTask;
            }
            catch (OperationCanceledException)
            {
            }
        }
        if (_deployTask is not null)
        {
            try
            {
                await _deployTask;
            }
            catch (OperationCanceledException)
            {
            }
        }

        _cts.Dispose();
        _deployCts.Dispose();
        GC.SuppressFinalize(this);
    }
}
