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 ScadaLink.Commons.Messages.Management;
using ScadaLink.Security;

namespace ScadaLink.ManagementService;

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

    public static IEndpointRouteBuilder MapManagementAPI(this IEndpointRouteBuilder endpoints)
    {
        endpoints.MapPost("/management", (Delegate)HandleRequest);
        return endpoints;
    }

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

        // 1. Decode Basic Auth
        var authHeader = context.Request.Headers.Authorization.ToString();
        if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
        {
            return Results.Json(new { error = "Authorization header required (Basic scheme).", code = "AUTH_FAILED" }, statusCode: 401);
        }

        string username, password;
        try
        {
            var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader["Basic ".Length..]));
            var colon = decoded.IndexOf(':');
            if (colon < 0) throw new FormatException();
            username = decoded[..colon];
            password = decoded[(colon + 1)..];
        }
        catch
        {
            return Results.Json(new { error = "Malformed Basic Auth header.", code = "AUTH_FAILED" }, statusCode: 401);
        }

        if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
        {
            return Results.Json(new { error = "Username and password are required.", code = "AUTH_FAILED" }, statusCode: 401);
        }

        // 2. LDAP authentication
        var ldapAuth = context.RequestServices.GetRequiredService<LdapAuthService>();
        var authResult = await ldapAuth.AuthenticateAsync(username, password);
        if (!authResult.Success)
        {
            return Results.Json(
                new { error = authResult.ErrorMessage ?? "Authentication failed.", code = "AUTH_FAILED" },
                statusCode: 401);
        }

        // 3. Role resolution
        var roleMapper = context.RequestServices.GetRequiredService<RoleMapper>();
        var mappingResult = await roleMapper.MapGroupsToRolesAsync(
            authResult.Groups ?? (IReadOnlyList<string>)Array.Empty<string>());

        var permittedSiteIds = mappingResult.IsSystemWideDeployment
            ? Array.Empty<string>()
            : mappingResult.PermittedSiteIds.ToArray();

        var authenticatedUser = new AuthenticatedUser(
            authResult.Username!,
            authResult.DisplayName!,
            mappingResult.Roles.ToArray(),
            permittedSiteIds);

        // 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);

        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)
    {
        public static CommandParseResult Ok(object command) => new(true, command, null, null);
        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 (finding ManagementService-006).
    /// </summary>
    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,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
            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("{}");
        }
    }
}
