using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Deployment;

namespace ZB.MOM.WW.ScadaBridge.ManagementService;

/// <summary>
/// Minimal-API endpoint a SITE calls to fetch a single deployment's flattened
/// configuration by id (the "notify-and-fetch" deploy path): central stages a
/// <see cref="PendingDeployment"/> row, then notifies the site of the deployment
/// id; the site fetches the staged JSON over HTTP rather than receiving it inside
/// an Akka message (avoids the 128 KB frame limit).
///
/// <para>
/// <b>Route.</b> <c>GET /api/internal/deployments/{deploymentId}/config</c>, mapped
/// in the same central-role block as <c>/api/audit/*</c> and <c>/management</c>, so
/// it is reachable on the same host/port (and through Traefik to the active node).
/// </para>
///
/// <para>
/// <b>Auth (security boundary).</b> This is a machine-to-machine call — NOT a
/// cookie/JWT/LDAP-authenticated operator request. It is gated SOLELY by a
/// per-deployment fetch token the site presents in the <c>X-Deployment-Token</c>
/// header, compared against the staged row's token in constant time
/// (<see cref="DeploymentFetchToken.ConstantTimeEquals"/>). The endpoint is marked
/// <c>.AllowAnonymous()</c>; the central host registers NO authorization
/// <c>FallbackPolicy</c>, so an anonymous endpoint is reachable without the
/// per-operator Basic/LDAP flow the audit/management endpoints apply by hand.
/// The token — short-TTL, single-deployment-scoped, removed on supersession — is
/// the entire security boundary.
/// </para>
///
/// <para>
/// <b>Existence hiding.</b> Unknown, superseded (the row is deleted), and expired
/// deployments are indistinguishable (all <c>404 NotFound</c>) to any caller,
/// regardless of the presented token. A live, non-expired row with a missing/wrong
/// token returns <c>401</c> — which DOES confirm the id exists — and that is
/// acceptable because deployment ids are unguessable GUIDs and the 256-bit
/// per-deployment token is the real security boundary.
/// </para>
/// </summary>
public static class DeploymentConfigEndpoints
{
    /// <summary>Request header carrying the per-deployment fetch token.</summary>
    public const string TokenHeader = "X-Deployment-Token";

    /// <summary>
    /// Registers the <c>GET /api/internal/deployments/{deploymentId}/config</c>
    /// token-gated fetch endpoint. Marked <c>AllowAnonymous</c> — the per-deployment
    /// token is the sole security boundary (see the type-level remarks).
    /// </summary>
    /// <param name="endpoints">The endpoint route builder to register the route on.</param>
    /// <returns>The same <paramref name="endpoints"/> builder, for chaining.</returns>
    public static IEndpointRouteBuilder MapDeploymentConfigAPI(this IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("/api/internal/deployments/{deploymentId}/config", (Delegate)HandleGetConfig)
                 .AllowAnonymous();
        return endpoints;
    }

    /// <summary>
    /// Thin HTTP adapter: pulls the <c>X-Deployment-Token</c> header and the current
    /// UTC instant, loads the staged row, and delegates the security decision to the
    /// pure <see cref="Resolve"/> resolver.
    /// </summary>
    /// <param name="deploymentId">The deployment id route value (the fetch key).</param>
    /// <param name="ctx">The HTTP context for the current request.</param>
    /// <param name="repo">The deployment-manager repository resolving the staged row.</param>
    /// <param name="ct">A cancellation token tied to the request lifetime.</param>
    /// <returns>The HTTP result (200 staged JSON, 401, or 404).</returns>
    internal static async Task<IResult> HandleGetConfig(
        string deploymentId,
        HttpContext ctx,
        IDeploymentManagerRepository repo,
        CancellationToken ct)
    {
        var token = ctx.Request.Headers[TokenHeader].ToString();
        var row = await repo.GetPendingDeploymentByIdAsync(deploymentId, ct);
        return Resolve(row, token, DateTimeOffset.UtcNow);
    }

    /// <summary>
    /// Pure, security-critical decision resolver for the fetch endpoint. Order of
    /// checks matters: existence/TTL are evaluated BEFORE the token so a missing,
    /// superseded, or expired deployment is indistinguishable (all <c>404</c>) to a
    /// caller without a valid token.
    /// </summary>
    /// <param name="row">The staged pending deployment, or <c>null</c> if unknown/superseded.</param>
    /// <param name="token">The token presented in the <c>X-Deployment-Token</c> header (may be empty).</param>
    /// <param name="nowUtc">The current UTC instant used for the TTL comparison.</param>
    /// <returns>
    /// <c>404 NotFound</c> when <paramref name="row"/> is null or expired;
    /// <c>401 Unauthorized</c> when the token is missing/empty or does not match;
    /// otherwise <c>200</c> with the staged <see cref="PendingDeployment.ConfigurationJson"/>
    /// as <c>application/json</c>.
    /// </returns>
    public static IResult Resolve(PendingDeployment? row, string token, DateTimeOffset nowUtc)
    {
        // Unknown OR superseded (supersession deletes the row): hide existence.
        if (row is null)
        {
            return Results.NotFound();
        }

        // Expired (TTL elapsed): treat as gone — also hides existence to a wrong token.
        if (row.ExpiresAtUtc <= nowUtc)
        {
            return Results.NotFound();
        }

        // Token is the entire security boundary; compare in constant time.
        if (string.IsNullOrEmpty(token) || !DeploymentFetchToken.ConstantTimeEquals(token, row.Token))
        {
            return Results.Unauthorized();
        }

        return Results.Content(row.ConfigurationJson, "application/json");
    }
}
