using System.Reflection;
using DodoSSH.Api.Setup;
using DodoSSH.Contracts;
using DodoSSH.Crypto;
using FastEndpoints;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Options;

namespace DodoSSH.Api.Features.Meta;

/// <summary>
/// Capability and discovery endpoints.
/// </summary>
/// <remarks>
/// These replace URL-based API versioning. When client and server upgrade independently — which is
/// the normal case for self-hosted software — a client needs to ask what this particular server
/// supports rather than assume. See ADR 0002.
/// </remarks>
internal sealed class GetMetaEndpoint(
    IOptions<SyncOptions> sync,
    IOptions<RelayOptions> relay,
    IOptions<EventsOptions> events,
    IOptions<ServerOptions> server)
    : EndpointWithoutRequest<Ok<MetaResponse>>
{
    /// <summary>Sync semantics version. Bumped when push or pull behaviour changes.</summary>
    internal const int SyncProtocolVersion = 1;

    /// <summary>Feature flag for the relay.</summary>
    internal const string RelayFeature = "relay";

    /// <inheritdoc />
    public override void Configure()
    {
        Get("/api/v1/meta");

        // Anonymous by necessity: a client must be able to discover how to authenticate before it
        // can authenticate. Load-bearing rather than decorative — FastEndpoints authenticates every
        // endpoint that does not say otherwise, so removing this line is a 401 nobody can recover from.
        AllowAnonymous();

        Description(b => b
            .WithName("GetMeta")
            .WithSummary("Server capabilities, versions and limits."));
    }

    /// <inheritdoc />
    public override Task<Ok<MetaResponse>> ExecuteAsync(CancellationToken ct)
    {
        List<string> features = ["teams"];
        if (relay.Value.Enabled)
        {
            features.Add(RelayFeature);
        }

        // Advertised so a client knows whether to hold a socket open or rely on its timer. Absence is
        // not an error — synchronising on a timer is the supported behaviour and the socket only makes
        // it early — which is why this is a feature flag rather than a version bump. See ADR 0012.
        if (events.Value.Enabled)
        {
            features.Add(VaultEvents.Feature);
        }

        return Task.FromResult(TypedResults.Ok(new MetaResponse(
            ServerVersion: ServerVersion,
            ApiVersions: [1],
            SyncProtocolVersion: SyncProtocolVersion,
            CryptoSpecVersion: CryptoSpec.CurrentAadVersion,
            Features: features,
            MinClientVersion: server.Value.MinClientVersion,
            MaxOperationsPerPush: sync.Value.MaxOperationsPerPush,
            MaxPayloadBytes: sync.Value.MaxPayloadBytes,
            MaxItemPayloadBytes: sync.Value.MaxItemPayloadBytes)));
    }

    private static string ServerVersion { get; } =
        typeof(GetMetaEndpoint).Assembly
            .GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
        ?? "0.0.0";
}

/// <summary>
/// Everything a client needs to begin authenticating, from one URL.
/// </summary>
/// <remarks>
/// This <em>is</em> the onboarding story: the user types one server URL and the client discovers the
/// identity provider, the client id and the relay from here. See ADR 0002.
/// </remarks>
internal sealed class GetDodoSshConfigurationEndpoint(
    IOptions<OidcOptions> oidc,
    IOptions<RelayOptions> relay,
    IOptions<ServerOptions> server)
    : EndpointWithoutRequest<Ok<DodoSshConfiguration>>
{
    /// <inheritdoc />
    public override void Configure()
    {
        Get("/.well-known/dodossh-configuration");

        // Anonymous for the same reason as /api/v1/meta, and more so: this is the document that says
        // where the identity provider is.
        AllowAnonymous();

        Description(b => b
            .WithName("GetDodoSshConfiguration")
            .WithSummary("Everything a client needs to begin authenticating, from one URL."));
    }

    /// <inheritdoc />
    public override Task<Ok<DodoSshConfiguration>> ExecuteAsync(CancellationToken ct)
    {
        var relayOptions = relay.Value;

        return Task.FromResult(TypedResults.Ok(new DodoSshConfiguration(
            ApiBaseUrl: new Uri(server.Value.PublicBaseUrl, UriKind.Absolute),
            Oidc: new OidcConfiguration(
                Authority: new Uri(oidc.Value.Authority, UriKind.Absolute),
                ClientId: oidc.Value.ClientId,
                Scopes: [.. oidc.Value.Scopes],
                LoopbackRedirectPattern: oidc.Value.LoopbackRedirectPattern),
            Relay: new RelayConfiguration(
                Enabled: relayOptions.Enabled,
                WebSocketUrl: relayOptions.Enabled && relayOptions.WebSocketUrl is not null
                    ? new Uri(relayOptions.WebSocketUrl, UriKind.Absolute)
                    : null))));
    }
}
