using DodoSSH.Api.Features.Events;
using DodoSSH.Api.Features.Identity;
using DodoSSH.Api.Features.Meta;
using DodoSSH.Api.Features.Sync;
using DodoSSH.Api.Features.Teams;
using DodoSSH.Contracts;
using FastEndpoints;

namespace DodoSSH.Api.Setup;

/// <summary>
/// The single, explicit list of every endpoint.
/// </summary>
/// <remarks>
/// FastEndpoints can find endpoints by scanning assemblies. It is deliberately not asked to. Explicit
/// registration gives predictable startup, survives trimming, and makes every route greppable from one
/// file — and a route that silently disappears because an assembly was not scanned is a genuinely nasty
/// failure. Scanning is worse here than in general: under <c>WebApplicationFactory</c> the scan reaches
/// the test assembly too, so an endpoint written in a test would be registered into the host under test.
/// The cost is one line per endpoint, and a line forgotten is a route missing with no compile error —
/// which is what <c>EndpointInventoryTests</c> exists to catch.
/// </remarks>
internal static class EndpointRegistration
{
    /// <summary>
    /// The route template FastEndpoints maps whether or not it is wanted.
    /// </summary>
    /// <remarks>
    /// The registered template, not a request path: it is matched against what routing selected rather
    /// than against anything a caller typed. FastEndpoints registers it without a leading slash.
    /// </remarks>
    private const string RouteTableTemplate = "_test_url_cache_";

    internal static IServiceCollection AddDodoEndpoints(this IServiceCollection services) =>
        services.AddFastEndpoints(new List<Type>
        {
            typeof(GetMetaEndpoint),
            typeof(GetDodoSshConfigurationEndpoint),
            typeof(GetMeEndpoint),
            typeof(EnrollEndpoint),
            typeof(RegisterDeviceEndpoint),
            typeof(RevokeDeviceEndpoint),
            typeof(LookupDirectoryEndpoint),
            typeof(ReadKeyLogEndpoint),
            typeof(SyncPullEndpoint),
            typeof(SyncPushEndpoint),
            typeof(VaultEventsEndpoint),
            typeof(CreateTeamEndpoint),
            typeof(ListTeamsEndpoint),
            typeof(UpdateTeamEndpoint),
            typeof(ArchiveTeamEndpoint),
            typeof(TransferTeamOwnershipEndpoint),
            typeof(ListTeamMembersEndpoint),
            typeof(AddTeamMemberEndpoint),
            typeof(ChangeTeamMemberRoleEndpoint),
            typeof(RemoveTeamMemberEndpoint),
            typeof(CreateTeamVaultEndpoint),
            typeof(RenameVaultEndpoint),
            typeof(DeleteVaultEndpoint),
            typeof(ListVaultGrantsEndpoint),
            typeof(IssueVaultGrantEndpoint),
            typeof(RevokeVaultGrantEndpoint),
            typeof(RekeyVaultEndpoint),

            // Registered as each feature lands:
            //   Identity   — key rotation, passphrase change
            //   Vaults     — per-item ACLs
            //   Relay      — tickets and the WebSocket
            //   Audit, Admin
        });

    /// <summary>Hides the endpoint listing FastEndpoints publishes at <c>GET /_test_url_cache_</c>.</summary>
    /// <remarks>
    /// <para>
    /// <c>UseFastEndpoints</c> maps that route unconditionally, in every environment, carrying neither a
    /// policy nor <c>AllowAnonymous</c>. It answers with every endpoint class name and route template the
    /// server knows, to back a test helper this repository does not use, and there is no switch to turn
    /// it off. The deny-by-default policy means a caller has to be authenticated to read it, which is not
    /// the same as it being nobody's business.
    /// </para>
    /// <para>
    /// This asks routing which endpoint it selected rather than comparing the request path, because the
    /// path cannot be compared correctly by hand: routing matches literal segments case-insensitively
    /// and tolerates a trailing slash, so <c>/_TEST_URL_CACHE_</c> and <c>/_test_url_cache_/</c> reach
    /// the same endpoint as the canonical spelling. A hand-written comparison that agrees with the
    /// matcher on Monday is a bypass on Tuesday. <c>WebApplication</c> inserts <c>UseRouting</c> ahead of
    /// every middleware registered here, so the selected endpoint is already available; short-circuiting
    /// before <c>UseEndpoints</c> is what keeps the answer independent of registration order.
    /// </para>
    /// </remarks>
    internal static WebApplication BlockFastEndpointsRouteTable(this WebApplication app)
    {
        app.Use(static async (context, next) =>
        {
            if (context.GetEndpoint() is RouteEndpoint selected
                && string.Equals(
                    selected.RoutePattern.RawText?.Trim('/'),
                    RouteTableTemplate,
                    StringComparison.Ordinal))
            {
                context.Response.StatusCode = StatusCodes.Status404NotFound;
                return;
            }

            await next(context).ConfigureAwait(false);
        });

        return app;
    }

    internal static WebApplication MapDodoEndpoints(this WebApplication app)
    {
        app.UseFastEndpoints(config =>
        {
            // Every route is written out in full in its own Configure(). A global prefix would rewrite
            // all of them at once, and /.well-known/ is not under /api at all.
            config.Endpoints.RoutePrefix = null;

            // FastEndpoints serialises through its own copy of the host's JsonOptions, taken implicitly
            // at this point. Applied again explicitly because that copy is undocumented, and Setup/Json.cs
            // records what silent JSON drift on this exact surface already cost once.
            DodoSshJsonContext.ApplyTo(config.Serializer.Options);

            // A body that will not deserialise is answered here, before any handler runs. The default
            // body is not a problem document despite the media type it claims, and it names the failing
            // .NET type; Problems.ForBindingFailure says the same thing in the shape everything else uses.
            config.Errors.ProducesMetadataType = null;
            config.Errors.ResponseBuilder =
                static (_, context, statusCode) => Problems.ForBindingFailure(context, statusCode);

            // Applied to every endpoint rather than endpoint by endpoint: the default is the dangerous
            // one, so the safe choice has to be the one nobody can forget.
            config.Endpoints.Configurator =
                static endpoint => endpoint.RequestBinder(typeof(BodyOnlyRequestBinder<>));
        });

        return app;
    }
}

/// <summary>
/// Binds a request DTO from the JSON body and nothing else.
/// </summary>
/// <remarks>
/// <para>
/// FastEndpoints' default binder deserialises the body and then writes route values, query-string
/// parameters, headers, claims and cookies over the top, matching DTO properties by name. Minimal APIs
/// bound a body parameter from the body alone, so leaving the default in place would silently widen
/// every request: <c>?cursor=…</c> would override the cursor in a pull body, and — the reason this is
/// not merely untidy — <c>?identityProviderToken=…</c> would let an ID token be supplied in a URL,
/// where proxies, browser history and access logs all keep copies of it. This API takes some trouble to
/// keep tokens out of logs; see <c>IncludeErrorDetails = false</c> in <see cref="Auth"/>.
/// </para>
/// <para>
/// Route values are still read, deliberately and one at a time, with <c>Route&lt;T&gt;("name")</c> in
/// the handlers that need one. That reads the route directly rather than through the DTO, so it is
/// unaffected by this.
/// </para>
/// </remarks>
/// <typeparam name="TRequest">The request DTO being bound.</typeparam>
internal sealed class BodyOnlyRequestBinder<TRequest>()
    : RequestBinder<TRequest>(BindingSource.JsonBody)
    where TRequest : notnull;
