@page "/clusters/{ClusterId}/draft/{GenerationId:long}/diff"
@using ZB.MOM.WW.OtOpcUa.Admin.Services
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
@inject GenerationService GenerationSvc

<div class="d-flex justify-content-between align-items-center mb-3">
    <div>
        <h1 class="mb-0">Draft diff</h1>
        <small class="text-muted">
            Cluster <code>@ClusterId</code> — from last published (@(_fromLabel)) → to draft @GenerationId
        </small>
    </div>
    <a class="btn btn-outline-secondary" href="/clusters/@ClusterId/draft/@GenerationId">Back to editor</a>
</div>

@if (_rows is null)
{
    <p>Computing diff…</p>
}
else if (_error is not null)
{
    <div class="alert alert-danger">@_error</div>
}
else if (_rows.Count == 0)
{
    <p class="text-muted">No differences — draft is structurally identical to the last published generation.</p>
}
else
{
    <p class="small text-muted mb-3">
        @_rows.Count row@(_rows.Count == 1 ? "" : "s") across @_sectionsWithChanges of @Sections.Count sections.
        Each section is capped at @DiffSection.DefaultRowCap rows to keep the browser responsive on pathological drafts.
    </p>

    @foreach (var sec in Sections)
    {
        <DiffSection Title="@sec.Title"
                     Description="@sec.Description"
                     Rows="@RowsFor(sec.TableName)"/>
    }
}

@code {
    [Parameter] public string ClusterId { get; set; } = string.Empty;
    [Parameter] public long GenerationId { get; set; }

    /// <summary>
    ///     Ordered section definitions — each maps a <c>TableName</c> emitted by
    ///     <c>sp_ComputeGenerationDiff</c> to a human label + description. The proc currently
    ///     emits Namespace/DriverInstance/Equipment/Tag; UnsLine + NodeAcl entries render as
    ///     empty "no changes" cards until the proc is extended (tracked in tasks #196 + #156
    ///     follow-up). Six sections total matches the task #156 target.
    /// </summary>
    private static readonly IReadOnlyList<SectionDef> Sections = new[]
    {
        new SectionDef("Namespace",      "Namespaces",      "OPC UA namespace URIs + enablement"),
        new SectionDef("DriverInstance", "Driver instances","Per-cluster driver configuration rows"),
        new SectionDef("Equipment",      "Equipment",       "UNS level-5 rows + identification fields"),
        new SectionDef("Tag",            "Tags",            "Per-device tag definitions + poll-group binding"),
        new SectionDef("UnsLine",        "UNS structure",   "Site / Area / Line hierarchy (proc-extension pending)"),
        new SectionDef("NodeAcl",        "ACLs",            "LDAP-group → node-scope permission grants (logical id = LdapGroup|ScopeKind|ScopeId)"),
    };

    private List<DiffRow>? _rows;
    private string _fromLabel = "(empty)";
    private string? _error;
    private int _sectionsWithChanges;

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            var all = await GenerationSvc.ListRecentAsync(ClusterId, 50, CancellationToken.None);
            var from = all.FirstOrDefault(g => g.Status == GenerationStatus.Published);
            _fromLabel = from is null ? "(empty)" : $"gen {from.GenerationId}";
            _rows = await GenerationSvc.ComputeDiffAsync(from?.GenerationId ?? 0, GenerationId, CancellationToken.None);
            _sectionsWithChanges = Sections.Count(s => _rows.Any(r => r.TableName == s.TableName));
        }
        catch (Exception ex) { _error = ex.Message; }
    }

    private IReadOnlyList<DiffRow> RowsFor(string tableName) =>
        _rows?.Where(r => r.TableName == tableName).ToList() ?? [];

    private sealed record SectionDef(string TableName, string Title, string Description);
}
