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

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

/// <summary>
/// Imports a ciphertext-only <see cref="SecretBundle"/> into the session's store. The bundle is read and
/// parsed <b>before</b> any write so its <see cref="SecretBundle.SourceKekId"/> can be inspected: when it
/// differs from the session KEK the operator is asked to supply the source key (paste / env var / key
/// file) so the rows can be re-wrapped under the session KEK on import — otherwise foreign-KEK rows are
/// skipped and reported. The operator may opt into a per-conflict prompt (default off = pure
/// last-writer-wins); when on, each name collision renders both rows' timestamp/revision and asks whether
/// to take the bundle row. A tally table (Imported / Skipped / Conflicts) closes the flow. Requires a
/// KEK-capable session (the shell upgrades a degraded session before dispatch); a pasted key is masked.
/// </summary>
public sealed class BundleImportFlow : IInteractiveFlow
{
    private const string PasteKeyLabel = "Paste base64 key";
    private const string EnvVarLabel = "Environment variable";
    private const string KeyFileLabel = "Key file path";
    private const string CancelLabel = "Cancel";

    /// <inheritdoc />
    public string Title => "Import bundle";

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

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

        string sessionKekId = session.MasterKey?.KekId
            ?? throw new InvalidOperationException("flow requires an unlocked KEK session");

        string path = console.Prompt(new TextPrompt<string>("Bundle [green]path[/]:"));
        if (!File.Exists(path))
        {
            console.MarkupLineInterpolated($"[red]No bundle found at '{path}'.[/]");
            return;
        }

        // Read + parse FIRST so the format version and source KEK can be inspected before any write —
        // or any prompting — is attempted.
        SecretBundle bundle;
        try
        {
            string json = await File.ReadAllTextAsync(path, ct).ConfigureAwait(false);
            bundle = SecretBundleCodec.Deserialize(json);
        }
        catch (Exception ex) when (ex is InvalidOperationException or JsonException or IOException or UnauthorizedAccessException)
        {
            console.MarkupLineInterpolated($"[red]Could not read bundle '{path}': {ex.Message}[/]");
            return;
        }

        // Format gate up front: reject an unreadable version before consuming any operator input. The
        // service applies the same gate as defense-in-depth, but failing here keeps the flow honest.
        if (bundle.FormatVersion != SecretBundle.CurrentFormatVersion)
        {
            console.MarkupLineInterpolated(
                $"[red]Bundle '{path}' has unsupported format version {bundle.FormatVersion}; this build supports version {SecretBundle.CurrentFormatVersion}.[/]");
            return;
        }

        // Foreign-KEK detection: a bundle exported under another KEK must be re-wrapped, which needs the
        // source key. Prompt for it up front; without it every row would be skipped as foreign-KEK.
        IMasterKeyProvider? sourceKek = null;
        if (!string.Equals(bundle.SourceKekId, sessionKekId, StringComparison.Ordinal))
        {
            console.MarkupLineInterpolated(
                $"[yellow]This bundle was exported under KEK '{bundle.SourceKekId}', but this session uses '{sessionKekId}'.[/]");
            console.MarkupLine(
                "Supply the bundle's source key so its rows can be re-wrapped under this session's KEK.");

            sourceKek = PromptSourceKey(console);
            if (sourceKek is null)
            {
                console.MarkupLine("[yellow]Import cancelled.[/]");
                return;
            }

            // The first KekId read is where an env/file source actually resolves the key: it lazily throws
            // MasterKeyUnavailableException (an unset var, a missing file) or, for a file source, a raw
            // IOException/UnauthorizedAccessException straight out of File.ReadAllBytes (e.g. a
            // permission-denied key file), since FileMasterKeyProvider does not wrap those. All must render
            // a friendly line and return here — not escape to the shell.
            string sourceKekId;
            try
            {
                sourceKekId = sourceKek.KekId;
            }
            catch (Exception ex) when (ex is MasterKeyUnavailableException or IOException or UnauthorizedAccessException)
            {
                console.MarkupLineInterpolated($"[red]Source KEK unavailable:[/] {ex.Message}");
                return;
            }

            // A valid-but-wrong key would otherwise silently land every row in SkippedForeignKek. Verify
            // the supplied key actually derives the bundle's source KEK id before importing.
            if (!string.Equals(sourceKekId, bundle.SourceKekId, StringComparison.Ordinal))
            {
                console.MarkupLineInterpolated(
                    $"[red]That key does not match this bundle's source KEK ('{bundle.SourceKekId}' expected, got '{sourceKekId}').[/]");
                return;
            }
        }

