using System.Text.Json;

namespace ZB.MOM.WW.MxGateway.Server.Security.Authentication;

/// <summary>
/// Executes API key administration commands from the CLI.
/// </summary>
public sealed class ApiKeyAdminCliRunner(
    IAuthStoreMigrator migrator,
    IApiKeyAdminStore adminStore,
    IApiKeyAuditStore auditStore,
    IApiKeySecretHasher hasher)
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        WriteIndented = true
    };

    /// <summary>
    /// Runs an API key administration command and writes the output.
    /// </summary>
    /// <param name="command">API key administration command to execute.</param>
    /// <param name="output">Text writer for command output.</param>
    /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
    public async Task<int> RunAsync(
        ApiKeyAdminCommand command,
        TextWriter output,
        CancellationToken cancellationToken)
    {
        ApiKeyAdminOutput result = command.Kind switch
        {
            ApiKeyAdminCommandKind.InitDb => await InitDbAsync(cancellationToken).ConfigureAwait(false),
            ApiKeyAdminCommandKind.CreateKey => await CreateKeyAsync(command, cancellationToken).ConfigureAwait(false),
            ApiKeyAdminCommandKind.ListKeys => await ListKeysAsync(cancellationToken).ConfigureAwait(false),
            ApiKeyAdminCommandKind.RevokeKey => await RevokeKeyAsync(command, cancellationToken).ConfigureAwait(false),
            ApiKeyAdminCommandKind.RotateKey => await RotateKeyAsync(command, cancellationToken).ConfigureAwait(false),
            _ => throw new InvalidOperationException($"Unsupported API key command '{command.Kind}'.")
        };

        await WriteOutputAsync(command, result, output).ConfigureAwait(false);

        return 0;
    }

    private async Task<ApiKeyAdminOutput> InitDbAsync(CancellationToken cancellationToken)
    {
        await migrator.MigrateAsync(cancellationToken).ConfigureAwait(false);
        await AppendAuditAsync(null, "init-db", null, cancellationToken).ConfigureAwait(false);

        return new ApiKeyAdminOutput("init-db", "initialized", null, []);
    }

    private async Task<ApiKeyAdminOutput> CreateKeyAsync(
        ApiKeyAdminCommand command,
        CancellationToken cancellationToken)
    {
        await migrator.MigrateAsync(cancellationToken).ConfigureAwait(false);

        string keyId = Required(command.KeyId);
        string secret = ApiKeySecretGenerator.Generate();
        string apiKey = FormatApiKey(keyId, secret);

        await adminStore.CreateAsync(
                new ApiKeyCreateRequest(
                    KeyId: keyId,
                    KeyPrefix: $"mxgw_{keyId}",
                    SecretHash: hasher.HashSecret(secret),
                    DisplayName: Required(command.DisplayName),
                    Scopes: command.Scopes,
                    Constraints: command.Constraints,
                    CreatedUtc: DateTimeOffset.UtcNow),
                cancellationToken)
            .ConfigureAwait(false);
        await AppendAuditAsync(keyId, "create-key", null, cancellationToken).ConfigureAwait(false);

        return new ApiKeyAdminOutput("create-key", "created", apiKey, []);
    }

    private async Task<ApiKeyAdminOutput> ListKeysAsync(CancellationToken cancellationToken)
    {
        await migrator.MigrateAsync(cancellationToken).ConfigureAwait(false);
        IReadOnlyList<ApiKeyRecord> keys = await adminStore.ListAsync(cancellationToken).ConfigureAwait(false);
        await AppendAuditAsync(null, "list-keys", null, cancellationToken).ConfigureAwait(false);

        return new ApiKeyAdminOutput(
            "list-keys",
            "ok",
            null,
            keys.Select(ToListedKey).ToArray());
    }

    private async Task<ApiKeyAdminOutput> RevokeKeyAsync(
        ApiKeyAdminCommand command,
        CancellationToken cancellationToken)
    {
        await migrator.MigrateAsync(cancellationToken).ConfigureAwait(false);

        string keyId = Required(command.KeyId);
        bool revoked = await adminStore.RevokeAsync(keyId, DateTimeOffset.UtcNow, cancellationToken)
            .ConfigureAwait(false);

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

        return new ApiKeyAdminOutput("revoke-key", revoked ? "revoked" : "not-found-or-already-revoked", null, []);
    }

    private async Task<ApiKeyAdminOutput> RotateKeyAsync(
        ApiKeyAdminCommand command,
        CancellationToken cancellationToken)
    {
        await migrator.MigrateAsync(cancellationToken).ConfigureAwait(false);

        string keyId = Required(command.KeyId);
        string secret = ApiKeySecretGenerator.Generate();
        string apiKey = FormatApiKey(keyId, secret);

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

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

        return new ApiKeyAdminOutput("rotate-key", rotated ? "rotated" : "not-found", rotated ? apiKey : null, []);
    }

    private static async Task WriteOutputAsync(
        ApiKeyAdminCommand command,
        ApiKeyAdminOutput result,
        TextWriter output)
    {
        if (command.Json)
        {
            await output.WriteLineAsync(JsonSerializer.Serialize(result, JsonOptions)).ConfigureAwait(false);
            return;
        }

        await output.WriteLineAsync($"{result.Command}: {result.Status}").ConfigureAwait(false);

        if (result.ApiKey is not null)
        {
            await output.WriteLineAsync($"API key: {result.ApiKey}").ConfigureAwait(false);
        }

        foreach (ApiKeyAdminListedKey key in result.Keys)
        {
            string revoked = key.RevokedUtc is null ? "active" : "revoked";
            await output.WriteLineAsync($"{key.KeyId}\t{key.DisplayName}\t{revoked}\t{string.Join(',', key.Scopes)}")
                .ConfigureAwait(false);
        }
    }

    private async Task AppendAuditAsync(
        string? keyId,
        string eventType,
        string? details,
        CancellationToken cancellationToken)
    {
        await auditStore.AppendAsync(
                new ApiKeyAuditEntry(
                    KeyId: keyId,
                    EventType: eventType,
                    RemoteAddress: null,
                    Details: details),
                cancellationToken)
            .ConfigureAwait(false);
    }

    private static ApiKeyAdminListedKey ToListedKey(ApiKeyRecord key)
    {
        return new ApiKeyAdminListedKey(
            KeyId: key.KeyId,
            KeyPrefix: key.KeyPrefix,
            DisplayName: key.DisplayName,
            Scopes: key.Scopes,
            Constraints: key.Constraints,
            CreatedUtc: key.CreatedUtc,
            LastUsedUtc: key.LastUsedUtc,
            RevokedUtc: key.RevokedUtc);
    }

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

    private static string Required(string? value)
    {
        return value ?? throw new InvalidOperationException("Required command value was not provided.");
    }
}
