using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Spectre.Console;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Cli;
using ZB.MOM.WW.Secrets.Cli.Interactive;
using ZB.MOM.WW.Secrets.DependencyInjection;
using ZB.MOM.WW.Secrets.MasterKey;

// Headless `secret` CLI: set / get / list / rm / rotate over the ZB.MOM.WW secrets store.
// The heavy lifting lives in SecretCommands; Program only builds the host, runs the schema
// migration, parses the verb + positional args, and dispatches.

// A bare `secret` on a real TTY launches the interactive console, which builds its OWN per-target
// sessions — so this must run BEFORE the host build + schema migration below, which the headless
// verbs (and only they) require against the CLI's own appsettings-configured store and KEK.
if (InteractiveEntry.ShouldEnterInteractive(args, Console.IsInputRedirected, Console.IsOutputRedirected))
    return await InteractiveEntry.CreateShell(AnsiConsole.Console).RunAsync(CancellationToken.None);

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Configuration
    .AddJsonFile("appsettings.json", optional: true)
    .AddEnvironmentVariables();
builder.Services.AddZbSecrets(builder.Configuration, "Secrets");

using IHost host = builder.Build();

// Ensure the schema exists before any store operation. Resolved through the ISecretsStoreMigrator
// seam so the CLI provisions whichever store the app configured (local SQLite or shared SQL Server).
await host.Services.GetRequiredService<ISecretsStoreMigrator>()
    .MigrateAsync(CancellationToken.None);

var commands = new SecretCommands(
    host.Services.GetRequiredService<ISecretStore>(),
    host.Services.GetRequiredService<ISecretCipher>(),
    host.Services.GetRequiredService<ISecretResolver>(),
    Console.Out);

string actor = string.IsNullOrWhiteSpace(Environment.UserName) ? "cli" : Environment.UserName;
CancellationToken ct = CancellationToken.None;

if (args.Length == 0)
{
    return Usage();
}

try
{
    switch (args[0])
    {
        case "set":
        {
            if (args.Length < 3)
                return Usage("set requires <name> and <value>.");
            (SecretContentType contentType, string? description) = ParseSetOptions(args, startIndex: 3);
            return await commands.SetAsync(args[1], args[2], contentType, description, actor, ct);
        }

        case "rotate":
        {
            if (args.Length < 3)
                return Usage("rotate requires <name> and <value>.");
            (SecretContentType contentType, string? description) = ParseSetOptions(args, startIndex: 3);
            return await commands.RotateAsync(args[1], args[2], contentType, description, actor, ct);
        }

        case "get":
        {
            if (args.Length < 2)
                return Usage("get requires <name>.");
            return await commands.GetAsync(args[1], ct);
        }

        case "list":
        {
            bool includeDeleted = args.Skip(1).Any(a =>
                string.Equals(a, "--include-deleted", StringComparison.Ordinal));
            return await commands.ListAsync(includeDeleted, ct);
        }

        case "rm":
        {
            if (args.Length < 2)
                return Usage("rm requires <name>.");
            return await commands.RemoveAsync(args[1], actor, ct);
        }

        case "rewrap-all":
        {
            // Rotate the master KEK by re-wrapping every row's DEK. Key material is supplied ONLY by
            // env-var NAME or file PATH — never as a literal argument (which would be echoed/logged).
            string? oldKind = null, oldValue = null, oldKekId = null;
            string? newKind = null, newValue = null, newKekId = null;

            for (int i = 1; i < args.Length; i++)
            {
                switch (args[i])
                {
                    case "--old-key-env": (oldKind, oldValue) = ("env", RequireValue(args, ref i)); break;
                    case "--old-key-file": (oldKind, oldValue) = ("file", RequireValue(args, ref i)); break;
                    case "--old-key-id": oldKekId = RequireValue(args, ref i); break;
                    case "--new-key-env": (newKind, newValue) = ("env", RequireValue(args, ref i)); break;
                    case "--new-key-file": (newKind, newValue) = ("file", RequireValue(args, ref i)); break;
                    case "--new-key-id": newKekId = RequireValue(args, ref i); break;
                    default: return Usage($"Unknown option '{args[i]}'.");
                }
            }

            if (oldKind is null)
                return Usage("rewrap-all requires --old-key-env <VAR> or --old-key-file <path>.");
            if (newKind is null && newKekId is not null)
                return Usage("--new-key-id requires --new-key-env <VAR> or --new-key-file <path>.");

            IMasterKeyProvider oldKek = BuildProvider(oldKind, oldValue!, oldKekId);
            // Default the new KEK to the app's configured provider (Secrets:MasterKey); override with flags.
            IMasterKeyProvider newKek = newKind is null
                ? host.Services.GetRequiredService<IMasterKeyProvider>()
                : BuildProvider(newKind, newValue!, newKekId);

            return await commands.RewrapAllAsync(oldKek, newKek, ct);
        }

        default:
            return Usage($"Unknown command '{args[0]}'.");
    }
}
catch (ArgumentException ex)
{
    // Invalid secret name, bad --content-type value, unknown option: report without a stack trace.
    Console.Error.WriteLine(ex.Message);
    return 2;
}

