using System.Security.Cryptography;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using Tiku.Api.Contracts;
using Tiku.Api.Options;
using Tiku.Application.Auth;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
using Tiku.Domain.Tenancy;

namespace Tiku.Api.Controllers;

[ApiController]
[Tags("租户端-浏览器认证")]
[Route("api/tenant/auth/browser")]
[Produces("application/json")]
public sealed class BrowserAuthController(
    IAuthService authService,
    IOwnerActivationService ownerActivationService,
    ISmsVerificationService smsVerificationService,
    ITenantContext tenantContext,
    ITenantContextInitializer tenantContextInitializer,
    ITenantDirectory tenantDirectory,
    ICurrentUser currentUser,
    IOptions<TenantResolutionOptions> tenantResolutionOptions) : ControllerBase
{
    [AllowAnonymous]
    [EnableRateLimiting(AuthRateLimitPolicies.Password)]
    [HttpPost("activation/complete")]
    [EndpointSummary("完成租户 Owner 激活并建立浏览器会话")]
    public async Task<ActionResult<object>> CompleteOwnerActivation(
        CompleteOwnerActivationDto request,
        CancellationToken cancellationToken)
    {
        EnsureTrustedOrigin();
        var tenantId = tenantContext.TenantId ?? throw new TenantNotFoundException();
        var result = await ownerActivationService.CompleteAndAuthenticateAsync(
            request.ToRequest(),
            tenantId,
            Request.Host.Host,
            HttpContext.Connection.RemoteIpAddress?.ToString(),
            Request.Headers.UserAgent.ToString(),
            cancellationToken);
        return Ok(WriteResult(result));
    }

    [AllowAnonymous]
    [EnableRateLimiting(AuthRateLimitPolicies.Sms)]
    [HttpPost("sms/send")]
    [EndpointSummary("发送浏览器短信验证码")]
    public async Task<ActionResult<SmsSendResult>> SendSmsCode(
        [FromBody] SendSmsCodeDto request,
        CancellationToken cancellationToken)
    {
        EnsureTrustedOrigin();
        var realm = request.Realm!.Value;
        if (realm != AuthRealm.Tenant)
            throw new RequiredFieldException("SMS authentication is only available in the tenant realm.");
        var tenantId = await ResolveTenantIdAsync(realm, request.TenantCode, cancellationToken)
                       ?? throw new RequiredFieldException("tenantCode is required for SMS authentication.");
        var result = await smsVerificationService.CreateCodeAsync(new SendSmsCodeRequest(
            tenantId,
            request.Phone,
            SmsPurpose.Login,
            HttpContext.Connection.RemoteIpAddress?.ToString(),
            Request.Headers.UserAgent.ToString(),
            request.DeviceId), cancellationToken);
        return Accepted(result);
    }

    [AllowAnonymous]
    [HttpPost("login/password")]
    [EndpointSummary("浏览器手机号密码登录")]
    public async Task<ActionResult<object>> LoginWithPassword(
        [FromBody] PasswordLoginDto request,
        CancellationToken cancellationToken)
    {
        EnsureTrustedOrigin();
        var realm = request.Realm!.Value;
        var identifier = request.Identifier ?? request.Phone;
        if (string.IsNullOrWhiteSpace(identifier)) throw new RequiredFieldException("identifier is required.");
        var result = await authService.LoginWithPasswordAsync(new PasswordLoginRequest(
            realm,
            await ResolveTenantIdAsync(realm, request.TenantCode, cancellationToken),
            identifier,
            request.Password,
            HttpContext.Connection.RemoteIpAddress?.ToString(),
            Request.Headers.UserAgent.ToString()), cancellationToken);
        return Ok(WriteResult(result));
    }

    [AllowAnonymous]
    [EnableRateLimiting(AuthRateLimitPolicies.Sms)]
    [HttpPost("login/sms")]
    [EndpointSummary("浏览器短信验证码登录")]
    public async Task<ActionResult<object>> LoginWithSms(
        [FromBody] SmsLoginDto request,
        CancellationToken cancellationToken)
    {
        EnsureTrustedOrigin();
        var realm = request.Realm!.Value;
        var result = await authService.LoginWithSmsAsync(new SmsLoginRequest(
            realm,
            await ResolveTenantIdAsync(realm, request.TenantCode, cancellationToken),
            request.Phone,
            request.Code,
            HttpContext.Connection.RemoteIpAddress?.ToString(),
            Request.Headers.UserAgent.ToString()), cancellationToken);
        return Ok(WriteResult(result));
    }

    [AllowAnonymous]
    [HttpPost("oauth/wechat")]
    [EndpointSummary("浏览器微信网页 OAuth 登录")]
    public async Task<ActionResult<object>> LoginWithWechatWeb(
        [FromBody] OAuthCodeDto request,
        CancellationToken cancellationToken)
    {
        EnsureTrustedOrigin();
        var realm = request.Realm!.Value;
        var result = await authService.LoginWithWechatWebAsync(new WechatLoginRequest(
            realm,
            await ResolveTenantIdAsync(realm, request.TenantCode, cancellationToken),
            request.Code,
            HttpContext.Connection.RemoteIpAddress?.ToString(),
            Request.Headers.UserAgent.ToString()), cancellationToken);
        return Ok(WriteResult(result));
    }

    [AllowAnonymous]
    [HttpPost("oauth/wechat-miniapp")]
    [EndpointSummary("浏览器微信小程序登录")]
    public async Task<ActionResult<object>> LoginWithWechatMiniApp(
        [FromBody] OAuthCodeDto request,
        CancellationToken cancellationToken)
    {
        EnsureTrustedOrigin();
        var realm = request.Realm!.Value;
        var result = await authService.LoginWithWechatMiniAppAsync(new WechatLoginRequest(
            realm,
            await ResolveTenantIdAsync(realm, request.TenantCode, cancellationToken),
            request.Code,
            HttpContext.Connection.RemoteIpAddress?.ToString(),
            Request.Headers.UserAgent.ToString()), cancellationToken);
        return Ok(WriteResult(result));
    }

    [AllowAnonymous]
    [HttpPost("refresh")]
    [EndpointSummary("刷新浏览器登录会话")]
    [EndpointDescription("读取浏览器 refresh cookie，轮换会话并重新写入认证 cookie。")]
    public async Task<ActionResult<object>> Refresh(CancellationToken cancellationToken)
    {
        var refreshToken = Request.Cookies[BrowserAuthOptions.RefreshCookie];
        if (string.IsNullOrWhiteSpace(refreshToken)) return Unauthorized();
        var tokens = await authService.RefreshAsync(new RefreshSessionRequest(
            refreshToken,
            HttpContext.Connection.RemoteIpAddress?.ToString(),
            Request.Headers.UserAgent.ToString()), cancellationToken);
        WriteCookies(tokens);
        return Ok(new { status = "authenticated" });
    }

    [AllowAnonymous]
    [HttpPost("logout")]
    [EndpointSummary("退出浏览器登录")]
    [EndpointDescription("撤销浏览器 refresh cookie 对应会话，并清理 access、refresh 与 CSRF cookie。")]
    public async Task<IActionResult> Logout(CancellationToken cancellationToken)
    {
        var refreshToken = Request.Cookies[BrowserAuthOptions.RefreshCookie];
        if (!string.IsNullOrWhiteSpace(refreshToken))
            await authService.LogoutAsync(new LogoutSessionRequest(refreshToken), cancellationToken);
        ClearCookies();
        return NoContent();
    }

    [Authorize]
    [HttpPost("logout-all")]
    [EndpointSummary("退出全部浏览器登录会话")]
    public async Task<IActionResult> LogoutAll(CancellationToken cancellationToken)
    {
        if (currentUser.UserId is not { } userId) return Unauthorized();
        await authService.LogoutAllAsync(userId, cancellationToken);
        ClearCookies();
        return NoContent();
    }

    [AllowAnonymous]
    [EnableRateLimiting(AuthRateLimitPolicies.Sms)]
    [HttpPost("password/reset/sms/send")]
    [EndpointSummary("发送浏览器密码重置短信验证码")]
    public async Task<ActionResult<SmsSendResult>> SendPasswordResetCode(
        PasswordResetSmsSendDto request,
        CancellationToken cancellationToken)
    {
        EnsureTrustedOrigin();
        var tenantId = await ResolveTenantIdAsync(AuthRealm.Tenant, request.TenantCode, cancellationToken)
                       ?? throw new RequiredFieldException("tenantCode is required for password reset.");
        var result = await authService.RequestPasswordResetAsync(
            new PasswordResetCodeRequest(
                tenantId,
                request.Phone,
                HttpContext.Connection.RemoteIpAddress?.ToString(),
                Request.Headers.UserAgent.ToString(),
                request.DeviceId),
            cancellationToken);
        return Accepted(result);
    }

    [AllowAnonymous]
    [EnableRateLimiting(AuthRateLimitPolicies.Password)]
    [HttpPost("password/reset")]
    [EndpointSummary("使用短信验证码重置浏览器账号密码")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> ResetPassword(
        PasswordResetDto request,
        CancellationToken cancellationToken)
    {
        EnsureTrustedOrigin();
        var tenantId = await ResolveTenantIdAsync(AuthRealm.Tenant, request.TenantCode, cancellationToken)
                       ?? throw new RequiredFieldException("tenantCode is required for password reset.");
        await authService.ResetPasswordAsync(
            new PasswordResetRequest(
                tenantId,
                request.Phone,
                request.Code,
                request.NewPassword,
                HttpContext.Connection.RemoteIpAddress?.ToString(),
                Request.Headers.UserAgent.ToString()),
            cancellationToken);
        ClearCookies();
        return NoContent();
    }

    [Authorize]
    [EnableRateLimiting(AuthRateLimitPolicies.Password)]
    [HttpPost("password/change")]
    [EndpointSummary("浏览器已登录用户修改密码")]
    public async Task<ActionResult<object>> ChangePassword(
        AuthenticatedPasswordChangeDto request,
        CancellationToken cancellationToken)
    {
        EnsureTrustedOrigin();
        if (currentUser.UserId is not { } userId || currentUser.SessionId is not { } sessionId) return Unauthorized();

        var result = await authService.ChangePasswordAsync(
            new AuthenticatedPasswordChangeRequest(
                userId,
                sessionId,
                request.CurrentPassword,
                request.NewPassword,
                HttpContext.Connection.RemoteIpAddress?.ToString(),
                Request.Headers.UserAgent.ToString()),
            cancellationToken);
        return Ok(WriteResult(result));
    }

    private object WriteResult(AuthenticationResult result)
    {
        if (result.User?.Tokens is { } tokens) WriteCookies(tokens);
        return new
        {
            status = result.Status.ToString(),
            user = result.User is null
                ? null
                : new
                {
                    result.User.UserId,
                    result.User.Phone,
                    result.User.Email,
                    result.User.Name,
                    result.User.Realm,
                    result.User.Tenant
                },
            result.ChallengeToken,
            result.ChallengeExpiresAt
        };
    }

    private void WriteCookies(AuthTokenPair tokens)
    {
        Response.Cookies.Append(BrowserAuthOptions.AccessCookie, tokens.AccessToken, new CookieOptions
        {
            Secure = true,
            HttpOnly = true,
            SameSite = SameSiteMode.Lax,
            Path = "/",
            MaxAge = TimeSpan.FromMinutes(15)
        });
        Response.Cookies.Append(BrowserAuthOptions.RefreshCookie, tokens.RefreshToken, new CookieOptions
        {
            Secure = true,
            HttpOnly = true,
            SameSite = SameSiteMode.Strict,
            Path = "/api/tenant/auth/browser",
            MaxAge = TimeSpan.FromDays(30)
        });
        Response.Cookies.Append(BrowserAuthOptions.CsrfCookie,
            Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLowerInvariant(), new CookieOptions
            {
                Secure = true,
                HttpOnly = false,
                SameSite = SameSiteMode.Strict,
                Path = "/"
            });
    }

    private void ClearCookies()
    {
        Response.Cookies.Delete(BrowserAuthOptions.AccessCookie, new CookieOptions { Secure = true, Path = "/" });
        Response.Cookies.Delete(BrowserAuthOptions.RefreshCookie,
            new CookieOptions { Secure = true, Path = "/api/tenant/auth/browser" });
        Response.Cookies.Delete(BrowserAuthOptions.CsrfCookie, new CookieOptions { Secure = true, Path = "/" });
    }

    private void EnsureTrustedOrigin()
    {
        var origin = Request.Headers.Origin.ToString();
        if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri) ||
            !string.Equals(uri.Scheme, Request.Scheme, StringComparison.OrdinalIgnoreCase) ||
            !string.Equals(uri.Authority, Request.Host.Value, StringComparison.OrdinalIgnoreCase))
            throw new BrowserOriginException();
    }

    private async Task<Guid?> ResolveTenantIdAsync(AuthRealm realm, string? tenantCode,
        CancellationToken cancellationToken)
    {
        if (realm == AuthRealm.Platform)
        {
            var requestHost = Request.Host.Host.Trim().TrimEnd('.');
            if (!tenantResolutionOptions.Value.PlatformHosts.Any(host =>
                    string.Equals(host.Trim().TrimEnd('.'), requestHost, StringComparison.OrdinalIgnoreCase)))
                throw new RequiredFieldException("platform realm is only available on a configured platform host.");
            return null;
        }

        if (tenantContext.TenantId is { } resolved) return resolved;
        if (string.IsNullOrWhiteSpace(tenantCode)) throw new RequiredFieldException("tenantCode is required.");
        var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken)
                     ?? throw new TenantNotFoundException();
        tenantContextInitializer.Initialize(tenant.TenantId, tenant.TenantCode, TenantResolutionSource.TenantCode);
        return tenant.TenantId;
    }
}

public sealed class BrowserOriginException() : Exception("Browser authentication requires a same-origin request.");