using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Text.RegularExpressions;
using ApiDenuncias.Configuration;
using GestionaDenuncias.Shared.Models;
using ApiDenuncias.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace ApiDenuncias.Controllers;

[ApiController]
[Route("api/auth")]
public sealed class AuthController : ControllerBase
{
    private readonly GlobalLeaksClient _globalLeaksClient;
    private readonly GlobalLeaksSessionStore _sessionStore;
    private readonly PendingGlobalLeaksLoginStore _pendingLoginStore;
    private readonly LoginRateLimiter _rateLimiter;
    private readonly JwtOptions _jwtOptions;
    private readonly GlobalLeaksOptions _globalLeaksOptions;
    private readonly ILogger<AuthController> _logger;

    public AuthController(
        GlobalLeaksClient globalLeaksClient,
        GlobalLeaksSessionStore sessionStore,
        PendingGlobalLeaksLoginStore pendingLoginStore,
        LoginRateLimiter rateLimiter,
        IOptions<JwtOptions> jwtOptions,
        IOptions<GlobalLeaksOptions> globalLeaksOptions,
        ILogger<AuthController> logger)
    {
        _globalLeaksClient = globalLeaksClient;
        _sessionStore = sessionStore;
        _pendingLoginStore = pendingLoginStore;
        _rateLimiter = rateLimiter;
        _jwtOptions = jwtOptions.Value;
        _globalLeaksOptions = globalLeaksOptions.Value;
        _logger = logger;
    }

    [HttpPost("login/prepare")]
    [AllowAnonymous]
    public async Task<ActionResult<ApiLoginPrepareResponse>> PrepareLogin(
        ApiLoginPrepareRequest request,
        CancellationToken cancellationToken)
    {
        var usernameForLogs = string.IsNullOrWhiteSpace(request.Username) ? "(sin usuario)" : request.Username.Trim();
        var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        if (!_rateLimiter.AllowAttempt(ip))
        {
            return StatusCode(StatusCodes.Status429TooManyRequests, new ApiError("Demasiados intentos. Espera un minuto."));
        }

        if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
        {
            return BadRequest(new ApiError("Debes indicar usuario y contrasena."));
        }

        try
        {
            var loginTimeoutSeconds = Math.Clamp(_globalLeaksOptions.TimeoutSeconds + 20, 30, 140);
            using var loginCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            loginCancellation.CancelAfter(TimeSpan.FromSeconds(loginTimeoutSeconds));

            _logger.LogInformation(
                "Preparando login GlobalLeaks para {Username} contra {BaseUrl}. Timeout total API={TimeoutSeconds}s",
                usernameForLogs,
                _globalLeaksOptions.BaseUrl,
                loginTimeoutSeconds);

            var prepared = await _globalLeaksClient.PrepareLoginAsync(
                request.Username.Trim(),
                request.Password,
                loginCancellation.Token);

            var pending = _pendingLoginStore.Create(
                prepared.Username,
                request.Password,
                prepared.FinalPassword,
                prepared.TokenAnswer);

            return Ok(new ApiLoginPrepareResponse(pending.Id, pending.Username, pending.ExpiresAtUtc));
        }
        catch (GlobalLeaksValidationException ex)
        {
            _logger.LogWarning(
                "GlobalLeaks rechazo la preparacion del login de {Username}. Status={StatusCode}. Mensaje={Message}",
                usernameForLogs,
                ex.StatusCode,
                ex.Message);
            return StatusCode(ex.StatusCode, new ApiError(ex.Message));
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            return StatusCode(
                StatusCodes.Status504GatewayTimeout,
                new ApiError($"GlobalLeaks no ha preparado el login en {Math.Clamp(_globalLeaksOptions.TimeoutSeconds + 20, 30, 140)} segundos ({_globalLeaksOptions.BaseUrl})."));
        }
        catch (HttpRequestException ex)
        {
            return StatusCode(
                StatusCodes.Status502BadGateway,
                new ApiError($"No se ha podido conectar con GlobalLeaks ({_globalLeaksOptions.BaseUrl}): {ex.Message}"));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error no controlado preparando login de {Username}", usernameForLogs);
            return StatusCode(
                StatusCodes.Status500InternalServerError,
                new ApiError($"No se ha podido preparar el inicio de sesion en la API: {ex.Message}"));
        }
    }

