using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using UniVerse.Application.DTOs.Auth;
using UniVerse.Application.Interfaces;
using UniVerse.Domain.Enums;
using System.Security.Cryptography;
using System.Security.Claims;

namespace UniVerse.Api.Controllers;

/// <summary>Аутентификация и управление сессией пользователя.</summary>
[ApiController]
[Route("api/v1/auth")]
[Produces("application/json")]
public class AuthController : ControllerBase
{
    private readonly IAuthService _auth;
    private readonly IConfiguration _config;

    private const string MicrosoftStateCookieName    = "msAuthState";
    private const string MicrosoftReturnUrlCookieName = "msAuthReturnUrl";

    public AuthController(IAuthService auth, IConfiguration config)
    {
        _auth   = auth;
        _config = config;
    }

    /// <summary>Вход через Microsoft Entra ID (SPA/PKCE flow).</summary>
    /// <remarks>
    /// Фронтенд самостоятельно обрабатывает редирект к Microsoft и передаёт сюда
    /// полученный authorization code. В ответ возвращается пара JWT-токенов;
    /// refresh token устанавливается в HttpOnly cookie.
    /// </remarks>
    /// <param name="request">Authorization code и redirect URI из Microsoft OAuth2.</param>
    /// <response code="200">Успешный вход — возвращает access token и данные пользователя.</response>
    /// <response code="400">Неверный или просроченный authorization code.</response>
    [HttpPost("login/microsoft")]
    [ProducesResponseType(typeof(AuthResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<AuthResponse>> LoginMicrosoft([FromBody] LoginMicrosoftRequest request)
    {
        var result = await _auth.LoginWithMicrosoftAsync(request.AuthorizationCode, request.RedirectUri, GetClientIpAddress());
        SetRefreshTokenCookie(result.RefreshToken);
        return Ok(result.Response);
    }

    /// <summary>Инициация server-driven входа через Microsoft (редирект-flow).</summary>
    /// <remarks>
    /// Браузер переходит на этот URL; backend строит Microsoft authorize URL с CSRF state
    /// и редиректит пользователя на `login.microsoftonline.com`.
    /// После успешного входа Microsoft редиректит на `GET /api/v1/auth/callback/microsoft`.
    /// </remarks>
    /// <param name="returnUrl">URL для редиректа после успешного входа (опционально).</param>
    /// <response code="302">Редирект на Microsoft authorize endpoint.</response>
    /// <response code="500">Microsoft authentication не настроен (AzureAd:TenantId/ClientId отсутствуют).</response>
    [HttpGet("login/microsoft")]
    [AllowAnonymous]
    [ProducesResponseType(StatusCodes.Status302Found)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    public IActionResult LoginMicrosoftRedirect([FromQuery] string? returnUrl = null)
    {
        var tenantId = _config["AzureAd:TenantId"];
        var clientId = _config["AzureAd:ClientId"];
        var instance = _config["AzureAd:Instance"] ?? "https://login.microsoftonline.com/";

        if (string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(clientId))
            return Problem("Microsoft authentication is not configured (AzureAd:TenantId/ClientId).", statusCode: StatusCodes.Status500InternalServerError);

        var redirectUri = _config["AzureAd:RedirectUri"] ?? BuildAbsoluteUrl("/api/v1/auth/callback/microsoft");

        var state = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
        Response.Cookies.Append(MicrosoftStateCookieName, state, new CookieOptions
        {
            HttpOnly = true,
            Secure   = Request.IsHttps,
            SameSite = SameSiteMode.Lax,
            Expires  = DateTimeOffset.UtcNow.AddMinutes(10)
        });

        if (!string.IsNullOrWhiteSpace(returnUrl) && IsAllowedReturnUrl(returnUrl))
        {
            Response.Cookies.Append(MicrosoftReturnUrlCookieName, returnUrl, new CookieOptions
            {
                HttpOnly = true,
                Secure   = Request.IsHttps,
                SameSite = SameSiteMode.Lax,
                Expires  = DateTimeOffset.UtcNow.AddMinutes(10)
            });
        }

        var authorizeEndpoint = $"{instance.TrimEnd('/')}/{tenantId}/oauth2/v2.0/authorize";
        var scope = _config["AzureAd:Scopes"] ?? "openid profile email offline_access User.Read";

        var authorizeUrl = QueryHelpers.AddQueryString(authorizeEndpoint, new Dictionary<string, string?>
        {
            ["client_id"]     = clientId,
            ["response_type"] = "code",
            ["redirect_uri"]  = redirectUri,
            ["response_mode"] = "query",
            ["scope"]         = scope,
            ["state"]         = state
        });

        return Redirect(authorizeUrl);
    }

    /// <summary>OAuth2 callback — обмен code на токены (server-driven flow).</summary>
    /// <remarks>
    /// Microsoft редиректит браузер сюда после успешного входа.
    /// Backend валидирует CSRF state, обменивает code на токены,
    /// устанавливает refresh token cookie и редиректит на `returnUrl` с access token в URL-фрагменте.
    /// </remarks>
    /// <param name="code">Authorization code от Microsoft.</param>
    /// <param name="state">CSRF state для верификации.</param>
    /// <param name="error">Код ошибки от Microsoft (если вход не удался).</param>
    /// <param name="errorDescription">Описание ошибки от Microsoft.</param>
    /// <response code="302">Успешный вход — редирект на returnUrl с токеном в URL-фрагменте.</response>
    /// <response code="200">Если returnUrl не задан — возвращает JSON с токенами (удобно для тестирования).</response>
    /// <response code="400">Отсутствует authorization code.</response>
    /// <response code="401">Ошибка от Microsoft или невалидный CSRF state.</response>
    [HttpGet("callback/microsoft")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(AuthResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status302Found)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> CallbackMicrosoft(
        [FromQuery] string? code = null,
        [FromQuery] string? state = null,
        [FromQuery] string? error = null,
        [FromQuery(Name = "error_description")] string? errorDescription = null)
    {
        if (!string.IsNullOrEmpty(error))
        {
            return Unauthorized(new
            {
                error,
                errorDescription
            });
        }

        if (string.IsNullOrWhiteSpace(code))
            return BadRequest(new { error = "missing_code" });

        var expectedState = Request.Cookies[MicrosoftStateCookieName];
        if (string.IsNullOrWhiteSpace(expectedState) || string.IsNullOrWhiteSpace(state) || !string.Equals(expectedState, state, StringComparison.Ordinal))
            return Unauthorized(new { error = "invalid_state" });

        Response.Cookies.Delete(MicrosoftStateCookieName);

        var redirectUri = _config["AzureAd:RedirectUri"] ?? BuildAbsoluteUrl("/api/v1/auth/callback/microsoft");

        var result = await _auth.LoginWithMicrosoftAsync(code, redirectUri, GetClientIpAddress());
        SetRefreshTokenCookie(result.RefreshToken);

        var returnUrl = Request.Cookies[MicrosoftReturnUrlCookieName] ?? _config["AzureAd:PostLoginRedirectUri"];
        Response.Cookies.Delete(MicrosoftReturnUrlCookieName);

        if (!string.IsNullOrWhiteSpace(returnUrl) && IsAllowedReturnUrl(returnUrl))
        {
            // Put access token in URL fragment so it is not sent as Referer to the backend.
            // Frontend can read it from location.hash on the landing page.
            var fragment = $"access_token={Uri.EscapeDataString(result.Response.AccessToken)}&expires_at={Uri.EscapeDataString(result.Response.ExpiresAt.ToString("O"))}";
            return Redirect($"{returnUrl}#{fragment}");
        }

        // Useful for manual testing without frontend: you'll see JSON in the browser.
        return Ok(result.Response);
    }

    /// <summary>Dev-only вход без OAuth (только в Development-окружении).</summary>
    /// <remarks>
    /// Создаёт или находит пользователя по email без реального OAuth flow.
    /// Возвращает 404 в Production и Staging.
    /// </remarks>
    /// <param name="request">Email, отображаемое имя и роль тестового пользователя.</param>
    /// <response code="200">Успешный вход.</response>
    /// <response code="404">Endpoint недоступен вне Development.</response>
    [HttpPost("login/dev")]
    [ProducesResponseType(typeof(AuthResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<AuthResponse>> DevLogin([FromBody] DevLoginRequest request)
    {
        if (!HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment())
            return NotFound();
        var roles = request.Roles?.Count > 0 ? request.Roles : [UserRole.Student];
        var result = await _auth.DevLoginAsync(request.Email, request.DisplayName, roles, GetClientIpAddress());
        SetRefreshTokenCookie(result.RefreshToken);
        return Ok(result.Response);
    }

    /// <summary>Обновление access token по refresh token из HttpOnly cookie.</summary>
    /// <remarks>
    /// Refresh token читается из HttpOnly cookie `refreshToken` (устанавливается при входе).
    /// Возвращает новую пару токенов и обновляет cookie.
    /// </remarks>
    /// <response code="200">Новая пара токенов.</response>
    /// <response code="401">Refresh token отсутствует, просрочен или отозван.</response>
    /// <response code="403">Аккаунт деактивирован или refresh token недействителен.</response>
    [HttpPost("refresh")]
    [ProducesResponseType(typeof(AuthResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    public async Task<ActionResult<AuthResponse>> Refresh()
    {
        var refreshToken = Request.Cookies["refreshToken"];
        if (string.IsNullOrEmpty(refreshToken)) return Unauthorized();
        var result = await _auth.RefreshTokenAsync(refreshToken);
        SetRefreshTokenCookie(result.RefreshToken);
        return Ok(result.Response);
    }

    /// <summary>Выход из системы — отзыв refresh token.</summary>
    /// <remarks>
    /// Инвалидирует текущий refresh token в БД и удаляет cookie.
    /// После этого вызова access token остаётся валидным до истечения его TTL (30 минут).
    /// </remarks>
    /// <response code="204">Выход выполнен успешно.</response>
    /// <response code="401">Требуется аутентификация.</response>
    [Authorize]
    [HttpPost("logout")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> Logout()
    {
        var refreshToken = Request.Cookies["refreshToken"];
        if (!string.IsNullOrEmpty(refreshToken))
            await _auth.RevokeRefreshTokenAsync(refreshToken);
        Response.Cookies.Delete("refreshToken");
        return NoContent();
    }

    /// <summary>Получение профиля текущего авторизованного пользователя.</summary>
    /// <response code="200">Данные текущего пользователя.</response>
    /// <response code="401">Требуется аутентификация.</response>
    /// <response code="404">Пользователь не найден в БД (рассинхронизация токена).</response>
    [Authorize]
    [HttpGet("me")]
    [ProducesResponseType(typeof(UniVerse.Application.DTOs.Users.CurrentUserDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult> Me()
    {
        var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)
            ?? User.FindFirstValue("sub") ?? "0");
        var user = await _auth.GetCurrentUserAsync(userId);
        return Ok(user);
    }

    private string? GetClientIpAddress()
    {
        if (Request.Headers.TryGetValue("X-Forwarded-For", out var forwardedFor))
        {
            var firstForwardedAddress = forwardedFor.ToString().Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
            if (!string.IsNullOrWhiteSpace(firstForwardedAddress))
                return firstForwardedAddress;
        }

        if (Request.Headers.TryGetValue("X-Real-IP", out var realIp) && !string.IsNullOrWhiteSpace(realIp))
            return realIp;

        return HttpContext.Connection.RemoteIpAddress?.ToString();
    }

    private void SetRefreshTokenCookie(string token)
    {
        Response.Cookies.Append("refreshToken", token, new CookieOptions
        {
            HttpOnly = true, Secure = true, SameSite = SameSiteMode.Strict,
            Expires  = DateTime.UtcNow.AddDays(30)
        });
    }

    private string BuildAbsoluteUrl(string path)
    {
        if (!path.StartsWith('/')) path = "/" + path;
        return $"{Request.Scheme}://{Request.Host}{path}";
    }

    private bool IsAllowedReturnUrl(string returnUrl)
    {
        if (Uri.TryCreate(returnUrl, UriKind.Relative, out _))
            return true;

        if (!Uri.TryCreate(returnUrl, UriKind.Absolute, out var absolute))
            return false;

        var allowedOrigins = _config.GetSection("Cors:Origins").Get<string[]>() ?? Array.Empty<string>();
        foreach (var origin in allowedOrigins)
        {
            if (!Uri.TryCreate(origin, UriKind.Absolute, out var allowed))
                continue;
            if (string.Equals(allowed.Scheme, absolute.Scheme, StringComparison.OrdinalIgnoreCase)
                && string.Equals(allowed.Host, absolute.Host, StringComparison.OrdinalIgnoreCase)
                && allowed.Port == absolute.Port)
                return true;
        }

        return false;
    }
}
