using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Api;

/// <summary>
///     Headless REST gateway over the admin-operations cluster singleton, for CI / scripts that
///     need to trigger a deployment without driving the Blazor AdminUI. Mounted on admin-role nodes
///     only (where <see cref="IAdminOperationsClient"/> + the singleton proxy live), it forwards to
///     the same <see cref="IAdminOperationsClient.StartDeploymentAsync"/> the "Deploy current
///     configuration" button calls.
///     <para>
///         Auth is a single configured secret (<c>Security:DeployApiKey</c>) checked from the
///         <c>X-Api-Key</c> header in fixed time — deliberately orthogonal to the cookie-only web
///         auth so automation needs no LDAP login round-trip. The endpoint is <c>AllowAnonymous</c>
///         (so the global <c>RequireAuthenticatedUser</c> fallback doesn't 401 it) and enforces the
///         key itself; when the key is unconfigured the endpoint is disabled (503), so it is never
///         open.
///     </para>
/// </summary>
public static class DeployApiEndpoints
{
    /// <summary>Header carrying the deploy API key.</summary>
    public const string ApiKeyHeader = "X-Api-Key";
    /// <summary>Configuration key holding the deploy API secret.</summary>
    public const string ConfigKey = "Security:DeployApiKey";

    /// <summary>Maps <c>POST /api/deployments</c> — the headless deploy trigger. No-op-safe to call
    /// on any admin-role node; the handler self-disables (503) until <c>Security:DeployApiKey</c> is set.</summary>
    /// <param name="app">The endpoint route builder.</param>
    /// <param name="config">Application configuration (read for the deploy API key).</param>
    /// <returns>The same <paramref name="app"/> for chaining.</returns>
    public static IEndpointRouteBuilder MapOtOpcUaDeployApi(this IEndpointRouteBuilder app, IConfiguration config)
    {
        var configuredKey = config[ConfigKey];

        app.MapPost("/api/deployments", async (
            HttpContext http,
            DeployRequest? body,
            IAdminOperationsClient adminOps,
            CancellationToken ct) =>
        {
            if (string.IsNullOrEmpty(configuredKey))
                return Results.Problem(
                    "Deploy API is disabled. Set Security:DeployApiKey to enable it.",
                    statusCode: StatusCodes.Status503ServiceUnavailable);

            if (!IsAuthorized(http.Request.Headers[ApiKeyHeader].ToString(), configuredKey))
                return Results.Unauthorized();

            var createdBy = string.IsNullOrWhiteSpace(body?.CreatedBy) ? "rest-api" : body!.CreatedBy!.Trim();
            var result = await adminOps.StartDeploymentAsync(createdBy, ct);
            return ToResult(result);
        })
        .AllowAnonymous()         // gated by the API key, not the cookie auth fallback
        .DisableAntiforgery()     // machine endpoint, not a browser form post
        .WithName("StartDeployment");

        return app;
    }

    /// <summary>Fixed-time compare of the supplied key against the configured secret. False when
    /// either is empty (so an unconfigured key never authorizes).</summary>
    /// <param name="provided">The caller-supplied key (from the <c>X-Api-Key</c> header).</param>
    /// <param name="configuredKey">The configured secret.</param>
    /// <returns><see langword="true"/> only when both are non-empty and equal.</returns>
    public static bool IsAuthorized(string? provided, string? configuredKey)
    {
        if (string.IsNullOrEmpty(configuredKey) || string.IsNullOrEmpty(provided)) return false;
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(provided), Encoding.UTF8.GetBytes(configuredKey));
    }

    /// <summary>Map a <see cref="StartDeploymentResult"/> to its HTTP result: Accepted → 202 (with a
    /// Location), NoChanges → 200, AnotherDeploymentInFlight → 409, Rejected → 422, anything else → 500.</summary>
    /// <param name="r">The deployment outcome from the admin-operations singleton.</param>
    /// <returns>The corresponding HTTP result.</returns>
    public static IResult ToResult(StartDeploymentResult r) => r.Outcome switch
    {
        StartDeploymentOutcome.Accepted => Results.Accepted(
            $"/api/deployments/{r.DeploymentId?.Value}",
            new DeployResponse(r.Outcome.ToString(), r.DeploymentId?.Value, r.RevisionHash?.Value)),
        StartDeploymentOutcome.NoChanges => Results.Ok(
            new DeployResponse(r.Outcome.ToString(), r.DeploymentId?.Value, r.RevisionHash?.Value)),
        StartDeploymentOutcome.AnotherDeploymentInFlight => Results.Conflict(
            new DeployResponse(r.Outcome.ToString(), DeploymentId: null, RevisionHash: null)),
        StartDeploymentOutcome.Rejected => Results.Problem(
            r.Message ?? "Deployment rejected.", statusCode: StatusCodes.Status422UnprocessableEntity),
        _ => Results.Problem($"Unexpected deployment outcome: {r.Outcome}",
            statusCode: StatusCodes.Status500InternalServerError),
    };

    /// <summary>Optional request body. <c>CreatedBy</c> is recorded as the deployment's initiator
    /// (audit); defaults to <c>"rest-api"</c> when absent.</summary>
    /// <param name="CreatedBy">Who/what initiated the deploy (for the audit trail).</param>
    public sealed record DeployRequest(string? CreatedBy);

    /// <summary>Response body for a deploy trigger.</summary>
    /// <param name="Outcome">The <see cref="StartDeploymentOutcome"/> name.</param>
    /// <param name="DeploymentId">The new deployment id when one was created; otherwise null.</param>
    /// <param name="RevisionHash">The config revision hash, when known.</param>
    public sealed record DeployResponse(string Outcome, Guid? DeploymentId, string? RevisionHash);
}