// --- helpers ---------------------------------------------------------------

// Parses the optional flags shared by `set` and `rotate`:
//   [--content-type text|connection-string|json|binary-base64] [--description "..."]
static (SecretContentType contentType, string? description) ParseSetOptions(string[] args, int startIndex)
{
    SecretContentType contentType = SecretContentType.Text;
    string? description = null;

    for (int i = startIndex; i < args.Length; i++)
    {
        switch (args[i])
        {
            case "--content-type":
                if (i + 1 >= args.Length)
                    throw new ArgumentException("--content-type requires a value.");
                contentType = ParseContentType(args[++i]);
                break;

            case "--description":
                if (i + 1 >= args.Length)
                    throw new ArgumentException("--description requires a value.");
                description = args[++i];
                break;

            default:
                throw new ArgumentException($"Unknown option '{args[i]}'.");
        }
    }

    return (contentType, description);
}

// Maps the CLI content-type token (kebab-case) to the enum.
static SecretContentType ParseContentType(string value) => value switch
{
    "text" => SecretContentType.Text,
    "connection-string" => SecretContentType.ConnectionString,
    "json" => SecretContentType.Json,
    "binary-base64" => SecretContentType.BinaryBase64,
    _ => throw new ArgumentException(
        $"Unknown --content-type '{value}'; expected text|connection-string|json|binary-base64."),
};

// Reads the value that must follow an option flag at args[i], advancing i past it.
static string RequireValue(string[] args, ref int i)
{
    if (i + 1 >= args.Length)
        throw new ArgumentException($"{args[i]} requires a value.");
    return args[++i];
}

// Builds a master-key provider from a source kind ("env" | "file") + its env-var name or file path,
// with an optional explicit KEK id (needed when the app configured Secrets:MasterKey:KekId rather
// than the derived sha256: id). The key VALUE itself is only ever read from the env var / file.
static IMasterKeyProvider BuildProvider(string kind, string value, string? kekId) => kind switch
{
    "env" => MasterKeyProviderFactory.Create(
        new MasterKeyOptions { Source = MasterKeySource.Environment, EnvVarName = value, KekId = kekId }),
    "file" => MasterKeyProviderFactory.Create(
        new MasterKeyOptions { Source = MasterKeySource.File, FilePath = value, KekId = kekId }),
    _ => throw new ArgumentException($"Unknown key source '{kind}'."),
};

// Prints usage (optionally preceded by an error line) and returns the standard usage exit code.
int Usage(string? error = null)
{
    if (error is not null)
        Console.Error.WriteLine(error);

    Console.Error.WriteLine(
        """
        Usage: secret <command> [args]

          set <name> <value> [--content-type text|connection-string|json|binary-base64] [--description "..."]
          rotate <name> <value> [--content-type ...] [--description "..."]
          get <name>
          list [--include-deleted]
          rm <name>
          rewrap-all --old-key-env <VAR>|--old-key-file <path> [--old-key-id <id>]
                     [--new-key-env <VAR>|--new-key-file <path>] [--new-key-id <id>]

        rewrap-all rotates the master KEK: it re-wraps every stored secret's data key from the old
        KEK to the new one (bodies are never re-encrypted, no plaintext is exposed). The new KEK
        defaults to the configured Secrets:MasterKey provider. Supply key material only by env-var
        NAME or file PATH — never as a literal. Run it with resolve traffic quiesced; it is
        idempotent and safe to re-run.

        Run `secret` with no arguments at an interactive terminal to open the interactive
        console (deployment seeding, KEK-lockout recovery, ciphertext bundles).
        """);
    return 2;
}
