@page "/apikeys"
@inherits DashboardPageBase
@using ZB.MOM.WW.MxGateway.Server.Security.Authentication
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IDashboardApiKeyManagementService ApiKeyManagementService

<PageTitle>Dashboard API Keys</PageTitle>

@if (Snapshot is null)
{
    <div class="empty-state">Loading API keys.</div>
}
else
{
    <div class="dashboard-page-header">
        <div>
            <h1>API Keys</h1>
            <div class="text-secondary">@Snapshot.ApiKeys.Count key rows</div>
        </div>
        @if (CanManageApiKeys)
        {
            <button type="button" class="btn btn-primary" @onclick="OpenCreateDialog">
                Create API Key
            </button>
        }
    </div>

    @if (CanManageApiKeys)
    {
        @if (!string.IsNullOrWhiteSpace(ResultMessage))
        {
            <div class="alert @(LastOperationSucceeded ? "alert-success" : "alert-danger")" role="alert">
                @ResultMessage
                @if (!string.IsNullOrWhiteSpace(LastGeneratedApiKey))
                {
                    <div class="mt-2">
                        <code class="one-time-secret">@LastGeneratedApiKey</code>
                    </div>
                }
            </div>
        }

        <ConfirmDialog IsOpen="@(PendingAction is not null)"
                       Title="@(PendingAction?.Title ?? string.Empty)"
                       Message="@(PendingAction?.Message ?? string.Empty)"
                       ConfirmLabel="@(PendingAction?.ConfirmLabel ?? "Confirm")"
                       ConfirmButtonClass="@(PendingAction?.ConfirmButtonClass ?? "btn-primary")"
                       IsBusy="IsBusy"
                       OnConfirm="ConfirmPendingAsync"
                       OnCancel="CancelPending" />

        @if (IsCreateDialogOpen)
        {
            <div class="modal-backdrop fade show"></div>
            <div class="modal fade show api-key-create-modal" role="dialog" aria-modal="true" aria-labelledby="createApiKeyTitle">
                <div class="modal-dialog modal-xl modal-dialog-scrollable">
                    <div class="modal-content">
                        <EditForm Model="@CreateModel" OnSubmit="@CreateApiKeyAsync">
                            <div class="modal-header">
                                <h2 class="modal-title h5" id="createApiKeyTitle">Create API Key</h2>
                                <button type="button" class="btn-close" aria-label="Close" @onclick="CloseCreateDialog"></button>
                            </div>
                            <div class="modal-body">
                                <div class="api-key-management-grid">
                                    <div class="mb-3">
                                        <label for="keyId" class="form-label">Key ID</label>
                                        <input id="keyId" class="form-control" @bind="CreateModel.KeyId" @bind:event="oninput" />
                                    </div>
                                    <div class="mb-3">
                                        <label for="displayName" class="form-label">Display Name</label>
                                        <input id="displayName" class="form-control" @bind="CreateModel.DisplayName" @bind:event="oninput" />
                                    </div>
                                </div>
                                <fieldset class="mb-3">
                                    <legend class="form-label">Scopes</legend>
                                    <div class="scope-grid">
                                        @foreach (string scope in AvailableScopes)
                                        {
                                            <label class="form-check">
                                                <input class="form-check-input" type="checkbox"
                                                       checked="@IsScopeSelected(scope)"
                                                       @onchange="eventArgs => SetScope(scope, eventArgs)" />
                                                <span class="form-check-label">@scope</span>
                                            </label>
                                        }
                                    </div>
                                </fieldset>
                                <div class="api-key-management-grid">
                                    <div class="mb-3">
                                        <label for="readSubtrees" class="form-label">Read subtrees</label>
                                        <textarea id="readSubtrees" class="form-control" rows="2" @bind="CreateModel.ReadSubtrees" @bind:event="oninput"></textarea>
                                    </div>
                                    <div class="mb-3">
                                        <label for="writeSubtrees" class="form-label">Write subtrees</label>
                                        <textarea id="writeSubtrees" class="form-control" rows="2" @bind="CreateModel.WriteSubtrees" @bind:event="oninput"></textarea>
                                    </div>
                                    <div class="mb-3">
                                        <label for="readTagGlobs" class="form-label">Read tag globs</label>
                                        <textarea id="readTagGlobs" class="form-control" rows="2" @bind="CreateModel.ReadTagGlobs" @bind:event="oninput"></textarea>
                                    </div>
                                    <div class="mb-3">
                                        <label for="writeTagGlobs" class="form-label">Write tag globs</label>
                                        <textarea id="writeTagGlobs" class="form-control" rows="2" @bind="CreateModel.WriteTagGlobs" @bind:event="oninput"></textarea>
                                    </div>
                                    <div class="mb-3">
                                        <label for="browseSubtrees" class="form-label">Browse subtrees</label>
                                        <textarea id="browseSubtrees" class="form-control" rows="2" @bind="CreateModel.BrowseSubtrees" @bind:event="oninput"></textarea>
                                    </div>
                                    <div class="mb-3">
                                        <label for="maxWriteClassification" class="form-label">Max write classification</label>
                                        <input id="maxWriteClassification" class="form-control" @bind="CreateModel.MaxWriteClassification" @bind:event="oninput" />
                                    </div>
                                </div>
                                <div class="d-flex flex-wrap gap-3">
                                    <label class="form-check">
                                        <InputCheckbox class="form-check-input" @bind-Value="CreateModel.ReadAlarmOnly" />
                                        <span class="form-check-label">Read alarm only</span>
                                    </label>
                                    <label class="form-check">
                                        <InputCheckbox class="form-check-input" @bind-Value="CreateModel.ReadHistorizedOnly" />
                                        <span class="form-check-label">Read historized only</span>
                                    </label>
                                </div>
                            </div>
                            <div class="modal-footer">
                                <button type="button" class="btn btn-outline-secondary" disabled="@IsBusy" @onclick="CloseCreateDialog">
                                    Cancel
                                </button>
                                <button type="submit" class="btn btn-primary" disabled="@IsBusy">Create Key</button>
                            </div>
                        </EditForm>
                    </div>
                </div>
            </div>
        }
    }

    <section class="dashboard-section">
        @if (Snapshot.ApiKeys.Count == 0)
        {
            <div class="empty-state">No API keys are available for display.</div>
        }
        else
        {
            <div class="table-responsive">
                <table class="table table-sm align-middle dashboard-table">
                    <thead>
                        <tr>
                            <th scope="col">Key</th>
                            <th scope="col">Status</th>
                            <th scope="col">Display Name</th>
                            <th scope="col">Scopes</th>
                            <th scope="col">Constraints</th>
                            <th scope="col">Created</th>
                            <th scope="col">Last Used</th>
                            @if (CanManageApiKeys)
                            {
                                <th scope="col">Actions</th>
                            }
                        </tr>
                    </thead>
                    <tbody>
                        @foreach (DashboardApiKeySummary key in Snapshot.ApiKeys)
                        {
                            <tr>
                                <td><code>@key.KeyId</code></td>
                                <td><StatusBadge Text="@(key.RevokedUtc is null ? "Active" : "Revoked")" /></td>
                                <td>@DashboardDisplay.Text(key.DisplayName)</td>
                                <td>@DashboardDisplay.Text(string.Join(", ", key.Scopes.Order(StringComparer.Ordinal)))</td>
                                <td>@DashboardDisplay.Text(ConstraintText(key.Constraints))</td>
                                <td>@DashboardDisplay.DateTime(key.CreatedUtc)</td>
                                <td>@DashboardDisplay.DateTime(key.LastUsedUtc)</td>
                                @if (CanManageApiKeys)
                                {
                                    <td>
                                        <div class="btn-group btn-group-sm" role="group" aria-label="API key actions">
                                            @if (key.RevokedUtc is null)
                                            {
                                                @* Rotate clears revoked_utc, which would silently reactivate a
                                                   deliberately revoked key. Only offer it for active keys so a
                                                   revoked key is not un-revoked as a side effect of rotation. *@
                                                <button type="button" class="btn btn-outline-secondary"
                                                        disabled="@IsBusy"
                                                        @onclick="() => RequestRotate(key.KeyId)">
                                                    Rotate
                                                </button>
                                                <button type="button" class="btn btn-outline-danger"
                                                        disabled="@IsBusy"
                                                        @onclick="() => RequestRevoke(key.KeyId)">
                                                    Revoke
                                                </button>
                                            }
                                            else
                                            {
                                                <button type="button" class="btn btn-outline-danger"
                                                        disabled="@IsBusy"
                                                        @onclick="() => RequestDelete(key.KeyId)">
                                                    Delete
                                                </button>
                                            }
                                        </div>
                                    </td>
                                }
                            </tr>
                        }
                    </tbody>
                </table>
            </div>
        }
    </section>
}

