using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
using ZB.MOM.WW.Auth.ApiKeys.Sqlite;

namespace ZB.MOM.WW.Auth.ApiKeys.Admin;

/// <summary>
/// Result of a verb that yields a freshly assembled token (create-key / rotate-key).
/// The <see cref="Token"/> is the ONLY moment the secret is ever available; it is never
/// retrievable afterwards. A <c>null</c> <see cref="Token"/> indicates the verb failed
/// (for example, rotating a key that does not exist).
/// </summary>
public sealed record CreateKeyResult(string KeyId, string? Token);

/// <summary>Result of a mutating verb that succeeds or fails without yielding a token.</summary>
public sealed record KeyActionResult(bool Succeeded, string? Message);

/// <summary>
/// Reusable, front-end-agnostic API-key administration command set. Each verb returns a
/// structured result and performs no console I/O, so consumers can wire their own CLI or HTTP
/// front-end on top. Audit is wired here (the command layer): every mutating verb appends an
/// <see cref="ApiKeyAuditEntry"/> via <see cref="IApiKeyAuditStore"/>.
/// </summary>
/// <remarks>
/// <para>
/// <c>create-key</c> and <c>rotate-key</c> return the assembled token EXACTLY ONCE — the only
/// time the secret is ever available. No other result carries the secret or its hash;
/// <see cref="ApiKeyListItem"/> is a hash-free projection by construction.
/// </para>
/// </remarks>
public sealed class ApiKeyAdminCommands
{
    private readonly ApiKeyOptions _options;
    private readonly IApiKeyAdminStore _adminStore;
    private readonly IApiKeyAuditStore _auditStore;
    private readonly IApiKeyPepperProvider _pepperProvider;
    private readonly SqliteAuthStoreMigrator _migrator;
    private readonly TimeProvider _clock;

    /// <summary>Creates the command set over the supplied stores and options.</summary>
    /// <param name="options">API-key options (token prefix, store path, ...).</param>
    /// <param name="adminStore">Mutating store (create / revoke / rotate / delete / list).</param>
    /// <param name="auditStore">Append-only audit store wired into every mutating verb.</param>
    /// <param name="pepperProvider">Resolves the pepper used to hash secrets.</param>
    /// <param name="migrator">Schema migrator used by <see cref="InitDbAsync"/>.</param>
    /// <param name="clock">Optional clock; defaults to <see cref="TimeProvider.System"/>.</param>
    public ApiKeyAdminCommands(
        ApiKeyOptions options,
        IApiKeyAdminStore adminStore,
        IApiKeyAuditStore auditStore,
        IApiKeyPepperProvider pepperProvider,
        SqliteAuthStoreMigrator migrator,
        TimeProvider? clock = null)
    {
        ArgumentNullException.ThrowIfNull(options);
        ArgumentNullException.ThrowIfNull(adminStore);
        ArgumentNullException.ThrowIfNull(auditStore);
        ArgumentNullException.ThrowIfNull(pepperProvider);
        ArgumentNullException.ThrowIfNull(migrator);

        _options = options;
        _adminStore = adminStore;
        _auditStore = auditStore;
        _pepperProvider = pepperProvider;
        _migrator = migrator;
        _clock = clock ?? TimeProvider.System;
    }

    /// <summary>
    /// init-db: applies the schema migration, then appends an <c>init-db</c> audit entry.
    /// </summary>
    public async Task InitDbAsync(string? remoteAddress, CancellationToken ct)
    {
        await _migrator.MigrateAsync(ct).ConfigureAwait(false);
        await AppendAuditAsync(keyId: null, "init-db", remoteAddress, details: null, ct).ConfigureAwait(false);
    }

    /// <summary>
    /// create-key: generates a secret, persists its hash, appends a <c>create-key</c> audit entry,
    /// and returns the assembled token <c>&lt;prefix&gt;_&lt;keyId&gt;_&lt;secret&gt;</c> EXACTLY ONCE.
    /// </summary>
    /// <exception cref="InvalidOperationException">The pepper is unavailable; nothing is persisted or audited.</exception>
    public Task<CreateKeyResult> CreateKeyAsync(
        string keyId,
        string displayName,
        IReadOnlySet<string> scopes,
        string? constraintsJson,
        string? remoteAddress,
        CancellationToken ct) =>
        CreateKeyAsync(keyId, displayName, scopes, constraintsJson, expiresUtc: null, remoteAddress, ct);