    [HttpPost("login/complete")]
    [AllowAnonymous]
    public async Task<ActionResult<ApiLoginResponse>> CompleteLogin(
        ApiLoginCompleteRequest request,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(request.PendingLoginId))
        {
            return BadRequest(new ApiError("La preparacion del login ha caducado. Vuelve a introducir usuario y contrasena."));
        }

        if (string.IsNullOrWhiteSpace(request.Authcode) || !Regex.IsMatch(request.Authcode.Trim(), @"^\d{6}$"))
        {
            return BadRequest(new ApiError("El codigo 2FA debe tener exactamente 6 digitos."));
        }

        PendingGlobalLeaksLogin pending;
        try
        {
            pending = _pendingLoginStore.Get(request.PendingLoginId);
        }
        catch (InvalidOperationException ex)
        {
            return BadRequest(new ApiError(ex.Message));
        }

        try
        {
            var loginTimeoutSeconds = Math.Clamp(_globalLeaksOptions.TimeoutSeconds + 20, 30, 140);
            using var loginCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            loginCancellation.CancelAfter(TimeSpan.FromSeconds(loginTimeoutSeconds));

            var session = await _globalLeaksClient.CompleteLoginAsync(
                pending.Username,
                pending.FinalPassword,
                pending.TokenAnswer,
                request.Authcode.Trim(),
                loginCancellation.Token);

            var response = await CreateLoginResponseAsync(
                pending.Username,
                pending.Password,
                session,
                loginCancellation.Token);

            _pendingLoginStore.Remove(pending.Id);
            return Ok(response);
        }
        catch (GlobalLeaksValidationException ex)
        {
            _logger.LogWarning(
                "GlobalLeaks rechazo el 2FA de {Username}. Status={StatusCode}. Mensaje={Message}",
                pending.Username,
                ex.StatusCode,
                ex.Message);
            return StatusCode(ex.StatusCode, new ApiError(ex.Message));
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            return StatusCode(
                StatusCodes.Status504GatewayTimeout,
                new ApiError($"GlobalLeaks no ha completado el login en {Math.Clamp(_globalLeaksOptions.TimeoutSeconds + 20, 30, 140)} segundos ({_globalLeaksOptions.BaseUrl})."));
        }
        catch (HttpRequestException ex)
        {
            return StatusCode(
                StatusCodes.Status502BadGateway,
                new ApiError($"No se ha podido conectar con GlobalLeaks ({_globalLeaksOptions.BaseUrl}): {ex.Message}"));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error no controlado completando login de {Username}", pending.Username);
            return StatusCode(
                StatusCodes.Status500InternalServerError,
                new ApiError($"No se ha podido completar el inicio de sesion en la API: {ex.Message}"));
        }
    }

    [HttpPost("login")]
    [AllowAnonymous]
    public async Task<ActionResult<ApiLoginResponse>> Login(LoginRequest request, CancellationToken cancellationToken)
    {
        var usernameForLogs = string.IsNullOrWhiteSpace(request.Username) ? "(sin usuario)" : request.Username.Trim();
        var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        if (!_rateLimiter.AllowAttempt(ip))
        {
            return StatusCode(StatusCodes.Status429TooManyRequests, new ApiError("Demasiados intentos. Espera un minuto."));
        }

        if (string.IsNullOrWhiteSpace(request.Username) ||
            string.IsNullOrWhiteSpace(request.Password) ||
            string.IsNullOrWhiteSpace(request.Authcode))
        {
            return BadRequest(new ApiError("Debes indicar usuario, contrasena y codigo 2FA."));
        }

        if (!Regex.IsMatch(request.Authcode.Trim(), @"^\d{6}$"))
        {
            return BadRequest(new ApiError("El codigo 2FA debe tener exactamente 6 digitos."));
        }

        try
        {
            var loginTimeoutSeconds = Math.Clamp(_globalLeaksOptions.TimeoutSeconds + 20, 30, 140);
            using var loginCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            loginCancellation.CancelAfter(TimeSpan.FromSeconds(loginTimeoutSeconds));

            _logger.LogInformation(
                "Iniciando login GlobalLeaks para {Username} contra {BaseUrl}. Timeout total API={TimeoutSeconds}s",
                usernameForLogs,
                _globalLeaksOptions.BaseUrl,
                loginTimeoutSeconds);

            var session = await _globalLeaksClient.LoginAsync(
                request.Username.Trim(),
                request.Password,
                request.Authcode.Trim(),
                loginCancellation.Token);

            return Ok(await CreateLoginResponseAsync(
                request.Username.Trim(),
                request.Password,
                session,
                loginCancellation.Token));
        }
        catch (GlobalLeaksValidationException ex)
        {
            _logger.LogWarning(
                "GlobalLeaks rechazo el login de {Username}. Status={StatusCode}. Mensaje={Message}",
                usernameForLogs,
                ex.StatusCode,
                ex.Message);
            return StatusCode(ex.StatusCode, new ApiError(ex.Message));
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            _logger.LogWarning(
                "Timeout de GlobalLeaks al iniciar sesion para {Username}. BaseUrl={BaseUrl}. Timeout={TimeoutSeconds}s",
                usernameForLogs,
                _globalLeaksOptions.BaseUrl,
                Math.Clamp(_globalLeaksOptions.TimeoutSeconds + 20, 30, 140));
            return StatusCode(
                StatusCodes.Status504GatewayTimeout,
                new ApiError($"GlobalLeaks no ha completado el login en {Math.Clamp(_globalLeaksOptions.TimeoutSeconds + 20, 30, 140)} segundos ({_globalLeaksOptions.BaseUrl}). Revisa el visor de eventos de ApiDenuncias para ver el paso exacto."));
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(
                ex,
                "No se ha podido conectar con GlobalLeaks para {Username}. BaseUrl={BaseUrl}",
                usernameForLogs,
                _globalLeaksOptions.BaseUrl);
            return StatusCode(
                StatusCodes.Status502BadGateway,
                new ApiError($"No se ha podido conectar con GlobalLeaks ({_globalLeaksOptions.BaseUrl}): {ex.Message}"));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error no controlado durante el login de {Username}", usernameForLogs);
            return StatusCode(
                StatusCodes.Status500InternalServerError,
                new ApiError($"No se ha podido completar el inicio de sesion en la API: {ex.Message}"));
        }
    }

    [HttpPost("logout")]
    [Authorize]
    public async Task<IActionResult> Logout(CancellationToken cancellationToken)
    {
        var username = User.Identity?.Name;
        if (!string.IsNullOrWhiteSpace(username))
        {
            await _sessionStore.DeleteAsync(username, cancellationToken);
        }

        return Ok(new { ok = true });
    }

    private async Task<ApiLoginResponse> CreateLoginResponseAsync(
        string fallbackUsername,
        string password,
        GlSession session,
        CancellationToken cancellationToken)
    {
        var username = string.IsNullOrWhiteSpace(session.Username)
            ? fallbackUsername.Trim()
            : session.Username.Trim();

        _logger.LogInformation("Login GlobalLeaks validado para {Username}. Guardando sesion cifrada.", username);
        await _sessionStore.SaveAsync(username, password, session.Id, session.Role, cancellationToken);
        _logger.LogInformation("Sesion GlobalLeaks guardada para {Username}. Generando JWT.", username);

        var expiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(Math.Max(5, _jwtOptions.ExpirationMinutes));
        var token = CreateJwt(username, session.Role, expiresAtUtc);
        return new ApiLoginResponse(username, token, expiresAtUtc, session.Role);
    }

    private string CreateJwt(string username, string? role, DateTimeOffset expiresAtUtc)
    {
        if (string.IsNullOrWhiteSpace(_jwtOptions.SigningKey))
        {
            throw new InvalidOperationException("Falta Jwt:SigningKey en la configuracion de ApiDenuncias.");
        }

        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, username),
            new(ClaimTypes.Name, username),
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N"))
        };

        if (!string.IsNullOrWhiteSpace(role))
        {
            claims.Add(new Claim("gl_role", role));
        }

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SigningKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var token = new JwtSecurityToken(
            issuer: _jwtOptions.Issuer,
            audience: _jwtOptions.Audience,
            claims: claims,
            notBefore: DateTime.UtcNow,
            expires: expiresAtUtc.UtcDateTime,
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}