@code {
    private static readonly string[] AvailableScopes =
    [
        GatewayScopes.SessionOpen,
        GatewayScopes.SessionClose,
        GatewayScopes.InvokeRead,
        GatewayScopes.InvokeWrite,
        GatewayScopes.InvokeSecure,
        GatewayScopes.EventsRead,
        GatewayScopes.MetadataRead,
        GatewayScopes.Admin
    ];

    private ApiKeyCreateModel CreateModel { get; } = new();

    private bool CanManageApiKeys { get; set; }

    private bool IsBusy { get; set; }

    private bool IsCreateDialogOpen { get; set; }

    private string? ResultMessage { get; set; }

    private bool LastOperationSucceeded { get; set; }

    private string? LastGeneratedApiKey { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync().ConfigureAwait(false);

        AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
            .ConfigureAwait(false);
        CanManageApiKeys = ApiKeyManagementService.CanManage(authenticationState.User);
    }

    private async Task CreateApiKeyAsync()
    {
        if (IsBusy)
        {
            return;
        }

        if (!TryBuildCreateRequest(out DashboardApiKeyManagementRequest? request, out string? validationMessage))
        {
            SetResult(DashboardApiKeyManagementResult.Fail(validationMessage ?? "API key request is invalid."));
            return;
        }

        await RunManagementActionAsync(user => ApiKeyManagementService.CreateAsync(
                user,
                request,
                CancellationToken.None))
            .ConfigureAwait(false);
    }

    private PendingConfirm? PendingAction { get; set; }

    private void RequestRotate(string keyId)
    {
        if (IsBusy)
        {
            return;
        }

        PendingAction = new PendingConfirm(
            Title: "Rotate API key?",
            Message: $"Rotate the secret for key {keyId}? Any client still using the previous secret will start failing immediately.",
            ConfirmLabel: "Rotate",
            ConfirmButtonClass: "btn-warning",
            Action: user => ApiKeyManagementService.RotateAsync(user, keyId, CancellationToken.None));
    }

    private void RequestRevoke(string keyId)
    {
        if (IsBusy)
        {
            return;
        }

        PendingAction = new PendingConfirm(
            Title: "Revoke API key?",
            Message: $"Revoke key {keyId}? Clients using it will be rejected on the next request.",
            ConfirmLabel: "Revoke",
            ConfirmButtonClass: "btn-danger",
            Action: user => ApiKeyManagementService.RevokeAsync(user, keyId, CancellationToken.None));
    }

    private void RequestDelete(string keyId)
    {
        if (IsBusy)
        {
            return;
        }

        PendingAction = new PendingConfirm(
            Title: "Delete API key?",
            Message: $"Permanently delete revoked key {keyId}? This removes the row from the auth database — only the audit log will retain the history.",
            ConfirmLabel: "Delete",
            ConfirmButtonClass: "btn-danger",
            Action: user => ApiKeyManagementService.DeleteAsync(user, keyId, CancellationToken.None));
    }

    private void CancelPending()
    {
        if (!IsBusy)
        {
            PendingAction = null;
        }
    }

    private async Task ConfirmPendingAsync()
    {
        if (IsBusy || PendingAction is null)
        {
            return;
        }

        // Server-047: align the pending-action lifecycle with SessionsPage / SessionDetailsPage —
        // hold PendingAction while the awaited action runs so the shared ConfirmDialog can render
        // its in-flight (IsBusy) state, then clear in finally regardless of outcome.
        Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardApiKeyManagementResult>> action = PendingAction.Action;
        try
        {
            await RunManagementActionAsync(action).ConfigureAwait(false);
        }
        finally
        {
            PendingAction = null;
        }
    }

    private sealed record PendingConfirm(
        string Title,
        string Message,
        string ConfirmLabel,
        string ConfirmButtonClass,
        Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardApiKeyManagementResult>> Action);

    private async Task RunManagementActionAsync(
        Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardApiKeyManagementResult>> action)
    {
        if (IsBusy)
        {
            return;
        }

        IsBusy = true;
        try
        {
            AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
                .ConfigureAwait(false);
            CanManageApiKeys = ApiKeyManagementService.CanManage(authenticationState.User);
            DashboardApiKeyManagementResult result = await action(authenticationState.User).ConfigureAwait(false);
            SetResult(result);
            if (result.Succeeded && result.ApiKey is not null)
            {
                CreateModel.Reset();
                IsCreateDialogOpen = false;
            }
        }
        finally
        {
            IsBusy = false;
        }
    }

    private void SetResult(DashboardApiKeyManagementResult result)
    {
        LastOperationSucceeded = result.Succeeded;
        ResultMessage = result.Message;
        LastGeneratedApiKey = result.ApiKey;
    }

    private void OpenCreateDialog()
    {
        IsCreateDialogOpen = true;
    }

    private void CloseCreateDialog()
    {
        if (!IsBusy)
        {
            IsCreateDialogOpen = false;
        }
    }

    private bool TryBuildCreateRequest(
        [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out DashboardApiKeyManagementRequest? request,
        out string? validationMessage)
    {
        request = null;
        validationMessage = null;

        if (!string.IsNullOrWhiteSpace(CreateModel.MaxWriteClassification)
            && !int.TryParse(
                CreateModel.MaxWriteClassification,
                System.Globalization.NumberStyles.Integer,
                System.Globalization.CultureInfo.InvariantCulture,
                out int _))
        {
            validationMessage = "Max write classification must be an integer.";
            return false;
        }

        int? maxWriteClassification = string.IsNullOrWhiteSpace(CreateModel.MaxWriteClassification)
            ? null
            : int.Parse(
                CreateModel.MaxWriteClassification,
                System.Globalization.NumberStyles.Integer,
                System.Globalization.CultureInfo.InvariantCulture);

        request = new DashboardApiKeyManagementRequest(
            KeyId: CreateModel.KeyId,
            DisplayName: CreateModel.DisplayName,
            Scopes: CreateModel.SelectedScopes,
            Constraints: new ApiKeyConstraints(
                ReadSubtrees: ParseList(CreateModel.ReadSubtrees),
                WriteSubtrees: ParseList(CreateModel.WriteSubtrees),
                ReadTagGlobs: ParseList(CreateModel.ReadTagGlobs),
                WriteTagGlobs: ParseList(CreateModel.WriteTagGlobs),
                MaxWriteClassification: maxWriteClassification,
                BrowseSubtrees: ParseList(CreateModel.BrowseSubtrees),
                ReadAlarmOnly: CreateModel.ReadAlarmOnly,
                ReadHistorizedOnly: CreateModel.ReadHistorizedOnly));

        return true;
    }

    private bool IsScopeSelected(string scope)
    {
        return CreateModel.SelectedScopes.Contains(scope);
    }

    private void SetScope(string scope, ChangeEventArgs eventArgs)
    {
        bool selected = eventArgs.Value is bool value && value;
        if (selected)
        {
            CreateModel.SelectedScopes.Add(scope);
        }
        else
        {
            CreateModel.SelectedScopes.Remove(scope);
        }
    }

    private static string ConstraintText(ApiKeyConstraints constraints)
    {
        if (constraints.IsEmpty)
        {
            return "unconstrained";
        }

        List<string> parts = [];
        AddList(parts, "read_subtrees", constraints.ReadSubtrees);
        AddList(parts, "write_subtrees", constraints.WriteSubtrees);
        AddList(parts, "read_tag_globs", constraints.ReadTagGlobs);
        AddList(parts, "write_tag_globs", constraints.WriteTagGlobs);
        AddList(parts, "browse_subtrees", constraints.BrowseSubtrees);
        if (constraints.MaxWriteClassification is { } max)
        {
            parts.Add($"max_write_classification={max}");
        }

        if (constraints.ReadAlarmOnly)
        {
            parts.Add("read_alarm_only");
        }

        if (constraints.ReadHistorizedOnly)
        {
            parts.Add("read_historized_only");
        }

        return string.Join("; ", parts);
    }

    private static void AddList(List<string> parts, string name, IReadOnlyList<string> values)
    {
        if (values.Count > 0)
        {
            parts.Add($"{name}=[{string.Join(", ", values)}]");
        }
    }

    private static IReadOnlyList<string> ParseList(string? value)
    {
        return (value ?? string.Empty)
            .Split([',', ';', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Where(item => !string.IsNullOrWhiteSpace(item))
            .ToArray();
    }

    private sealed class ApiKeyCreateModel
    {
        public string KeyId { get; set; } = string.Empty;

        public string DisplayName { get; set; } = string.Empty;

        public HashSet<string> SelectedScopes { get; } = new(StringComparer.Ordinal);

        public string ReadSubtrees { get; set; } = string.Empty;

        public string WriteSubtrees { get; set; } = string.Empty;

        public string ReadTagGlobs { get; set; } = string.Empty;

        public string WriteTagGlobs { get; set; } = string.Empty;

        public string BrowseSubtrees { get; set; } = string.Empty;

        public string MaxWriteClassification { get; set; } = string.Empty;

        public bool ReadAlarmOnly { get; set; }

        public bool ReadHistorizedOnly { get; set; }

        public void Reset()
        {
            KeyId = string.Empty;
            DisplayName = string.Empty;
            SelectedScopes.Clear();
            ReadSubtrees = string.Empty;
            WriteSubtrees = string.Empty;
            ReadTagGlobs = string.Empty;
            WriteTagGlobs = string.Empty;
            BrowseSubtrees = string.Empty;
            MaxWriteClassification = string.Empty;
            ReadAlarmOnly = false;
            ReadHistorizedOnly = false;
        }
    }
}
