using LiteCqrs;
using Microsoft.Extensions.Hosting;
using TeleWave.Api.Common;
using TeleWave.Application.Auth;
using TeleWave.Application.Auth.ChangePassword;
using TeleWave.Application.Auth.ChangeUserName;
using TeleWave.Application.Auth.DeleteMyAccount;
using TeleWave.Application.Auth.Login;
using TeleWave.Application.Auth.Logout;
using TeleWave.Application.Auth.Me;
using TeleWave.Application.Auth.Refresh;
using TeleWave.Application.Auth.Register;
using TeleWave.Application.Settings.GetSiteSettings;

namespace TeleWave.Api.Endpoints;

public static class AuthEndpoints
{
    private const string RefreshCookieName = "tw_refresh_token";

    public static IEndpointRouteBuilder MapAuthEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/auth")
            .WithTags("Auth")
            .RequireRateLimiting(RateLimiting.AuthPolicy);

        group.MapGet("/registration", RegistrationStatus).Produces<RegistrationStatusDto>();
        group.MapPost("/register", Register).Produces<AuthResponseDto>();
        group.MapPost("/login", Login).Produces<AuthResponseDto>();
        group.MapPost("/refresh", Refresh).Produces<AuthResponseDto>();
        group
            .MapPost("/logout", Logout)
            .RequireAuthorization()
            .Produces(StatusCodes.Status204NoContent);
        group
            .MapPost("/change-password", ChangePassword)
            .RequireAuthorization()
            .Produces(StatusCodes.Status204NoContent);
        group
            .MapPost("/change-username", ChangeUserName)
            .RequireAuthorization()
            .Produces(StatusCodes.Status204NoContent);
        group.MapGet("/me", Me).RequireAuthorization().Produces<CurrentUserDto>();
        group
            .MapDelete("/me", DeleteMe)
            .RequireAuthorization()
            .Produces(StatusCodes.Status204NoContent);

