using System.Text;
using System.Text.Json;
using Akka.Actor;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.Auth.Abstractions.Ldap;
using ZB.MOM.WW.ScadaBridge.Security;

namespace ZB.MOM.WW.ScadaBridge.ManagementService;

public static class ManagementEndpoints
{
    private static readonly TimeSpan DefaultAskTimeout = TimeSpan.FromSeconds(30);

    /// <summary>
    /// Shared options for the per-request command payload JSON parse in
    /// <see cref="ParseCommand"/> (perf remediation, arch-review WP1.5) — avoids
    /// allocating a new <see cref="JsonSerializerOptions"/> per inbound management
    /// request. Settings preserved exactly.
    /// </summary>
    private static readonly JsonSerializerOptions CommandPayloadJsonOptions =
        new() { PropertyNameCaseInsensitive = true };

    /// <summary>
    /// Resolves the ManagementActor Ask timeout from configuration.
    /// Falls back to <see cref="DefaultAskTimeout"/>
    /// when options are absent or the configured value is not strictly positive — a
    /// zero/negative timeout would make every management call fail immediately.
    /// </summary>
    /// <param name="options">The management service options, or <c>null</c> if not configured.</param>
    /// <returns>The configured timeout, or <see cref="DefaultAskTimeout"/> when none is set.</returns>
    public static TimeSpan ResolveAskTimeout(ManagementServiceOptions? options)
    {
        if (options is { CommandTimeout: { Ticks: > 0 } configured })
            return configured;
        return DefaultAskTimeout;
    }

    private static readonly TimeSpan DefaultLongRunningTimeout = TimeSpan.FromMinutes(5);

    /// <summary>Transport/deploy commands legitimately run for minutes (the CLI already
    /// uses a 5-minute client timeout for bundles); a 30 s server-side Ask 504s while the
    /// piped ProcessCommand keeps applying — the caller then retries and double-applies
    /// (arch-review S1). These command types get LongRunningCommandTimeout instead.</summary>
    private static readonly HashSet<Type> LongRunningCommands =
    [
        typeof(ImportBundleCommand), typeof(PreviewBundleCommand), typeof(ExportBundleCommand),
        typeof(MgmtDeployArtifactsCommand), typeof(MgmtDeployInstanceCommand),
        // A bulk site deploy is by construction the longest command in the set —
        // it is N instance deploys, so it must never inherit the ordinary Ask
        // timeout or the caller would 504 while the batch is still applying.
        typeof(MgmtDeploySiteCommand),
    ];

    /// <summary>
    /// Resolves the ManagementActor Ask timeout for a specific command type. Long-running
    /// transport/deploy commands use <see cref="ManagementServiceOptions.LongRunningCommandTimeout"/>
    /// (default 5 min); all other commands fall through to <see cref="ResolveAskTimeout(ManagementServiceOptions?)"/>.
    /// </summary>
    /// <param name="options">The management service options, or <c>null</c> if not configured.</param>
    /// <param name="commandType">The runtime type of the command being dispatched.</param>
    /// <returns>The resolved Ask timeout for the command.</returns>
    public static TimeSpan ResolveAskTimeout(ManagementServiceOptions? options, Type commandType)
        => LongRunningCommands.Contains(commandType)
            ? (options is { LongRunningCommandTimeout.Ticks: > 0 } o ? o.LongRunningCommandTimeout : DefaultLongRunningTimeout)
            : ResolveAskTimeout(options);

    /// <summary>Registers the <c>POST /management</c> endpoint on the given route builder.</summary>
    /// <param name="endpoints">The route builder to add the endpoint to.</param>
    /// <returns>The same <paramref name="endpoints"/> instance for chaining.</returns>
    public static IEndpointRouteBuilder MapManagementAPI(this IEndpointRouteBuilder endpoints)
    {
        endpoints.MapPost("/management", (Delegate)HandleRequest);
        return endpoints;
    }

    /// <summary>
    /// Per-request body-size ceiling for the management endpoint. ASP.NET Core's
    /// default cap is ~30 MB and would reject Transport Import calls -- a
    /// 100 MB raw bundle base64-inflates to ~140 MB plus envelope. 200 MB is
    /// comfortable without going unbounded.
    /// </summary>
    private const long MaxManagementRequestBodyBytes = 200L * 1024 * 1024;

