using ERPCore.Dtos.Auth;
using ERPCore.Dtos.Rbac;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace ERPCore.Controllers;

/// <summary>
/// Fronts the external AuthHex identity service (API_REFERENCE.md) so the
/// frontend never calls AuthHex directly. Sessions are delivered as httpOnly
/// Secure cookies (docs/02-SECURITY.md §B.2) via <see cref="AuthCookieWriter"/>
/// — response bodies never carry raw tokens. Does not inherit
/// <see cref="ApiControllerBase"/>: most actions here are pre-session and need
/// <see cref="AllowAnonymousAttribute"/>, and the ETag/If-Match handling that
/// base provides doesn't apply to auth flows.
/// </summary>
[ApiController]
[Produces("application/json")]
[Route("api/v1/auth")]
[Authorize(JwtAuthExtensions.ErpAccessPolicy)]
public sealed class AuthController : ControllerBase
{
    private readonly IAuthUserService _users;
    private readonly IAuthRecoveryService _recovery;
    private readonly IAuthAltService _alt;
    private readonly IRoleService _roles;

    public AuthController(IAuthUserService users, IAuthRecoveryService recovery, IAuthAltService alt, IRoleService roles)
    {
        _users = users;
        _recovery = recovery;
        _alt = alt;
        _roles = roles;
    }

    /// <summary>
    /// Authoritative current-session info for the frontend: role + the sidebar nav
    /// codes it may see (docs/10 C.9 shadow-Role sync). Replaces the frontend's
    /// previous reliance on a stale, untrusted `roleId` cached in localStorage.
    /// </summary>
    [HttpGet("me")]
    [ProducesResponseType(typeof(MeResponseDto), StatusCodes.Status200OK)]
    public async Task<ActionResult<MeResponseDto>> Me(CancellationToken ct)
    {
        var roleCode = User.FindFirst(AuthHexClaims.RoleCode)?.Value;
        return Ok(await _roles.GetMeAsync(roleCode, ct));
    }

    // ---- Session-issuing (UserManager) ------------------------------------

