using System.Globalization;
using Spectre.Console;
using ZB.MOM.WW.Secrets.Abstractions;

namespace ZB.MOM.WW.Secrets.Cli.Interactive.Flows;

/// <summary>
/// The deployment-setup flow: audits every <c>${secret:NAME}</c> reference in the target app's composed
/// configuration, renders a colour-coded status table, then walks the operator through seeding each gap so
/// the app can start. For every reference that is not already resolvable — and that <em>can</em> be seeded
/// (<see cref="ReferenceStatus.Missing"/>, <see cref="ReferenceStatus.Tombstoned"/>, or
/// <see cref="ReferenceStatus.Undecryptable"/>) — the operator is offered a masked value prompt and the
/// new secret is sealed under a fresh per-secret DEK via the session cipher. Seeding a
/// <see cref="ReferenceStatus.Tombstoned"/> reference revives it: the store's
/// <see cref="ISecretStore.UpsertAsync"/> overwrites the row in place and clears its tombstone. Seeding an
/// <see cref="ReferenceStatus.Undecryptable"/> reference overwrites the unreadable row with a fresh value
/// (the confirm text says so). An <see cref="ReferenceStatus.InvalidName"/> reference cannot be looked up
/// or seeded, so it renders a fix-the-token hint instead of a prompt;
/// <see cref="ReferenceStatus.PresentUnverified"/> only occurs on a degraded session and is unreachable
/// here (the flow requires a KEK). A closing re-audit prints a one-line count-by-status summary. Requires a
/// KEK-capable session (the shell upgrades a degraded session before dispatch); typed values are masked and
/// never echoed.
/// </summary>
public sealed class ReferenceAuditFlow : IInteractiveFlow
{
    /// <inheritdoc />
    public string Title => "Reference audit & seed";

    /// <inheritdoc />
    public bool RequiresKek => true;

    /// <inheritdoc />
    public async Task RunAsync(IAnsiConsole console, SecretsSession session, CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(console);
        ArgumentNullException.ThrowIfNull(session);

        ISecretCipher cipher = session.Cipher
            ?? throw new InvalidOperationException("flow requires an unlocked KEK session");

        var auditor = new ReferenceAuditor();
        IReadOnlyList<ReferenceFinding> findings = await auditor.AuditAsync(session, ct).ConfigureAwait(false);
        if (findings.Count == 0)
        {
            console.MarkupLine("[grey]No ${secret:} references found in the target configuration.[/]");
            return;
        }

        RenderTable(console, findings);

        foreach (ReferenceFinding finding in findings)
        {
            if (finding.Status == ReferenceStatus.InvalidName)
            {
                console.MarkupLineInterpolated(
                    $"[red]'{finding.SecretName}'[/] is not a valid secret name — fix the ${{secret:…}} token in the config; it cannot be seeded.");
                continue;
            }

            if (!IsSeedable(finding.Status))
            {
                continue; // Ok (nothing to do) or PresentUnverified (unreachable on a KEK-capable session).
            }

            string escapedName = Markup.Escape(finding.SecretName);
            string question = finding.Status == ReferenceStatus.Undecryptable
                ? $"'{escapedName}' exists but does not decrypt — overwrite with a fresh value now?"
                : $"Seed '{escapedName}' now?";

            if (!console.Prompt(new ConfirmationPrompt(question)
            {
                DefaultValue = finding.Status != ReferenceStatus.Undecryptable,
            }))
            {
                continue;
            }

            await SeedAsync(console, session, cipher, new SecretName(finding.SecretName), ct).ConfigureAwait(false);
            console.MarkupLineInterpolated($"[green]Sealed '{finding.SecretName}'.[/]");
        }

        IReadOnlyList<ReferenceFinding> after = await auditor.AuditAsync(session, ct).ConfigureAwait(false);
        console.MarkupLineInterpolated($"Audit complete: {Summarize(after)}.");
    }

    // Seedable ⇔ a valid, non-resolvable reference we can fill: absent, tombstoned, or present-but-unreadable.
    // Ok needs nothing; InvalidName cannot be looked up; PresentUnverified only occurs on a degraded session.
    private static bool IsSeedable(ReferenceStatus status) =>
        status is ReferenceStatus.Missing or ReferenceStatus.Tombstoned or ReferenceStatus.Undecryptable;

    // Prompts for a masked value + content type + optional description and seals the secret under a fresh
    // per-secret DEK. Mirrors SetSecretFlow's seal-and-stamp shape without coupling to it.
    private static async Task SeedAsync(
        IAnsiConsole console, SecretsSession session, ISecretCipher cipher, SecretName name, CancellationToken ct)
    {
        string value = console.Prompt(
            new TextPrompt<string>($"  Value for [green]{Markup.Escape(name.Value)}[/]:").Secret());
        SecretContentType contentType = console.Prompt(
            new SelectionPrompt<SecretContentType>().Title("  Content type").AddChoices(Enum.GetValues<SecretContentType>()));
        string description = console.Prompt(
            new TextPrompt<string>("  Description [grey](optional)[/]:").AllowEmpty());

        StoredSecret row = cipher.Encrypt(name, value, contentType) with
        {
            Description = string.IsNullOrWhiteSpace(description) ? null : description,
            CreatedBy = FlowPrompts.Actor,
            UpdatedBy = FlowPrompts.Actor,
        };
        await session.Store.UpsertAsync(row, ct).ConfigureAwait(false); // revives a tombstone: overwrite clears IsDeleted.
    }

    // A colour-coded audit table: green Ok, yellow PresentUnverified, red for every unresolvable status.
    private static void RenderTable(IAnsiConsole console, IReadOnlyList<ReferenceFinding> findings)
    {
        var table = new Table()
            .AddColumn("Secret")
            .AddColumn("Status")
            .AddColumn("Config paths");

        foreach (ReferenceFinding finding in findings)
        {
            table.AddRow(
                Markup.Escape(finding.SecretName),
                $"[{StatusColor(finding.Status)}]{finding.Status}[/]",
                Markup.Escape(string.Join(Environment.NewLine, finding.ConfigPaths)));
        }

        console.Write(table);
    }

    private static string StatusColor(ReferenceStatus status) => status switch
    {
        ReferenceStatus.Ok => "green",
        ReferenceStatus.PresentUnverified => "yellow",
        _ => "red", // Missing / Tombstoned / Undecryptable / InvalidName
    };

    // A deterministic "N Status" count-by-status roll-up, ordered by the status enum.
    private static string Summarize(IReadOnlyList<ReferenceFinding> findings) =>
        string.Join(", ", findings
            .GroupBy(f => f.Status)
            .OrderBy(g => g.Key)
            .Select(g => string.Create(CultureInfo.InvariantCulture, $"{g.Count()} {g.Key}")));
}
