using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using CalledIt.Api.Authentication;
using CalledIt.Domain.Dtos;
using CalledIt.Domain.Services;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace CalledIt.Api.Controllers;

[ApiController]
[Authorize]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    private const string RefreshCookieDev = "Refresh";
    private const string RefreshCookieProd = "__Secure-Refresh";
    private const string RefreshCookiePath = "/api/user/refresh";
    private const string FgpCookieDev = "Fgp";
    private const string FgpCookieProd = "__Host-Fgp";

    private readonly IUserService _userService;
    private readonly IJwtTokenService _jwtTokenService;
    private readonly ITokenRevocationService _tokenRevocationService;
    private readonly IRefreshTokenService _refreshTokenService;
    private readonly JwtOptions _jwtOptions;
    private readonly IWebHostEnvironment _env;
    private readonly IValidator<CreateUserRequestDto> _createValidator;
    private readonly IValidator<LoginRequestDto> _loginValidator;

    public UserController(
        IUserService userService,
        IJwtTokenService jwtTokenService,
        ITokenRevocationService tokenRevocationService,
        IRefreshTokenService refreshTokenService,
        IOptions<JwtOptions> jwtOptions,
        IWebHostEnvironment env,
        IValidator<CreateUserRequestDto> createValidator,
        IValidator<LoginRequestDto> loginValidator)
    {
        _userService = userService;
        _jwtTokenService = jwtTokenService;
        _tokenRevocationService = tokenRevocationService;
        _refreshTokenService = refreshTokenService;
        _jwtOptions = jwtOptions.Value;
        _env = env;
        _createValidator = createValidator;
        _loginValidator = loginValidator;
    }

    [AllowAnonymous]
    [HttpPost]
    [ProducesResponseType(typeof(UserResponseDto), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<ActionResult<UserResponseDto>> Create(CreateUserRequestDto userDto)
    {
        var validationResult = await _createValidator.ValidateAsync(userDto);
        if (!validationResult.IsValid)
        {
            foreach (var error in validationResult.Errors)
            {
                ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
            }

            return ValidationProblem(ModelState);
        }

        try
        {
            var result = await _userService.CreateAsync(userDto);
            return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
        }
        catch (InvalidOperationException exception)
        {
            return Conflict(new { message = exception.Message });
        }
    }

    [HttpGet("{id}")]
    [ProducesResponseType(typeof(UserResponseDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<UserResponseDto>> GetById(int id)
    {
        try
        {
            var result = await _userService.GetByIdAsync(id);
            return Ok(result);
        }
        catch (KeyNotFoundException)
        {
            return NotFound();
        }
    }

    [HttpGet("me")]
    [ProducesResponseType(typeof(UserResponseDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<UserResponseDto>> GetMe()
    {
        var username = User.FindFirstValue(ClaimTypes.Name) ??
            User.FindFirstValue(JwtRegisteredClaimNames.UniqueName);

        if (string.IsNullOrWhiteSpace(username))
        {
            return Unauthorized();
        }

        try
        {
            var result = await _userService.GetByUsernameAsync(username);
            return Ok(result);
        }
        catch (KeyNotFoundException)
        {
            return NotFound();
        }
    }

    [AllowAnonymous]
    [HttpPost("login")]
    [ProducesResponseType(typeof(LoginResponseDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<ActionResult<LoginResponseDto>> Login(LoginRequestDto loginRequestDto)
    {
        var validationResult = await _loginValidator.ValidateAsync(loginRequestDto);
        if (!validationResult.IsValid)
        {
            foreach (var error in validationResult.Errors)
            {
                ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
            }

            return ValidationProblem(ModelState);
        }

        try
        {
            var user = await _userService.LoginAsync(loginRequestDto);
            var result = IssueSession(user);

            var refresh = await _refreshTokenService.IssueAsync(user.Id, _jwtOptions.RefreshTokenDays);
            SetRefreshCookie(refresh.RawToken, refresh.ExpiresAtUtc);

            return Ok(result);
        }
        catch (UnauthorizedAccessException exception)
        {
            return Unauthorized(new { message = exception.Message });
        }
    }

    [AllowAnonymous]
    [HttpPost("refresh")]
    [ProducesResponseType(typeof(LoginResponseDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<ActionResult<LoginResponseDto>> Refresh()
    {
        var refreshCookie = Request.Cookies[GetRefreshCookieName()];
        if (string.IsNullOrEmpty(refreshCookie))
        {
            return Unauthorized(new { message = "Missing refresh token." });
        }

        try
        {
            var rotation = await _refreshTokenService.RotateAsync(
                refreshCookie,
                _jwtOptions.RefreshTokenDays);

            var user = await _userService.GetAuthenticatedAsync(rotation.UserId);
            var result = IssueSession(user);

            SetRefreshCookie(rotation.NewRawToken, rotation.NewExpiresAtUtc);

            return Ok(result);
        }
        catch (UnauthorizedAccessException exception)
        {
            // Clear stale cookies so the client doesn't keep retrying with them.
            DeleteRefreshCookie();
            Response.Cookies.Delete(GetFgpCookieName());
            return Unauthorized(new { message = exception.Message });
        }
        catch (KeyNotFoundException)
        {
            DeleteRefreshCookie();
            Response.Cookies.Delete(GetFgpCookieName());
            return Unauthorized();
        }
    }

    [HttpPost("logout")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> Logout()
    {
        var jti = User.FindFirstValue(JwtRegisteredClaimNames.Jti);
        var expClaim = User.FindFirstValue(JwtRegisteredClaimNames.Exp);

        if (jti is not null && long.TryParse(expClaim, out var expUnix))
        {
            var expiresAtUtc = DateTimeOffset.FromUnixTimeSeconds(expUnix).UtcDateTime;
            await _tokenRevocationService.RevokeAsync(jti, expiresAtUtc);
        }

        // The refresh cookie is scoped to /api/user/refresh, so it isn't sent
        // to /logout. Revoke every non-revoked refresh token for this user
        // instead — this is also the natural "log out everywhere" semantic.
        var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (int.TryParse(userIdClaim, out var userId))
        {
            await _refreshTokenService.RevokeAllForUserAsync(userId);
        }

        Response.Cookies.Delete(GetFgpCookieName());
        DeleteRefreshCookie();

        return NoContent();
    }

    private LoginResponseDto IssueSession(AuthenticatedUserDto user)
    {
        var fgpBytes = RandomNumberGenerator.GetBytes(32);
        var fgpValue = Convert.ToBase64String(fgpBytes);
        var fgpHash = Convert.ToBase64String(
            SHA256.HashData(Encoding.UTF8.GetBytes(fgpValue)));

        var result = _jwtTokenService.CreateToken(user, fgpHash);

        Response.Cookies.Append(GetFgpCookieName(), fgpValue, new CookieOptions
        {
            HttpOnly = true,
            Secure = IsProduction,
            SameSite = SameSiteMode.Strict,
            MaxAge = TimeSpan.FromMinutes(_jwtOptions.ExpirationMinutes),
            Path = "/"
        });

        return result;
    }

    private void SetRefreshCookie(string rawToken, DateTime expiresAtUtc)
    {
        Response.Cookies.Append(GetRefreshCookieName(), rawToken, new CookieOptions
        {
            HttpOnly = true,
            Secure = IsProduction,
            SameSite = SameSiteMode.Strict,
            MaxAge = expiresAtUtc - DateTime.UtcNow,
            Path = RefreshCookiePath
        });
    }

    private void DeleteRefreshCookie()
    {
        // Cookie deletion must match the original Path attribute, otherwise
        // the browser keeps the original cookie around.
        Response.Cookies.Delete(GetRefreshCookieName(), new CookieOptions
        {
            Path = RefreshCookiePath
        });
    }

    private bool IsProduction => !_env.IsDevelopment();

    private string GetRefreshCookieName() => IsProduction ? RefreshCookieProd : RefreshCookieDev;

    private string GetFgpCookieName() => IsProduction ? FgpCookieProd : FgpCookieDev;
}