        return app;
    }

    private static async Task<IResult> RegistrationStatus(
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var settings = await sender.Send(new GetSiteSettingsQuery(), cancellationToken);
        return Results.Ok(new RegistrationStatusDto(settings.RegistrationEnabled));
    }

    private static async Task<IResult> Register(
        RegisterCommand command,
        ISender sender,
        HttpRequest request,
        HttpResponse response,
        IHostEnvironment env,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(command, cancellationToken);
        if (!result.IsSuccess)
            return result.ToHttpResult();

        SetRefreshCookie(
            response,
            result.Value.RefreshToken,
            result.Value.RefreshTokenExpiresAt,
            UseSecureCookie(request, env)
        );
        return Results.Ok(ToLoginResponse(result.Value));
    }

    private static async Task<IResult> Login(
        LoginCommand command,
        ISender sender,
        HttpRequest request,
        HttpResponse response,
        IHostEnvironment env,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(command, cancellationToken);
        if (!result.IsSuccess)
            return result.ToHttpResult();

        SetRefreshCookie(
            response,
            result.Value.RefreshToken,
            result.Value.RefreshTokenExpiresAt,
            UseSecureCookie(request, env)
        );
        return Results.Ok(ToLoginResponse(result.Value));
    }

    private static async Task<IResult> Refresh(
        HttpRequest request,
        HttpResponse response,
        ISender sender,
        IHostEnvironment env,
        CancellationToken cancellationToken
    )
    {
        if (
            !request.Cookies.TryGetValue(RefreshCookieName, out var rawToken)
            || string.IsNullOrEmpty(rawToken)
        )
            return Results.Unauthorized();

        var secure = UseSecureCookie(request, env);
        var result = await sender.Send(new RefreshCommand(rawToken), cancellationToken);
        if (!result.IsSuccess)
        {
            response.Cookies.Delete(RefreshCookieName, BuildCookieOptions(secure));
            return result.ToHttpResult();
        }

        SetRefreshCookie(
            response,
            result.Value.RefreshToken,
            result.Value.RefreshTokenExpiresAt,
            secure
        );
        return Results.Ok(ToLoginResponse(result.Value));
    }

    internal static AuthResponseDto ToLoginResponse(AuthResult auth) =>
        new(auth.AccessToken, auth.AccessTokenExpiresAt, auth.User);

    private static async Task<IResult> Logout(
        HttpRequest request,
        HttpResponse response,
        ISender sender,
        IHostEnvironment env,
        CancellationToken cancellationToken
    )
    {
        if (
            request.Cookies.TryGetValue(RefreshCookieName, out var rawToken)
            && !string.IsNullOrEmpty(rawToken)
        )
            await sender.Send(new LogoutCommand(rawToken), cancellationToken);

        var secure = UseSecureCookie(request, env);
        response.Cookies.Delete(RefreshCookieName, BuildCookieOptions(secure));
        DeleteStreamCookie(response, secure);
        return Results.NoContent();
    }

    private static async Task<IResult> ChangePassword(
        ChangePasswordCommand command,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(command, cancellationToken);
        return result.ToHttpResult();
    }

    private static async Task<IResult> ChangeUserName(
        ChangeUserNameCommand command,
        ISender sender,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(command, cancellationToken);
        return result.ToHttpResult();
    }

    private static async Task<IResult> Me(ISender sender, CancellationToken cancellationToken)
    {
        var result = await sender.Send(new GetCurrentUserQuery(), cancellationToken);
        return result.ToHttpResult();
    }

    private static async Task<IResult> DeleteMe(
        HttpRequest request,
        HttpResponse response,
        ISender sender,
        IHostEnvironment env,
        CancellationToken cancellationToken
    )
    {
        var result = await sender.Send(new DeleteMyAccountCommand(), cancellationToken);
        var secure = UseSecureCookie(request, env);
        response.Cookies.Delete(RefreshCookieName, BuildCookieOptions(secure));
        DeleteStreamCookie(response, secure);
        return result.ToHttpResult();
    }

    private static void SetRefreshCookie(
        HttpResponse response,
        string rawToken,
        DateTimeOffset expiresAt,
        bool secure
    )
    {
        var options = BuildCookieOptions(secure);
        options.Expires = expiresAt;
        response.Cookies.Append(RefreshCookieName, rawToken, options);
    }

    private static CookieOptions BuildCookieOptions(bool secure) =>
        new()
        {
            HttpOnly = true,
            Secure = secure,
            SameSite = SameSiteMode.Strict,
            Path = "/api/auth",
        };

    /// <summary>Ставить ли cookie с флагом Secure. Вне Development — всегда true: прод работает за
    /// внешним TLS-прокси, а <see cref="HttpRequest.IsHttps"/> ненадёжен (при неполной настройке
    /// ForwardedHeaders он false, и долгоживущий refresh-cookie ушёл бы без Secure). В Development
    /// допускаем HTTP-разработку.</summary>
    private static bool UseSecureCookie(HttpRequest request, IHostEnvironment env) =>
        !env.IsDevelopment() || request.IsHttps;

    /// <summary>Гасит stream-cookie (tw_stream, Path=/api) — иначе после выхода эфир можно смотреть
    /// по прямой ссылке ещё до истечения токена.</summary>
    private static void DeleteStreamCookie(HttpResponse response, bool secure) =>
        response.Cookies.Delete(
            "tw_stream",
            new CookieOptions
            {
                HttpOnly = true,
                Secure = secure,
                SameSite = SameSiteMode.Strict,
                Path = "/api",
            }
        );
}

public sealed record AuthResponseDto(
    string AccessToken,
    DateTimeOffset ExpiresAt,
    CurrentUserDto User
);

/// <summary>Публичный ответ: включена ли открытая регистрация (для страниц входа/регистрации).</summary>
public sealed record RegistrationStatusDto(bool Enabled);
