using System.Security.Claims;
using Microsoft.Data.Sqlite;
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,
    IApiKeyAdminStore adminStore,
    IApiKeyAuditStore auditStore,
    IApiKeySecretHasher hasher,
    IHttpContextAccessor httpContextAccessor) : IDashboardApiKeyManagementService
{
    private const string UnauthorizedMessage = "Sign in with an authorized LDAP account to manage API keys.";

    public bool CanManage(ClaimsPrincipal user)
    {
        return authorization.CanManage(user);
    }

    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();
        string secret = ApiKeySecretGenerator.Generate();
        string apiKey = FormatApiKey(keyId, secret);

        try
        {
            await adminStore.CreateAsync(
                    new ApiKeyCreateRequest(
                        KeyId: keyId,
                        KeyPrefix: $"mxgw_{keyId}",
                        SecretHash: hasher.HashSecret(secret),
                        DisplayName: request.DisplayName.Trim(),
                        Scopes: request.Scopes,
                        Constraints: request.Constraints,
                        CreatedUtc: DateTimeOffset.UtcNow),
                    cancellationToken)
                .ConfigureAwait(false);

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

            return DashboardApiKeyManagementResult.Success("API key created. Copy the key now; it will not be shown again.", apiKey);
        }
        catch (ApiKeyPepperUnavailableException)
        {
            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.");
        }
    }

    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();
        bool revoked = await adminStore
            .RevokeAsync(normalizedKeyId, DateTimeOffset.UtcNow, cancellationToken)
            .ConfigureAwait(false);

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

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

    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();
        string secret = ApiKeySecretGenerator.Generate();
        string apiKey = FormatApiKey(normalizedKeyId, secret);

        try
        {
            bool rotated = await adminStore
                .RotateAsync(normalizedKeyId, hasher.HashSecret(secret), DateTimeOffset.UtcNow, cancellationToken)
                .ConfigureAwait(false);

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

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

    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);

        await AppendAuditAsync(
                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 async Task AppendAuditAsync(
        string? keyId,
        string eventType,
        string? details,
        CancellationToken cancellationToken)
    {
        await auditStore.AppendAsync(
                new ApiKeyAuditEntry(
                    KeyId: keyId,
                    EventType: eventType,
                    RemoteAddress: httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(),
                    Details: details),
                cancellationToken)
            .ConfigureAwait(false);
    }

    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.";
    }

    private static string FormatApiKey(string keyId, string secret)
    {
        return $"mxgw_{keyId}_{secret}";
    }
}
