using System.Security.Claims;
using System.Text.Json;
using Microsoft.Data.Sqlite;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
using ZB.MOM.WW.Auth.ApiKeys.Admin;
using ZB.MOM.WW.MxGateway.Server.Security.Audit;
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;

namespace ZB.MOM.WW.MxGateway.Server.Dashboard;

public sealed class DashboardApiKeyManagementService(
    DashboardApiKeyAuthorization authorization,
    ApiKeyAdminCommands adminCommands,
    IApiKeyAdminStore adminStore,
    IAuditWriter auditWriter,
    IHttpContextAccessor httpContextAccessor,
    IApiKeyCacheInvalidator? cacheInvalidator = null) : IDashboardApiKeyManagementService
{
    private const string UnauthorizedMessage = "Sign in with an authorized LDAP account to manage API keys.";
    private const string PepperUnavailableMarker = "pepper unavailable";

    /// <inheritdoc />
    public bool CanManage(ClaimsPrincipal user)
    {
        return authorization.CanManage(user);
    }

    /// <inheritdoc />
    public async Task<DashboardApiKeyManagementResult> CreateAsync(
        ClaimsPrincipal user,
        DashboardApiKeyManagementRequest request,
        CancellationToken cancellationToken)
    {
        if (!CanManage(user))
        {
            return DashboardApiKeyManagementResult.Fail(UnauthorizedMessage);
        }

        string? validation = ValidateCreateRequest(request);
        if (validation is not null)
        {
            return DashboardApiKeyManagementResult.Fail(validation);
        }

        string keyId = request.KeyId.Trim();

        try
        {
            // The shared command set generates the secret, hashes it with the pepper, persists the
            // record and assembles the mxgw_<id>_<secret> token (shown once). It also appends its own
            // "create-key" audit entry (now canonicalized through the IApiKeyAuditStore->IAuditWriter
            // adapter); the dashboard layers a richer "dashboard-create-key" canonical AuditEvent
            // (Target + CorrelationId + remote address) on top via IAuditWriter to preserve the
            // dashboard audit vocabulary — both rows land in the canonical audit_event store.
            CreateKeyResult created = await adminCommands.CreateKeyAsync(
                    keyId,
                    request.DisplayName.Trim(),
                    request.Scopes,
                    ApiKeyConstraintSerializer.Serialize(request.Constraints),
                    RemoteAddress(),
                    cancellationToken)
                .ConfigureAwait(false);

            await WriteDashboardAuditAsync(user, keyId, "dashboard-create-key", null, cancellationToken).ConfigureAwait(false);

            return DashboardApiKeyManagementResult.Success(
                "API key created. Copy the key now; it will not be shown again.",
                created.Token);
        }
        catch (InvalidOperationException exception) when (IsPepperUnavailable(exception))
        {
            return DashboardApiKeyManagementResult.Fail("API key pepper is not configured.");
        }
        catch (SqliteException exception) when (exception.SqliteErrorCode == 19)
        {
            return DashboardApiKeyManagementResult.Fail("An API key with that id already exists.");
        }
    }

    /// <inheritdoc />
    public async Task<DashboardApiKeyManagementResult> RevokeAsync(
        ClaimsPrincipal user,
        string keyId,
        CancellationToken cancellationToken)
    {
        if (!CanManage(user))
        {
            return DashboardApiKeyManagementResult.Fail(UnauthorizedMessage);
        }

        string? validation = ValidateKeyId(keyId);
        if (validation is not null)
        {
            return DashboardApiKeyManagementResult.Fail(validation);
        }

        string normalizedKeyId = keyId.Trim();
        KeyActionResult result = await adminCommands
            .RevokeKeyAsync(normalizedKeyId, RemoteAddress(), cancellationToken)
            .ConfigureAwait(false);

        cacheInvalidator?.Invalidate(normalizedKeyId);

        await WriteDashboardAuditAsync(
                user,
                normalizedKeyId,
                "dashboard-revoke-key",
                result.Succeeded ? "revoked" : "not-found-or-already-revoked",
                cancellationToken)
            .ConfigureAwait(false);

        return result.Succeeded
            ? DashboardApiKeyManagementResult.Success("API key revoked.")
            : DashboardApiKeyManagementResult.Fail("API key was not found or is already revoked.");
    }

    /// <inheritdoc />
    public async Task<DashboardApiKeyManagementResult> RotateAsync(
        ClaimsPrincipal user,
        string keyId,
        CancellationToken cancellationToken)
    {
        if (!CanManage(user))
        {
            return DashboardApiKeyManagementResult.Fail(UnauthorizedMessage);
        }

        string? validation = ValidateKeyId(keyId);
        if (validation is not null)
        {
            return DashboardApiKeyManagementResult.Fail(validation);
        }

        string normalizedKeyId = keyId.Trim();

        try
        {
            CreateKeyResult rotated = await adminCommands
                .RotateKeyAsync(normalizedKeyId, RemoteAddress(), cancellationToken)
                .ConfigureAwait(false);

            cacheInvalidator?.Invalidate(normalizedKeyId);

            bool succeeded = rotated.Token is not null;

            await WriteDashboardAuditAsync(
                    user,
                    normalizedKeyId,
                    "dashboard-rotate-key",
                    succeeded ? "rotated" : "not-found",
                    cancellationToken)
                .ConfigureAwait(false);

            return succeeded
                ? DashboardApiKeyManagementResult.Success(
                    "API key rotated. Copy the key now; it will not be shown again.",
                    rotated.Token)
                : DashboardApiKeyManagementResult.Fail("API key was not found.");
        }
        catch (InvalidOperationException exception) when (IsPepperUnavailable(exception))
        {
            return DashboardApiKeyManagementResult.Fail("API key pepper is not configured.");
        }
    }

    /// <inheritdoc />
    public async Task<DashboardApiKeyManagementResult> DeleteAsync(
        ClaimsPrincipal user,
        string keyId,
        CancellationToken cancellationToken)
    {
        if (!CanManage(user))
        {
            return DashboardApiKeyManagementResult.Fail(UnauthorizedMessage);
        }

        string? validation = ValidateKeyId(keyId);
        if (validation is not null)
        {
            return DashboardApiKeyManagementResult.Fail(validation);
        }

        string normalizedKeyId = keyId.Trim();
        bool deleted = await adminStore
            .DeleteAsync(normalizedKeyId, cancellationToken)
            .ConfigureAwait(false);

        cacheInvalidator?.Invalidate(normalizedKeyId);

        await WriteDashboardAuditAsync(
                user,
                normalizedKeyId,
                "dashboard-delete-key",
                deleted ? "deleted" : "not-found-or-active",
                cancellationToken)
            .ConfigureAwait(false);

        return deleted
            ? DashboardApiKeyManagementResult.Success("API key deleted.")
            : DashboardApiKeyManagementResult.Fail("API key was not found, or is still active. Revoke it before deleting.");
    }

    private string? RemoteAddress() =>
        httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();

    /// <summary>
    /// Resolves the operator's username from the authenticated dashboard principal.
    /// </summary>
    /// <remarks>
    /// The passed <paramref name="user"/> is preferred over the ambient HTTP context because it
    /// is already in scope at every call site (the callers gate on <see cref="CanManage"/> using
    /// it) and is unambiguous. Falls back to <see cref="IAuditActorAccessor.CurrentActor"/> for
    /// defensive coverage, then to <c>"unknown"</c> when neither is available.
    /// </remarks>
    private static string ResolveOperatorActor(ClaimsPrincipal user)
    {
        // ZbClaimTypes.Username = "zb:username" — the canonical LDAP login name.
        string? username = user.FindFirstValue(ZB.MOM.WW.Auth.AspNetCore.ZbClaimTypes.Username);
        if (!string.IsNullOrWhiteSpace(username))
        {
            return username;
        }

        // Framework fallback: Identity.Name is driven by the nameClaimType on the ClaimsIdentity
        // (set to ZbClaimTypes.Name = ClaimTypes.Name by DashboardAuthenticator → display name).
        string? identityName = user.Identity?.Name;
        if (!string.IsNullOrWhiteSpace(identityName))
        {
            return identityName;
        }

        return "unknown";
    }

    /// <summary>
    /// Emits the dashboard's own canonical <see cref="AuditEvent"/> for a <c>dashboard-*</c> op
    /// directly through the best-effort <see cref="IAuditWriter"/>. This is in
    /// addition to the <c>create/revoke/rotate-key</c> event that <see cref="ApiKeyAdminCommands"/>
    /// emits via the canonical-forwarding <c>IApiKeyAuditStore</c> adapter — the doubled-audit
    /// behaviour is preserved, both rows now land in the canonical <c>audit_event</c> store.
    /// </summary>
    /// <remarks>
    /// <c>Actor</c> is the LDAP operator who performed the
    /// action (resolved from the <paramref name="user"/> principal); <c>Target</c> is the managed
    /// API key id. This fixes an earlier semantic gap where both fields held the keyId.
    /// </remarks>
    private async Task WriteDashboardAuditAsync(
        ClaimsPrincipal user,
        string keyId,
        string action,
        string? detail,
        CancellationToken cancellationToken)
    {
        AuditEvent auditEvent = new()
        {
            EventId = Guid.NewGuid(),
            OccurredAtUtc = DateTimeOffset.UtcNow,
            Actor = ResolveOperatorActor(user),
            Action = action,
            Outcome = AuditOutcome.Success,
            Category = CanonicalForwardingApiKeyAuditStore.ApiKeyCategory,
            Target = keyId,
            SourceNode = RemoteAddress(),
            CorrelationId = ParseCorrelationId(),
            DetailsJson = WrapDetail(detail),
        };

        await auditWriter.WriteAsync(auditEvent, cancellationToken).ConfigureAwait(false);
    }

    /// <summary>
    /// Derives a correlation id from the ASP.NET Core request trace identifier when it is a
    /// well-formed GUID; otherwise null (the default <c>HttpContext.TraceIdentifier</c> is the
    /// connection:request form, not a GUID, so it correlates to null rather than fabricating one).
    /// </summary>
    private Guid? ParseCorrelationId() =>
        Guid.TryParse(httpContextAccessor.HttpContext?.TraceIdentifier, out Guid correlationId)
            ? correlationId
            : null;

    private static string? WrapDetail(string? detail) =>
        detail is null
            ? null
            : JsonSerializer.Serialize(new Dictionary<string, string> { ["detail"] = detail });

    private static bool IsPepperUnavailable(InvalidOperationException exception) =>
        exception.Message.Contains(PepperUnavailableMarker, StringComparison.OrdinalIgnoreCase);

    private static string? ValidateCreateRequest(DashboardApiKeyManagementRequest request)
    {
        string? keyIdValidation = ValidateKeyId(request.KeyId);
        if (keyIdValidation is not null)
        {
            return keyIdValidation;
        }

        if (string.IsNullOrWhiteSpace(request.DisplayName))
        {
            return "Display name is required.";
        }

        string[] unknownScopes = request.Scopes
            .Where(scope => !GatewayScopes.IsKnown(scope))
            .ToArray();
        if (unknownScopes.Length > 0)
        {
            return $"Unknown scope(s): {string.Join(", ", unknownScopes)}. "
                + $"Valid scopes are: {string.Join(", ", GatewayScopes.All)}.";
        }

        return null;
    }

    private static string? ValidateKeyId(string keyId)
    {
        if (string.IsNullOrWhiteSpace(keyId))
        {
            return "API key id is required.";
        }

        return keyId.Trim().All(character =>
            char.IsAsciiLetterOrDigit(character)
            || character is '.' or '-')
            ? null
            : "API key id may contain only letters, numbers, periods, and hyphens.";
    }
}