    /// <summary>
    /// create-key with an optional absolute expiry. A non-null <paramref name="expiresUtc"/> is
    /// persisted on the record and enforced by <see cref="ApiKeyVerifier"/>; <c>null</c> means the
    /// key never expires (the same behaviour as the expiry-less overload).
    /// </summary>
    /// <exception cref="InvalidOperationException">The pepper is unavailable; nothing is persisted or audited.</exception>
    public async Task<CreateKeyResult> CreateKeyAsync(
        string keyId,
        string displayName,
        IReadOnlySet<string> scopes,
        string? constraintsJson,
        DateTimeOffset? expiresUtc,
        string? remoteAddress,
        CancellationToken ct)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
        if (keyId.Contains('_'))
            throw new ArgumentException("keyId must not contain '_'.", nameof(keyId));
        ArgumentException.ThrowIfNullOrWhiteSpace(displayName);
        ArgumentNullException.ThrowIfNull(scopes);

        string pepper = RequirePepper();

        string secret = ApiKeySecretGenerator.NewSecret();
        byte[] secretHash = ApiKeySecretHasher.Hash(secret, pepper);
        DateTimeOffset now = _clock.GetUtcNow();

        var record = new ApiKeyRecord(
            KeyId: keyId,
            // KeyPrefix is the bare token prefix (e.g. "mxgw"), NOT prefix_keyId — the key id is
            // already its own column. Embedding it here produced a self-referential value that
            // confused admin tooling and disagreed with the read/test paths (see Auth-005).
            KeyPrefix: _options.TokenPrefix,
            SecretHash: secretHash,
            DisplayName: displayName,
            Scopes: scopes,
            ConstraintsJson: constraintsJson,
            CreatedUtc: now,
            LastUsedUtc: null,
            RevokedUtc: null,
            ExpiresUtc: expiresUtc);

        await _adminStore.CreateAsync(record, ct).ConfigureAwait(false);
        await AppendAuditAsync(keyId, "create-key", remoteAddress, details: null, ct).ConfigureAwait(false);