    [HttpPost("register")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(AuthSessionResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<AuthSessionResponse>> Register([FromBody] RegisterRequest request, CancellationToken ct)
    {
        var result = await _users.RegisterAsync(request, ct);
        AuthCookieWriter.WriteSession(Response, result.AccessToken, result.RefreshToken, result.Body.ExpiresIn);
        return Ok(result.Body);
    }

    [HttpPost("login")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(AuthSessionResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<AuthSessionResponse>> Login([FromBody] LoginRequest request, CancellationToken ct)
    {
        var result = await _users.LoginAsync(request, ct);
        AuthCookieWriter.WriteSession(Response, result.AccessToken, result.RefreshToken, result.Body.ExpiresIn);
        return Ok(result.Body);
    }

    [HttpPost("login/otp/verify")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(OtpLoginVerifiedResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<OtpLoginVerifiedResponse>> VerifyLoginOtp([FromBody] VerifyOtpForLoginRequest request, CancellationToken ct)
    {
        var result = await _users.VerifyOtpForLoginAsync(request, ct);
        AuthCookieWriter.WriteSession(Response, result.AccessToken, result.RefreshToken, result.Body.ExpiresIn);
        return Ok(result.Body);
    }

    [HttpPost("refresh-token")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(AuthSessionResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<AuthSessionResponse>> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken ct)
    {
        if (!Request.Cookies.TryGetValue(JwtAuthExtensions.RefreshTokenCookie, out var refreshToken) || string.IsNullOrEmpty(refreshToken))
            throw new DomainException(ErrorCodes.RefreshTokenMissing, "No refresh session cookie present.", 401);

        var result = await _users.RefreshTokenAsync(refreshToken, request, ct);
        AuthCookieWriter.WriteSession(Response, result.AccessToken, result.RefreshToken, result.Body.ExpiresIn);
        return Ok(result.Body);
    }

    // ---- Profile / sessions (UserManager) ---------------------------------

    [HttpGet("users/{userId:guid}")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(GetUserDetailsResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<GetUserDetailsResponse>> GetUserDetails(Guid userId, CancellationToken ct)
        => Ok(await _users.GetUserDetailsAsync(userId, ct));

    [HttpGet("sessions")]
    [ProducesResponseType(typeof(List<SessionDto>), StatusCodes.Status200OK)]
    public async Task<ActionResult<List<SessionDto>>> GetSessions(CancellationToken ct)
        => Ok(await _users.GetUserSessionsAsync(RequireBearerToken(), ct));

    [HttpPost("status")]
    [ValidateCsrf]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> ChangeStatus([FromBody] ChangeUserStatusRequest request, CancellationToken ct)
    {
        await _users.ChangeUserStatusAsync(request, RequireBearerToken(), ct);
        return NoContent();
    }

    [HttpPost("lock")]
    [ValidateCsrf]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> Lock([FromBody] LockUserAccountRequest request, CancellationToken ct)
    {
        await _users.LockUserAccountAsync(request, RequireBearerToken(), ct);
        AuthCookieWriter.ClearSession(Response);
        return NoContent();
    }

    [HttpPost("change-password")]
    [ValidateCsrf]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> ChangePassword([FromBody] ChangeUserPasswordRequest request, CancellationToken ct)
    {
        await _users.ChangeUserPasswordAsync(request, RequireBearerToken(), ct);
        AuthCookieWriter.ClearSession(Response);
        return NoContent();
    }

    [HttpPost("verify-password")]
    [ProducesResponseType(typeof(VerifyPasswordResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<VerifyPasswordResponse>> VerifyPassword([FromBody] VerifyPasswordRequest request, CancellationToken ct)
        => Ok(await _users.VerifyPasswordAsync(request, RequireBearerToken(), ct));

    /// <summary>
    /// Ends the session: revokes it upstream where possible, and always clears our cookies.
    /// <para>
    /// <c>userId</c> is optional because callers usually cannot supply it — AuthHex returns
    /// <c>user.userId: null</c> in its own login/register response, so a browser has no id
    /// to send. It is resolved from the session token's <c>UserId</c> claim instead.
    /// </para>
    /// <para>
    /// The cookies are cleared even if the upstream revoke fails or no user can be
    /// resolved: a logout that leaves the caller holding a live session cookie is worse
    /// than one that leaves a stale session server-side (which lapses on its own).
    /// </para>
    /// </summary>
    [HttpPost("logout")]
    [AllowAnonymous]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> Logout([FromBody] LogoutRequest? request, CancellationToken ct)
    {
        var userId = request?.UserId ?? ResolveTokenUserId();
        if (userId is not null)
        {
            try
            {
                await _users.LogoutUserAsync(new LogoutRequest { UserId = userId.Value }, ct);
            }
            catch (DomainException)
            {
                // Upstream unreachable or already-revoked — fall through and clear anyway.
            }
        }

        AuthCookieWriter.ClearSession(Response);
        return NoContent();
    }

    /// <summary>AuthHex's identity claim, present when the request carried a valid session.</summary>
    private Guid? ResolveTokenUserId()
        => Guid.TryParse(User.FindFirst(AuthHexClaims.UserId)?.Value, out var id) ? id : null;

    [HttpPut("me")]
    [ValidateCsrf]
    [ProducesResponseType(typeof(UserSummaryDto), StatusCodes.Status200OK)]
    public async Task<ActionResult<UserSummaryDto?>> UpdateMe([FromBody] UpdateUserRequest request, CancellationToken ct)
        => Ok(await _users.UpdateUserAsync(request, RequireBearerToken(), ct));

    // ---- 2FA (UserManager) -------------------------------------------------

    [HttpPost("2fa/initiate")]
    [ValidateCsrf]
    [ProducesResponseType(typeof(TwoFaSetupResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<TwoFaSetupResponse>> InitiateTwoFa(CancellationToken ct)
        => Ok(await _users.InitiateTwoFaSetupAsync(RequireBearerToken(), ct));

    [HttpPost("2fa/complete")]
    [ValidateCsrf]
    [ProducesResponseType(typeof(CompleteTwoFaSetupResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<CompleteTwoFaSetupResponse>> CompleteTwoFa([FromBody] CompleteTwoFaSetupRequest request, CancellationToken ct)
        => Ok(await _users.CompleteTwoFaSetupAsync(request, RequireBearerToken(), ct));

    [HttpPost("2fa/verify")]
    [ValidateCsrf]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> VerifyTwoFa([FromBody] VerifyTwoFaRequest request, CancellationToken ct)
    {
        await _users.VerifyTwoFaAsync(request, RequireBearerToken(), ct);
        return NoContent();
    }

    [HttpPost("2fa/disable")]
    [ValidateCsrf]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> DisableTwoFa([FromBody] DisableTwoFaRequest request, CancellationToken ct)
    {
        await _users.DisableTwoFaAsync(request, RequireBearerToken(), ct);
        return NoContent();
    }

    [HttpGet("2fa/status")]
    [ProducesResponseType(typeof(TwoFaStatusResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<TwoFaStatusResponse>> GetTwoFaStatus(CancellationToken ct)
        => Ok(await _users.GetTwoFaStatusAsync(RequireBearerToken(), ct));

    // ---- Recovery -----------------------------------------------------------

    [HttpPost("recovery/forgot-password")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(ForgotPasswordResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<ForgotPasswordResponse>> ForgotPassword([FromBody] ForgotPasswordRequest request, CancellationToken ct)
        => Ok(await _recovery.ForgotPasswordAsync(request, ct));

    [HttpPost("recovery/verify-otp")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(VerifyRecoveryOtpResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<VerifyRecoveryOtpResponse>> VerifyRecoveryOtp([FromBody] VerifyRecoveryOtpRequest request, CancellationToken ct)
        => Ok(await _recovery.VerifyOtpAsync(request, ct));

    [HttpPost("recovery/reset-password")]
    [AllowAnonymous]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordRequest request, CancellationToken ct)
    {
        await _recovery.ResetPasswordAsync(request, ct);
        return NoContent();
    }

    [HttpPost("recovery/reset-password-token")]
    [AllowAnonymous]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> ResetPasswordWithToken([FromBody] ResetPasswordWithTokenRequest request, CancellationToken ct)
    {
        await _recovery.ResetPasswordWithTokenAsync(request, ct);
        return NoContent();
    }

    // ---- Availability / OTP (AltOptionManager) -----------------------------

    [HttpPost("availability")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(IsAvailableResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<IsAvailableResponse>> CheckAvailability([FromBody] IsAvailableRequest request, CancellationToken ct)
        => Ok(await _alt.IsAvailableAsync(request, ct));

    [HttpPost("otp/send")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(SendOtpResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<SendOtpResponse>> SendOtp([FromBody] SendOtpRequest request, CancellationToken ct)
        => Ok(await _alt.SendOtpAsync(request, ct));

    [HttpPost("otp/verify")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(OtpLoginVerifiedResponse), StatusCodes.Status200OK)]
    public async Task<ActionResult<OtpLoginVerifiedResponse>> VerifyAltOtp([FromBody] VerifyAltOtpRequest request, CancellationToken ct)
    {
        var result = await _alt.VerifyOtpAsync(request, ct);
        AuthCookieWriter.WriteSession(Response, result.AccessToken, result.RefreshToken, result.Body.ExpiresIn);
        return Ok(result.Body);
    }

    // ---- Helpers --------------------------------------------------------------

    /// <summary>The token that authenticated this request — Bearer header if present, else the session cookie.</summary>
    private string RequireBearerToken()
    {
        var header = Request.Headers.Authorization.ToString();
        if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
            return header["Bearer ".Length..];

        if (Request.Cookies.TryGetValue(JwtAuthExtensions.AccessTokenCookie, out var cookieToken) && !string.IsNullOrEmpty(cookieToken))
            return cookieToken;

        // [Authorize] already guaranteed one of the above was present to authenticate this request.
        throw new DomainException(ErrorCodes.AuthUpstreamError, "No bearer token found on an authenticated request.", 500);
    }
}
