using System.CommandLine;
using System.CommandLine.Parsing;
using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;

namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;

/// <summary>
/// Transport (#24) bundle export / preview / import. The bundle bytes travel
/// through the management endpoint as base64 inside the standard JSON envelope
/// so no transport plumbing diverges from the other commands; the CLI handles
/// file I/O at the edges.
/// </summary>
public static class BundleCommands
{
    private static readonly TimeSpan BundleCommandTimeout = TimeSpan.FromMinutes(5);

    /// <summary>Builds the <c>bundle</c> command group with export, preview, and import sub-commands.</summary>
    /// <param name="urlOption">Shared management URL option.</param>
    /// <param name="formatOption">Shared output format option.</param>
    /// <param name="usernameOption">Shared username option.</param>
    /// <param name="passwordOption">Shared password option.</param>
    /// <returns>The configured <see cref="Command"/> for the bundle group.</returns>
    public static Command Build(
        Option<string> urlOption, Option<string> formatOption,
        Option<string> usernameOption, Option<string> passwordOption)
    {
        var command = new Command("bundle")
        {
            Description = "Export, preview, and import Transport bundles",
        };
        command.Add(BuildExport(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildPreview(urlOption, formatOption, usernameOption, passwordOption));
        command.Add(BuildImport(urlOption, formatOption, usernameOption, passwordOption));
        return command;
    }

    // ====================================================================
    // bundle export
    // ====================================================================
    private static Command BuildExport(
        Option<string> urlOption, Option<string> formatOption,
        Option<string> usernameOption, Option<string> passwordOption)
    {
        var outputOption = new Option<string>("--output")
        {
            Description = "Output file path (.scadabundle)",
            Required = true,
        };
        var passphraseOption = new Option<string?>("--passphrase")
        {
            Description = "Encryption passphrase. Omit to produce an unencrypted bundle.",
        };
        var allOption = new Option<bool>("--all")
        {
            Description = "Export every entity of every supported type (ignores per-type name flags).",
        };
        var templatesOption = NameListOption("--templates", "Comma-separated template names");
        var sharedScriptsOption = NameListOption("--shared-scripts", "Comma-separated shared-script names");
        var externalSystemsOption = NameListOption("--external-systems", "Comma-separated external-system names");
        var dbConnectionsOption = NameListOption("--db-connections", "Comma-separated database-connection names");
        var notificationListsOption = NameListOption("--notification-lists", "Comma-separated notification-list names");
        var smtpConfigsOption = NameListOption("--smtp-configs", "Comma-separated SMTP host names");
        // Inbound API keys are not transported between environments (re-arch C4) — no
        // --api-keys option. Re-create keys and re-grant their method scopes on the
        // destination via the admin UI/CLI.
        var apiMethodsOption = NameListOption("--api-methods", "Comma-separated API-method names");
        var includeDepsOption = new Option<bool>("--include-dependencies")
        {
            Description = "Pull transitive dependencies (referenced shared scripts, parents, composed members) into the bundle.",
        };
        var sourceEnvOption = new Option<string?>("--source-environment")
        {
            Description = "SourceEnvironment value stamped into the bundle manifest. Defaults to 'cli'.",
        };

        var cmd = new Command("export")
        {
            Description = "Export a bundle to a file",
        };
        cmd.Add(outputOption);
        cmd.Add(passphraseOption);
        cmd.Add(allOption);
        cmd.Add(templatesOption);
        cmd.Add(sharedScriptsOption);
        cmd.Add(externalSystemsOption);
        cmd.Add(dbConnectionsOption);
        cmd.Add(notificationListsOption);
        cmd.Add(smtpConfigsOption);
        cmd.Add(apiMethodsOption);
        cmd.Add(includeDepsOption);
        cmd.Add(sourceEnvOption);

        cmd.SetAction(async (ParseResult result) =>
        {
            var output = result.GetValue(outputOption)!;
            var passphrase = result.GetValue(passphraseOption);
            var all = result.GetValue(allOption);
            var includeDeps = result.GetValue(includeDepsOption);
            var sourceEnv = result.GetValue(sourceEnvOption) ?? "cli";

            var payload = new ExportBundleCommand(
                All: all,
                TemplateNames: result.GetValue(templatesOption),
                SharedScriptNames: result.GetValue(sharedScriptsOption),
                ExternalSystemNames: result.GetValue(externalSystemsOption),
                DatabaseConnectionNames: result.GetValue(dbConnectionsOption),
                NotificationListNames: result.GetValue(notificationListsOption),
                SmtpConfigurationNames: result.GetValue(smtpConfigsOption),
                ApiMethodNames: result.GetValue(apiMethodsOption),
                IncludeDependencies: includeDeps,
                Passphrase: passphrase,
                SourceEnvironment: sourceEnv);

            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                payload,
                timeout: BundleCommandTimeout,
                onSuccess: jsonOk =>
                {
                    // CLI-020: previously the JSON envelope parse + property extraction +
                    // base64 decode all ran unguarded — a server-side bug that omits one of
                    // the two expected properties, returns a null base64 value, sends invalid
                    // base64, or returns a malformed JSON envelope would surface as one of
                    // KeyNotFoundException / InvalidOperationException / FormatException /
                    // JsonException, i.e. an unhandled stack trace rather than the
                    // documented "exit 1 with a clean INVALID_RESPONSE error". Wrap the
                    // envelope parse and the streamed write in a single try/catch matching
                    // the graceful-degradation theme established by CLI-002 / CLI-003 / CLI-005.
                    string base64;
                    int byteCount;
                    try
                    {
                        using var doc = JsonDocument.Parse(jsonOk);
                        base64 = doc.RootElement.GetProperty("base64Bundle").GetString()!;
                        byteCount = doc.RootElement.GetProperty("byteCount").GetInt32();
                    }
                    catch (Exception ex) when (ex is JsonException
                                              or KeyNotFoundException
                                              or InvalidOperationException)
                    {
                        OutputFormatter.WriteError(
                            $"Server returned a malformed bundle-export response: {ex.Message}",
                            "INVALID_RESPONSE");
                        return 1;
                    }

                    // CLI-019: stream the base64 → file write so a 100 MB bundle
                    // doesn't double-buffer through Convert.FromBase64String's
                    // ~100 MB byte[] on the LOH plus a synchronous File.WriteAllBytes.
                    // The management envelope's body is still buffered into the
                    // jsonOk string (wire-format limit), but the decode + write
                    // are now chunked, so peak working-set drops from
                    // ~base64+byte[]+envelope to ~base64+small-chunk.
                    long written;
                    try
                    {
                        written = StreamBase64ToFile(base64, output);
                    }
                    catch (FormatException ex)
                    {
                        OutputFormatter.WriteError(
                            $"Server returned invalid base64 in the bundle response: {ex.Message}",
                            "INVALID_RESPONSE");
                        return 1;
                    }
                    Console.WriteLine($"Wrote {written:N0} bytes to {output} (server reported {byteCount:N0}).");
                    return 0;
                });
        });
        return cmd;
    }