        bool perConflict = console.Prompt(
            new ConfirmationPrompt("Prompt per conflict? [grey](otherwise last-writer-wins)[/]")
            {
                DefaultValue = false,
            });

        Func<StoredSecret, StoredSecret, bool>? conflictOverride =
            perConflict ? (existing, incoming) => PromptConflict(console, existing, incoming) : null;

        BundleImportReport report;
        try
        {
            report = await new BundleService()
                .ImportAsync(session, path, sourceKek, conflictOverride, ct).ConfigureAwait(false);
        }
        catch (InvalidOperationException ex)
        {
            console.MarkupLineInterpolated($"[red]Import failed: {ex.Message}[/]");
            return;
        }

        RenderReport(console, report, bundle.SourceKekId);
    }

    // Builds the source-KEK provider the operator selects: a pasted base64 key (masked), an environment
    // variable, or a key file. Cancel (or a bad key) returns null and the caller aborts the import.
    private static IMasterKeyProvider? PromptSourceKey(IAnsiConsole console)
    {
        string choice;
        try
        {
            choice = console.Prompt(new SelectionPrompt<string>()
                .Title("Provide the bundle's source key")
                .AddChoices(PasteKeyLabel, EnvVarLabel, KeyFileLabel, CancelLabel));
        }
        catch (OperationCanceledException)
        {
            return null;
        }

        if (choice == CancelLabel)
        {
            return null;
        }

        try
        {
            switch (choice)
            {
                case PasteKeyLabel:
                    string base64 = console.Prompt(new TextPrompt<string>("Paste base64 source KEK:").Secret());
                    return new LiteralMasterKeyProvider(base64);

                case EnvVarLabel:
                    string envVar = console.Prompt(new TextPrompt<string>("Source-key environment variable name:"));
                    return MasterKeyProviderFactory.Create(new MasterKeyOptions
                    {
                        Source = MasterKeySource.Environment,
                        EnvVarName = envVar,
                    });

                default:
                    string filePath = console.Prompt(new TextPrompt<string>("Source-key file path:"));
                    return MasterKeyProviderFactory.Create(new MasterKeyOptions
                    {
                        Source = MasterKeySource.File,
                        FilePath = filePath,
                    });
            }
        }
        catch (ArgumentException ex)
        {
            // LiteralMasterKeyProvider rejects a bad paste eagerly; name the problem and abort.
            console.MarkupLineInterpolated($"[red]Invalid source key: {ex.Message}[/]");
            return null;
        }
    }

    // Renders a name collision — both rows' last-update timestamp and revision — and asks the operator
    // whether to take the incoming bundle row (true) or keep the local one (false).
    private static bool PromptConflict(IAnsiConsole console, StoredSecret existing, StoredSecret incoming)
    {
        console.MarkupLineInterpolated($"[yellow]Conflict on '{incoming.Name.Value}':[/]");
        console.MarkupLineInterpolated(
            $"  local:  updated {existing.UpdatedUtc.ToString("O", CultureInfo.InvariantCulture)}  revision {existing.Revision}");
        console.MarkupLineInterpolated(
            $"  bundle: updated {incoming.UpdatedUtc.ToString("O", CultureInfo.InvariantCulture)}  revision {incoming.Revision}");
        return console.Prompt(new ConfirmationPrompt("  Take the bundle row?") { DefaultValue = false });
    }

    // A compact tally table of the import outcome. When rows were skipped as foreign-KEK, the bundle's
    // source KEK id is appended so a wrong-key paste is self-diagnosable from the report alone.
    private static void RenderReport(IAnsiConsole console, BundleImportReport report, string sourceKekId)
    {
        var table = new Table()
            .AddColumn("Result")
            .AddColumn("Rows");

        string foreign = report.SkippedForeignKek.ToString(CultureInfo.InvariantCulture);
        if (report.SkippedForeignKek > 0)
        {
            foreign += $" (source KEK {Markup.Escape(sourceKekId)})";
        }

        table.AddRow("Imported", report.Imported.ToString(CultureInfo.InvariantCulture));
        table.AddRow("Skipped (older)", report.SkippedOlder.ToString(CultureInfo.InvariantCulture));
        table.AddRow("Skipped (foreign KEK)", foreign);
        table.AddRow("Conflicts", report.Conflicts.ToString(CultureInfo.InvariantCulture));

        console.Write(table);
    }
}