    private static async Task<IResult> HandleRequest(HttpContext context)
    {
        var logger = context.RequestServices.GetRequiredService<ILogger<ManagementActorHolder>>();

        // 0. Raise the per-request body-size cap before any body is read.
        // The feature is only writable before the request body has been touched.
        var maxBodyFeature = context.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature>();
        if (maxBodyFeature is { IsReadOnly: false })
        {
            maxBodyFeature.MaxRequestBodySize = MaxManagementRequestBodyBytes;
        }

        // 1-3. Authenticate: dev/test DisableLogin bypass → else HTTP Basic → LDAP → roles.
        // Centralised in ManagementAuthenticator so every CLI surface honours DisableLogin
        // identically — the cookie auto-login (AutoLoginAuthenticationHandler) only covers the
        // interactive UI, not this Basic-Auth surface.
        var auth = await ManagementAuthenticator.AuthenticateAsync(context);
        if (auth.Failure is not null)
        {
            return auth.Failure;
        }

        var authenticatedUser = auth.User!;

        // 4. Parse command from request body
        string body;
        using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8))
        {
            body = await reader.ReadToEndAsync();
        }

        var parse = ParseCommand(body);
        if (!parse.Success)
        {
            return Results.Json(new { error = parse.ErrorMessage, code = parse.ErrorCode }, statusCode: 400);
        }

        var command = parse.Command!;

        // 5. Dispatch to ManagementActor
        var holder = context.RequestServices.GetRequiredService<ManagementActorHolder>();
        if (holder.ActorRef == null)
        {
            return Results.Json(new { error = "Management service not ready.", code = "SERVICE_UNAVAILABLE" }, statusCode: 503);
        }

        var correlationId = Guid.NewGuid().ToString("N");
        var envelope = new ManagementEnvelope(authenticatedUser, command, correlationId);

        var askTimeout = ResolveAskTimeout(
            context.RequestServices.GetService<IOptions<ManagementServiceOptions>>()?.Value,
            command.GetType());

        object response;
        try
        {
            response = await holder.ActorRef.Ask(envelope, askTimeout);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "ManagementActor Ask timed out or failed (CorrelationId={CorrelationId})", correlationId);
            return Results.Json(new { error = "Request timed out.", code = "TIMEOUT" }, statusCode: 504);
        }

        // 6. Map response
        return response switch
        {
            ManagementSuccess success => Results.Text(success.JsonData, "application/json", statusCode: 200),
            ManagementError error => Results.Json(new { error = error.Error, code = error.ErrorCode }, statusCode: 400),
            ManagementUnauthorized unauth => Results.Json(new { error = unauth.Message, code = "UNAUTHORIZED" }, statusCode: 403),
            _ => Results.Json(new { error = "Unexpected response.", code = "INTERNAL_ERROR" }, statusCode: 500)
        };
    }

    /// <summary>
    /// Result of parsing a management request body into a strongly-typed command.
    /// </summary>
    public readonly record struct CommandParseResult(
        bool Success, object? Command, string? ErrorMessage, string? ErrorCode)
    {
        /// <summary>Creates a successful parse result wrapping the given command.</summary>
        /// <param name="command">The strongly-typed command object that was parsed.</param>
        /// <returns>A successful <see cref="CommandParseResult"/> containing the parsed command.</returns>
        public static CommandParseResult Ok(object command) => new(true, command, null, null);
        /// <summary>Creates a failed parse result with the given error message.</summary>
        /// <param name="message">Human-readable description of the parse failure.</param>
        /// <returns>A failed <see cref="CommandParseResult"/> with the error message and <c>BAD_REQUEST</c> code.</returns>
        public static CommandParseResult Fail(string message) => new(false, null, message, "BAD_REQUEST");
    }

    /// <summary>
    /// Parses a management request body — a JSON object with a <c>command</c> name and an
    /// optional <c>payload</c> — into the strongly-typed command record. The parsed
    /// <see cref="JsonDocument"/> is disposed deterministically and the missing-payload
    /// case does not allocate a throwaway document.
    /// </summary>
    /// <param name="body">The raw JSON request body string.</param>
    /// <returns>A <see cref="CommandParseResult"/> with the deserialized command on success, or an error on failure.</returns>
    public static CommandParseResult ParseCommand(string body)
    {
        using JsonDocument doc = ParseDocument(body, out var parseError);
        if (parseError != null)
            return CommandParseResult.Fail(parseError);

        if (!doc.RootElement.TryGetProperty("command", out var commandNameElement))
            return CommandParseResult.Fail("Missing 'command' field.");

        var commandName = commandNameElement.GetString();
        if (string.IsNullOrWhiteSpace(commandName))
            return CommandParseResult.Fail("Empty 'command' field.");

        var commandType = ManagementCommandRegistry.Resolve(commandName);
        if (commandType == null)
            return CommandParseResult.Fail($"Unknown command: '{commandName}'.");

        try
        {
            // Missing payload: deserialize from the empty-object literal rather than
            // allocating (and leaking) a throwaway JsonDocument.
            var payloadJson = doc.RootElement.TryGetProperty("payload", out var p)
                ? p.GetRawText()
                : "{}";
            var command = JsonSerializer.Deserialize(payloadJson, commandType,
                CommandPayloadJsonOptions)!;
            return CommandParseResult.Ok(command);
        }
        catch (Exception ex)
        {
            return CommandParseResult.Fail($"Failed to deserialize payload: {ex.Message}");
        }
    }

    private static JsonDocument ParseDocument(string body, out string? error)
    {
        try
        {
            error = null;
            return JsonDocument.Parse(body);
        }
        catch
        {
            error = "Invalid JSON body.";
            return JsonDocument.Parse("{}");
        }
    }
}
