@page "/role-grants"
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.SignalR.Client
@using ZB.MOM.WW.OtOpcUa.Admin.Hubs
@using ZB.MOM.WW.OtOpcUa.Admin.Services
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
@using ZB.MOM.WW.OtOpcUa.Configuration.Services
@inject ILdapGroupRoleMappingService RoleSvc
@inject ClusterService ClusterSvc
@inject AclChangeNotifier Notifier
@inject NavigationManager Nav
@implements IAsyncDisposable

<h1 class="mb-4">LDAP group → Admin role grants</h1>

<div class="alert alert-info small mb-4">
    Maps LDAP groups to Admin UI roles (ConfigViewer / ConfigEditor / FleetAdmin). Control-plane
    only — OPC UA data-path authorization reads <code>NodeAcl</code> rows directly and is
    unaffected by these mappings (see decision #150). A fleet-wide grant applies across every
    cluster; a cluster-scoped grant only binds within the named cluster. The same LDAP group
    may hold different roles on different clusters.
</div>

<div class="d-flex justify-content-end mb-3">
    <button class="btn btn-primary btn-sm" @onclick="StartAdd">Add grant</button>
</div>

@if (_rows is null)
{
    <p>Loading…</p>
}
else if (_rows.Count == 0)
{
    <p class="text-muted">No role grants defined yet. Without at least one FleetAdmin grant,
        only the bootstrap admin can publish drafts.</p>
}
else
{
    <table class="table table-sm table-hover">
        <thead>
            <tr><th>LDAP group</th><th>Role</th><th>Scope</th><th>Created</th><th>Notes</th><th></th></tr>
        </thead>
        <tbody>
        @foreach (var r in _rows)
        {
            <tr>
                <td><code>@r.LdapGroup</code></td>
                <td><span class="badge bg-secondary">@r.Role</span></td>
                <td>@(r.IsSystemWide ? "Fleet-wide" : $"Cluster: {r.ClusterId}")</td>
                <td class="small">@r.CreatedAtUtc.ToString("yyyy-MM-dd")</td>
                <td class="small text-muted">@r.Notes</td>
                <td><button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteAsync(r.Id)">Revoke</button></td>
            </tr>
        }
        </tbody>
    </table>
}

@if (_showForm)
{
    <div class="card mt-3">
        <div class="card-body">
            <h5>New role grant</h5>
            <div class="row g-3">
                <div class="col-md-4">
                    <label class="form-label">LDAP group (DN)</label>
                    <input class="form-control" @bind="_group" placeholder="cn=fleet-admin,ou=groups,dc=…"/>
                </div>
                <div class="col-md-3">
                    <label class="form-label">Role</label>
                    <select class="form-select" @bind="_role">
                        @foreach (var r in Enum.GetValues<AdminRole>())
                        {
                            <option value="@r">@r</option>
                        }
                    </select>
                </div>
                <div class="col-md-2 pt-4">
                    <div class="form-check">
                        <input class="form-check-input" type="checkbox" id="systemWide" @bind="_isSystemWide"/>
                        <label class="form-check-label" for="systemWide">Fleet-wide</label>
                    </div>
                </div>
                <div class="col-md-3">
                    <label class="form-label">Cluster @(_isSystemWide ? "(disabled)" : "")</label>
                    <select class="form-select" @bind="_clusterId" disabled="@_isSystemWide">
                        <option value="">-- select --</option>
                        @if (_clusters is not null)
                        {
                            @foreach (var c in _clusters)
                            {
                                <option value="@c.ClusterId">@c.ClusterId</option>
                            }
                        }
                    </select>
                </div>
                <div class="col-12">
                    <label class="form-label">Notes (optional)</label>
                    <input class="form-control" @bind="_notes"/>
                </div>
            </div>
            @if (_error is not null) { <div class="alert alert-danger mt-3">@_error</div> }
            <div class="mt-3">
                <button class="btn btn-sm btn-primary" @onclick="SaveAsync">Save</button>
                <button class="btn btn-sm btn-secondary ms-2" @onclick="() => _showForm = false">Cancel</button>
            </div>
        </div>
    </div>
}

@code {
    private IReadOnlyList<LdapGroupRoleMapping>? _rows;
    private List<ServerCluster>? _clusters;
    private bool _showForm;
    private string _group = string.Empty;
    private AdminRole _role = AdminRole.ConfigViewer;
    private bool _isSystemWide;
    private string _clusterId = string.Empty;
    private string? _notes;
    private string? _error;

    protected override async Task OnInitializedAsync() => await ReloadAsync();

    private async Task ReloadAsync()
    {
        _rows = await RoleSvc.ListAllAsync(CancellationToken.None);
        _clusters = await ClusterSvc.ListAsync(CancellationToken.None);
    }

    private void StartAdd()
    {
        _group = string.Empty;
        _role = AdminRole.ConfigViewer;
        _isSystemWide = false;
        _clusterId = string.Empty;
        _notes = null;
        _error = null;
        _showForm = true;
    }

    private async Task SaveAsync()
    {
        _error = null;
        try
        {
            var row = new LdapGroupRoleMapping
            {
                LdapGroup = _group.Trim(),
                Role = _role,
                IsSystemWide = _isSystemWide,
                ClusterId = _isSystemWide ? null : (string.IsNullOrWhiteSpace(_clusterId) ? null : _clusterId),
                Notes = string.IsNullOrWhiteSpace(_notes) ? null : _notes,
            };
            await RoleSvc.CreateAsync(row, CancellationToken.None);
            await Notifier.NotifyRoleGrantsChangedAsync(CancellationToken.None);
            _showForm = false;
            await ReloadAsync();
        }
        catch (Exception ex) { _error = ex.Message; }
    }

    private async Task DeleteAsync(Guid id)
    {
        await RoleSvc.DeleteAsync(id, CancellationToken.None);
        await Notifier.NotifyRoleGrantsChangedAsync(CancellationToken.None);
        await ReloadAsync();
    }

    private HubConnection? _hub;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender || _hub is not null) return;
        _hub = new HubConnectionBuilder()
            .WithUrl(Nav.ToAbsoluteUri("/hubs/fleet-status"))
            .WithAutomaticReconnect()
            .Build();
        _hub.On<RoleGrantsChangedMessage>("RoleGrantsChanged", async _ =>
        {
            await ReloadAsync();
            await InvokeAsync(StateHasChanged);
        });
        await _hub.StartAsync();
        await _hub.SendAsync("SubscribeFleet");
    }

    public async ValueTask DisposeAsync()
    {
        if (_hub is not null) { await _hub.DisposeAsync(); _hub = null; }
    }
}