    // ====================================================================
    // bundle preview
    // ====================================================================
    private static Command BuildPreview(
        Option<string> urlOption, Option<string> formatOption,
        Option<string> usernameOption, Option<string> passwordOption)
    {
        var inputOption = new Option<string>("--input")
        {
            Description = "Bundle file path (.scadabundle)",
            Required = true,
        };
        var passphraseOption = new Option<string?>("--passphrase")
        {
            Description = "Passphrase for encrypted bundles.",
        };

        var cmd = new Command("preview")
        {
            Description = "Load a bundle and print the diff preview without applying",
        };
        cmd.Add(inputOption);
        cmd.Add(passphraseOption);

        cmd.SetAction(async (ParseResult result) =>
        {
            var input = result.GetValue(inputOption)!;
            if (!File.Exists(input))
            {
                OutputFormatter.WriteError($"Bundle file not found: {input}", "FILE_NOT_FOUND");
                return 1;
            }
            var bytes = await File.ReadAllBytesAsync(input);
            var payload = new PreviewBundleCommand(
                Base64Bundle: Convert.ToBase64String(bytes),
                Passphrase: result.GetValue(passphraseOption));

            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                payload,
                timeout: BundleCommandTimeout,
                onSuccess: jsonOk =>
                {
                    Console.WriteLine(jsonOk);
                    return 0;
                });
        });
        return cmd;
    }

    // ====================================================================
    // bundle import
    // ====================================================================
    private static Command BuildImport(
        Option<string> urlOption, Option<string> formatOption,
        Option<string> usernameOption, Option<string> passwordOption)
    {
        var inputOption = new Option<string>("--input")
        {
            Description = "Bundle file path (.scadabundle)",
            Required = true,
        };
        var passphraseOption = new Option<string?>("--passphrase")
        {
            Description = "Passphrase for encrypted bundles.",
        };
        var onConflictOption = new Option<string>("--on-conflict")
        {
            Description = "Resolution policy applied to every Modified row: skip, overwrite, or rename. Default: overwrite.",
            DefaultValueFactory = _ => "overwrite",
        };

        var cmd = new Command("import")
        {
            Description = "Load + apply a bundle with a single global conflict policy",
        };
        cmd.Add(inputOption);
        cmd.Add(passphraseOption);
        cmd.Add(onConflictOption);

        cmd.SetAction(async (ParseResult result) =>
        {
            var input = result.GetValue(inputOption)!;
            if (!File.Exists(input))
            {
                OutputFormatter.WriteError($"Bundle file not found: {input}", "FILE_NOT_FOUND");
                return 1;
            }
            var bytes = await File.ReadAllBytesAsync(input);
            var payload = new ImportBundleCommand(
                Base64Bundle: Convert.ToBase64String(bytes),
                Passphrase: result.GetValue(passphraseOption),
                DefaultConflictPolicy: result.GetValue(onConflictOption)!);

            return await CommandHelpers.ExecuteCommandAsync(
                result, urlOption, formatOption, usernameOption, passwordOption,
                payload,
                timeout: BundleCommandTimeout,
                onSuccess: jsonOk =>
                {
                    Console.WriteLine(jsonOk);
                    return 0;
                });
        });
        return cmd;
    }

    // ====================================================================
    // Shared HTTP plumbing
    // ====================================================================
    //
    // CLI-017: bundle commands previously routed through a private
    // RunBundleCommandAsync that re-implemented URL/credential resolution and
    // skipped the IsAuthorizationFailure(...) check that ExecuteCommandAsync
    // enforces — a server that signalled FORBIDDEN/UNAUTHORIZED via the error
    // code on a non-403 status would exit 1 instead of the documented exit 2.
    // The bundle path now delegates to CommandHelpers.ExecuteCommandAsync with
    // the longer BundleCommandTimeout and a per-command success handler, so the
    // exit-code contract is unified across every command group.

    // CLI-019: chunked base64 → file streaming. The management envelope's
    // success body is a single buffered JSON string (the wire format does not
    // currently support response-body streaming), so we cannot remove the
    // ~base64-string + ~envelope-string allocation. What we CAN — and do —
    // remove is the intermediate ~bytecount-sized byte[] that
    // Convert.FromBase64String allocates plus the synchronous File.WriteAllBytes:
    // we slice the base64 string into 4-byte-multiple chunks (4 base64 chars
    // decode into exactly 3 bytes, so any multiple of 4 is a clean boundary)
    // and decode each chunk into a small rented buffer that we copy into the
    // output FileStream. The chunk size is a tradeoff — large enough that the
    // per-chunk loop overhead is negligible, small enough that we never put
    // anything on the LOH (1 MB is below the 85 KB LOH threshold's larger
    // cousin for buffers we don't keep). Returns the total decoded byte count
    // for the post-write summary line.
    internal const int Base64StreamChunkChars = 1024 * 1024; // 1 MB of base64 chars ≈ 768 KB decoded

    /// <summary>
    /// Decodes a base64 string into <paramref name="outputPath"/> in chunked fashion to avoid
    /// large intermediate allocations. Returns the total number of decoded bytes written.
    /// </summary>
    /// <param name="base64">The base64-encoded content to decode and write.</param>
    /// <param name="outputPath">Destination file path; created or overwritten.</param>
    /// <returns>Total number of bytes written to the output file.</returns>
    internal static long StreamBase64ToFile(string base64, string outputPath)
    {
        if (base64 is null) throw new ArgumentNullException(nameof(base64));
        if (string.IsNullOrEmpty(outputPath)) throw new ArgumentException("Output path required.", nameof(outputPath));

        // Skip any leading whitespace and trailing padding noise. Convert.TryFromBase64Chars
        // tolerates internal whitespace, but slicing on arbitrary positions would split a
        // run of base64 chars mid-quad — round the chunk to a multiple of 4 so each slice
        // is independently decodable.
        var chunkChars = Base64StreamChunkChars - (Base64StreamChunkChars % 4);
        var totalChars = base64.Length;
        var totalWritten = 0L;

        using var fileStream = new FileStream(
            outputPath, FileMode.Create, FileAccess.Write, FileShare.None,
            bufferSize: 81920, useAsync: false);

        // 4 base64 chars = 3 bytes, so the decoded buffer is sized accordingly.
        var byteBuffer = new byte[(chunkChars / 4) * 3];

        for (var offset = 0; offset < totalChars; offset += chunkChars)
        {
            var take = Math.Min(chunkChars, totalChars - offset);
            var slice = base64.AsSpan(offset, take);

            // The final slice may be shorter than chunkChars and may carry
            // trailing '=' padding; TryFromBase64Chars handles that.
            if (!Convert.TryFromBase64Chars(slice, byteBuffer, out var written))
            {
                throw new FormatException(
                    $"Bundle response contained invalid base64 at character offset {offset}.");
            }
            fileStream.Write(byteBuffer, 0, written);
            totalWritten += written;
        }

        return totalWritten;
    }

    private static Option<IReadOnlyList<string>?> NameListOption(string name, string description)
    {
        var opt = new Option<IReadOnlyList<string>?>(name)
        {
            Description = description,
            CustomParser = arg =>
            {
                var token = arg.Tokens.Count == 0 ? null : arg.Tokens[0].Value;
                if (string.IsNullOrWhiteSpace(token)) return null;
                return token
                    .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                    .ToArray();
            },
        };
        return opt;
    }
}
