using System.Text.Json;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
using ZB.MOM.WW.Auth.ApiKeys.Admin;

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

/// <summary>
/// Executes API key administration commands from the CLI.
/// </summary>
/// <remarks>
/// The create/revoke/rotate/list/init-db verbs (secret generation, peppered hashing, token
/// assembly and per-action audit) are delegated to the shared
/// <see cref="ApiKeyAdminCommands"/>. This runner adapts the gateway's strongly-typed command and
/// output DTOs (which carry <see cref="ApiKeyConstraints"/>) onto the library's JSON-based contract.
/// </remarks>
public sealed class ApiKeyAdminCliRunner(ApiKeyAdminCommands commands)
{
    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>
    /// <returns>The process exit code for the executed command.</returns>
    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 commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false);

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

    private async Task<ApiKeyAdminOutput> CreateKeyAsync(
        ApiKeyAdminCommand command,
        CancellationToken cancellationToken)
    {
        // The shared command set requires the schema to exist; init-db is idempotent.
        await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false);

        string keyId = Required(command.KeyId);
        CreateKeyResult created = await commands.CreateKeyAsync(
                keyId,
                Required(command.DisplayName),
                command.Scopes,
                ApiKeyConstraintSerializer.Serialize(command.Constraints),
                command.ExpiresUtc,
                remoteAddress: null,
                cancellationToken)
            .ConfigureAwait(false);

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

    private async Task<ApiKeyAdminOutput> ListKeysAsync(CancellationToken cancellationToken)
    {
        await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false);
        IReadOnlyList<ApiKeyListItem> keys = await commands.ListKeysAsync(cancellationToken).ConfigureAwait(false);

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

    private async Task<ApiKeyAdminOutput> RevokeKeyAsync(
        ApiKeyAdminCommand command,
        CancellationToken cancellationToken)
    {
        await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false);

        string keyId = Required(command.KeyId);
        KeyActionResult result = await commands.RevokeKeyAsync(keyId, remoteAddress: null, cancellationToken)
            .ConfigureAwait(false);

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

    private async Task<ApiKeyAdminOutput> RotateKeyAsync(
        ApiKeyAdminCommand command,
        CancellationToken cancellationToken)
    {
        await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false);

        string keyId = Required(command.KeyId);
        CreateKeyResult rotated = await commands.RotateKeyAsync(keyId, remoteAddress: null, cancellationToken)
            .ConfigureAwait(false);

        bool succeeded = rotated.Token is not null;

        return new ApiKeyAdminOutput("rotate-key", succeeded ? "rotated" : "not-found", rotated.Token, []);
    }

    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 status = key.RevokedUtc is not null
                ? "revoked"
                : key.ExpiresUtc is { } expiresAt && expiresAt <= DateTimeOffset.UtcNow
                    ? "expired"
                    : "active";
            string expiry = key.ExpiresUtc is { } expires
                ? expires.ToUniversalTime().ToString("u", System.Globalization.CultureInfo.InvariantCulture)
                : "-";
            // Dashboard tags are operator-facing labels, not key material, so they are safe to
            // print alongside the scopes; "-" keeps the column aligned for an untagged key.
            string dashboardTags = key.Constraints.DashboardTags.Count > 0
                ? string.Join(',', key.Constraints.DashboardTags)
                : "-";
            await output.WriteLineAsync(
                    $"{key.KeyId}\t{key.DisplayName}\t{status}\t{expiry}\t{string.Join(',', key.Scopes)}\t{dashboardTags}")
                .ConfigureAwait(false);
        }
    }

    private static ApiKeyAdminListedKey ToListedKey(ApiKeyListItem key)
    {
        return new ApiKeyAdminListedKey(
            KeyId: key.KeyId,
            KeyPrefix: key.KeyPrefix,
            DisplayName: key.DisplayName,
            Scopes: key.Scopes,
            Constraints: ApiKeyConstraintSerializer.Deserialize(key.ConstraintsJson),
            CreatedUtc: key.CreatedUtc,
            LastUsedUtc: key.LastUsedUtc,
            RevokedUtc: key.RevokedUtc,
            ExpiresUtc: key.ExpiresUtc);
    }

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