        return new CreateKeyResult(keyId, AssembleToken(keyId, secret));
    }

    /// <summary>
    /// list-keys: returns the hash-free <see cref="ApiKeyListItem"/> projection of every key,
    /// newest first. This is a read, so it appends no audit entry and never carries secret material.
    /// </summary>
    public Task<IReadOnlyList<ApiKeyListItem>> ListKeysAsync(CancellationToken ct) =>
        _adminStore.ListAsync(ct);

    /// <summary>
    /// revoke-key: marks the key revoked and appends a <c>revoke-key</c> audit entry.
    /// All attempts are audited, including failures (key not found or already revoked) — this is
    /// intentional to maintain a complete security trail.
    /// </summary>
    public async Task<KeyActionResult> RevokeKeyAsync(string keyId, string? remoteAddress, CancellationToken ct)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(keyId);

        DateTimeOffset now = _clock.GetUtcNow();
        bool revoked = await _adminStore.RevokeAsync(keyId, now, ct).ConfigureAwait(false);

        string status = revoked ? "revoked" : "not-found-or-already-revoked";
        await AppendAuditAsync(keyId, "revoke-key", remoteAddress, status, ct).ConfigureAwait(false);

        return new KeyActionResult(revoked, status);
    }

    /// <summary>
    /// rotate-key: replaces the stored secret with a freshly generated one and appends a
    /// <c>rotate-key</c> audit entry. Returns a <see cref="CreateKeyResult"/> whose token is the new
    /// secret (shown once); a <c>null</c> token indicates the key did not exist.
    /// All attempts are audited, including failures (key not found) — this is intentional to
    /// maintain a complete security trail.
    /// </summary>
    /// <remarks>
    /// Rotation replaces only the secret (and clears revoked/last-used); it deliberately leaves any
    /// existing <see cref="ApiKeyRecord.ExpiresUtc"/> untouched, so a key's expiry survives a secret
    /// rotation. Changing a key's expiry is a create-time concern (use <see cref="CreateKeyAsync(string,string,IReadOnlySet{string},string?,DateTimeOffset?,string?,CancellationToken)"/>).
    /// </remarks>
    /// <exception cref="InvalidOperationException">The pepper is unavailable; nothing is persisted or audited.</exception>
    public async Task<CreateKeyResult> RotateKeyAsync(string keyId, string? remoteAddress, CancellationToken ct)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(keyId);

        string pepper = RequirePepper();

        string secret = ApiKeySecretGenerator.NewSecret();
        byte[] newHash = ApiKeySecretHasher.Hash(secret, pepper);

        bool rotated = await _adminStore.RotateAsync(keyId, newHash, ct).ConfigureAwait(false);

        string status = rotated ? "rotated" : "not-found";
        await AppendAuditAsync(keyId, "rotate-key", remoteAddress, status, ct).ConfigureAwait(false);

        return new CreateKeyResult(keyId, rotated ? AssembleToken(keyId, secret) : null);
    }

    /// <summary>
    /// delete-key: removes the key (only succeeds once it has been revoked) and appends a
    /// <c>delete-key</c> audit entry.
    /// All attempts are audited, including failures (key not found or not yet revoked) — this is
    /// intentional to maintain a complete security trail.
    /// </summary>
    public async Task<KeyActionResult> DeleteKeyAsync(string keyId, string? remoteAddress, CancellationToken ct)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(keyId);

        bool deleted = await _adminStore.DeleteAsync(keyId, ct).ConfigureAwait(false);

        string status = deleted ? "deleted" : "not-found-or-not-revoked";
        await AppendAuditAsync(keyId, "delete-key", remoteAddress, status, ct).ConfigureAwait(false);

        return new KeyActionResult(deleted, status);
    }

    /// <summary>
    /// set-scopes: replaces the scope set on an existing key WITHOUT touching its secret, and
    /// appends a <c>set-scopes</c> audit entry. Only the scope count is recorded in the audit
    /// details — the scope values themselves are not logged verbatim.
    /// All attempts are audited, including failures (key not found) — this is intentional to
    /// maintain a complete security trail.
    /// </summary>
    public async Task<KeyActionResult> SetScopesAsync(
        string keyId, IReadOnlySet<string> scopes, string? remoteAddress, CancellationToken ct)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
        ArgumentNullException.ThrowIfNull(scopes);

        bool updated = await _adminStore.SetScopesAsync(keyId, scopes, ct).ConfigureAwait(false);

        string status = updated ? "scopes-set" : "not-found";
        // Record only the count, never the scope contents, to avoid leaking authority detail into audit.
        await AppendAuditAsync(keyId, "set-scopes", remoteAddress, $"{status}; count={scopes.Count}", ct)
            .ConfigureAwait(false);

        return new KeyActionResult(updated, status);
    }

    /// <summary>
    /// enable-key / disable-key: reversibly toggles a key's active state WITHOUT changing its
    /// secret, and appends an <c>enable-key</c> (when enabling) or <c>disable-key</c> (when
    /// disabling) audit entry.
    /// All attempts are audited, including failures (key not found) — this is intentional to
    /// maintain a complete security trail.
    /// </summary>
    public async Task<KeyActionResult> SetEnabledAsync(
        string keyId, bool enabled, string? remoteAddress, CancellationToken ct)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(keyId);

        DateTimeOffset now = _clock.GetUtcNow();
        bool updated = await _adminStore.SetEnabledAsync(keyId, enabled, now, ct).ConfigureAwait(false);

        string eventType = enabled ? "enable-key" : "disable-key";
        string status = updated
            ? (enabled ? "enabled" : "disabled")
            : "not-found";
        await AppendAuditAsync(keyId, eventType, remoteAddress, status, ct).ConfigureAwait(false);

        return new KeyActionResult(updated, status);
    }

    private string RequirePepper()
    {
        string? pepper = _pepperProvider.GetPepper();
        if (string.IsNullOrWhiteSpace(pepper))
        {
            throw new InvalidOperationException("pepper unavailable");
        }

        return pepper;
    }

    private string AssembleToken(string keyId, string secret) =>
        $"{_options.TokenPrefix}_{keyId}_{secret}";

    private Task AppendAuditAsync(
        string? keyId, string eventType, string? remoteAddress, string? details, CancellationToken ct) =>
        _auditStore.AppendAsync(
            new ApiKeyAuditEntry(keyId, eventType, remoteAddress, _clock.GetUtcNow(), details),
            ct);
}
