using System.Text.Json;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Rotation;

namespace ZB.MOM.WW.Secrets.Cli;

/// <summary>
/// The thin, front-end-agnostic command layer behind the <c>secret</c> CLI verbs
/// (<c>set</c> / <c>get</c> / <c>list</c> / <c>rm</c> / <c>rotate</c>). Each verb is an
/// <see cref="int"/>-returning task (0 on success, non-zero on error) and writes ALL output to the
/// injected <see cref="TextWriter"/> — never to <see cref="Console"/> directly — so the layer is
/// fully testable and can be hosted under any front-end. Confirmation and listing output is JSON
/// metadata only and NEVER carries a secret value; the sole value-exposing surface is
/// <see cref="GetAsync"/>, which intentionally prints the decrypted plaintext (see its remarks).
/// </summary>
public sealed class SecretCommands
{
    private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = false };

    private readonly ISecretStore _store;
    private readonly ISecretCipher _cipher;
    private readonly ISecretResolver _resolver;
    private readonly TextWriter _output;

    /// <summary>Creates the command set over the supplied core seams and output sink.</summary>
    /// <param name="store">The encrypted store (upsert / delete / list).</param>
    /// <param name="cipher">The envelope cipher used to seal values on set / rotate.</param>
    /// <param name="resolver">The read seam used by <c>get</c> to resolve current plaintext.</param>
    /// <param name="output">Sink for all command output (JSON confirmations, listings, values).</param>
    public SecretCommands(ISecretStore store, ISecretCipher cipher, ISecretResolver resolver, TextWriter output)
    {
        _store = store ?? throw new ArgumentNullException(nameof(store));
        _cipher = cipher ?? throw new ArgumentNullException(nameof(cipher));
        _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
        _output = output ?? throw new ArgumentNullException(nameof(output));
    }

    /// <summary>
    /// set: seals <paramref name="value"/> under a fresh per-secret DEK and upserts it (create or
    /// overwrite-in-place). Prints a JSON confirmation carrying only the name and action — never the
    /// value.
    /// </summary>
    /// <param name="name">The secret name.</param>
    /// <param name="value">The plaintext to seal.</param>
    /// <param name="contentType">How consumers should interpret the plaintext.</param>
    /// <param name="description">Optional human-readable description.</param>
    /// <param name="actor">Principal recorded as created_by / updated_by, if known.</param>
    /// <param name="ct">A token to cancel the operation.</param>
    /// <returns>0 on success.</returns>
    public async Task<int> SetAsync(
        string name, string value, SecretContentType contentType, string? description, string? actor, CancellationToken ct)
    {
        var secretName = new SecretName(name);
        await UpsertSealedAsync(secretName, value, contentType, description, actor, ct).ConfigureAwait(false);
        WriteJson(new { name = secretName.Value, action = "set" });
        return 0;
    }

    /// <summary>
    /// rotate: like <see cref="SetAsync"/>, but REQUIRES the secret to already exist (and not be
    /// tombstoned). Rotating a missing secret prints a not-found error and returns non-zero without
    /// creating anything. The confirmation carries only the name and action — never the value.
    /// </summary>
    /// <param name="name">The existing secret to rotate.</param>
    /// <param name="value">The new plaintext to seal in place.</param>
    /// <param name="contentType">How consumers should interpret the plaintext.</param>
    /// <param name="description">Optional human-readable description.</param>
    /// <param name="actor">Principal recorded as updated_by, if known.</param>
    /// <param name="ct">A token to cancel the operation.</param>
    /// <returns>0 on success; non-zero if the secret does not exist.</returns>
    public async Task<int> RotateAsync(
        string name, string value, SecretContentType contentType, string? description, string? actor, CancellationToken ct)
    {
        var secretName = new SecretName(name);

        StoredSecret? existing = await _store.GetAsync(secretName, ct).ConfigureAwait(false);
        if (existing is null || existing.IsDeleted)
        {
            WriteJson(new { name = secretName.Value, error = "not-found" });
            return 1;
        }

        await UpsertSealedAsync(secretName, value, contentType, description, actor, ct).ConfigureAwait(false);
        WriteJson(new { name = secretName.Value, action = "rotate" });
        return 0;
    }

    /// <summary>
    /// get: resolves and prints the current DECRYPTED plaintext for <paramref name="name"/> to the
    /// output sink.
    /// </summary>
    /// <remarks>
    /// <b>This deliberately exposes the secret value.</b> It is the one command whose whole purpose
    /// is to surface plaintext (to stdout), so an operator can pipe or capture it. Every other
    /// command emits metadata only. A missing or tombstoned secret prints a JSON <c>not-found</c>
    /// error and returns non-zero.
    /// </remarks>
    /// <param name="name">The secret to resolve.</param>
    /// <param name="ct">A token to cancel the operation.</param>
    /// <returns>0 when the value was printed; non-zero when the secret is absent.</returns>
    public async Task<int> GetAsync(string name, CancellationToken ct)
    {
        var secretName = new SecretName(name);

        string? value = await _resolver.GetAsync(secretName, ct).ConfigureAwait(false);
        if (value is null)
        {
            WriteJson(new { error = "not-found" });
            return 1;
        }

        _output.WriteLine(value);
        return 0;
    }

    /// <summary>
    /// list: prints a JSON array of the safe metadata projection for every secret — name,
    /// description, content type, KEK id, revision, tombstone flag, and update stamps. NEVER a value.
    /// </summary>
    /// <param name="includeDeleted">When <see langword="true"/>, tombstoned rows are included.</param>
    /// <param name="ct">A token to cancel the operation.</param>
    /// <returns>0 on success.</returns>
    public async Task<int> ListAsync(bool includeDeleted, CancellationToken ct)
    {
        IReadOnlyList<SecretMetadata> items = await _store.ListAsync(includeDeleted, ct).ConfigureAwait(false);

        var projection = items.Select(m => new
        {
            name = m.Name.Value,
            description = m.Description,
            contentType = m.ContentType.ToString(),
            kekId = m.KekId,
            revision = m.Revision,
            isDeleted = m.IsDeleted,
            updatedUtc = m.UpdatedUtc,
            updatedBy = m.UpdatedBy,
        });

        WriteJson(projection);
        return 0;
    }

    /// <summary>
    /// rm: soft-deletes (tombstones) <paramref name="name"/> and prints a JSON confirmation with a
    /// <c>found</c> flag.
    /// </summary>
    /// <param name="name">The secret to tombstone.</param>
    /// <param name="actor">Principal recorded as updated_by, if known.</param>
    /// <param name="ct">A token to cancel the operation.</param>
    /// <returns>0 if a row was tombstoned; non-zero if no such row existed.</returns>
    public async Task<int> RemoveAsync(string name, string? actor, CancellationToken ct)
    {
        var secretName = new SecretName(name);

        bool removed = await _store.DeleteAsync(secretName, actor, ct).ConfigureAwait(false);
        WriteJson(new { name = secretName.Value, action = "removed", found = removed });
        return removed ? 0 : 1;
    }

    /// <summary>
    /// rewrap-all: rotates the master KEK by re-wrapping every row's DEK from <paramref name="oldKek"/>
    /// onto <paramref name="newKek"/> — bodies are never re-encrypted and no plaintext is exposed.
    /// Prints a JSON report of the counts (total / rewrapped / already-current) and NEVER any key
    /// material. Idempotent + resumable; fails closed (non-zero) on an identical-KEK mistake or a row
    /// wrapped by an unrecognized KEK.
    /// </summary>
    /// <param name="oldKek">The provider for the KEK rows are currently wrapped under.</param>
    /// <param name="newKek">The provider for the KEK rows should be re-wrapped onto.</param>
    /// <param name="ct">A token to cancel the operation.</param>
    /// <returns>0 on success; non-zero if the rotation was rejected or aborted.</returns>
    public async Task<int> RewrapAllAsync(
        IMasterKeyProvider oldKek, IMasterKeyProvider newKek, CancellationToken ct)
    {
        var service = new KekRotationService(_store, _cipher);
        try
        {
            RewrapReport report = await service.RewrapAllAsync(oldKek, newKek, ct).ConfigureAwait(false);
            WriteJson(new
            {
                action = "rewrap-all",
                total = report.Total,
                rewrapped = report.Rewrapped,
                alreadyCurrent = report.AlreadyCurrent,
            });
            return 0;
        }
        catch (ArgumentException ex)
        {
            // Identical old/new KEK ids — nothing to rotate.
            WriteJson(new { action = "rewrap-all", error = ex.Message });
            return 1;
        }
        catch (SecretDecryptionException ex)
        {
            // A row on an unrecognized KEK aborted the pass, or a DEK failed to unwrap.
            WriteJson(new { action = "rewrap-all", error = ex.Message });
            return 1;
        }
    }

    /// <summary>Seals a value under the cipher and upserts it, carrying description + actor stamps.</summary>
    private async Task UpsertSealedAsync(
        SecretName name, string value, SecretContentType contentType, string? description, string? actor, CancellationToken ct)
    {
        StoredSecret row = _cipher.Encrypt(name, value, contentType) with
        {
            Description = description,
            CreatedBy = actor,
            UpdatedBy = actor,
        };
        await _store.UpsertAsync(row, ct).ConfigureAwait(false);
    }

    /// <summary>Serializes <paramref name="value"/> to compact JSON on its own line.</summary>
    private void WriteJson(object value) => _output.WriteLine(JsonSerializer.Serialize(value, JsonOptions));
}
