using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

public class AuthService : IAuthService
{
    private readonly AppDbContext _db;
    private readonly JwtOptions _jwtOptions;

    public AuthService(AppDbContext db, IOptions<JwtOptions> jwtOptions)
    {
        _db = db;
        _jwtOptions = jwtOptions.Value;
    }

    public async Task<LoginResponse> LoginAsync(LoginRequest req)
    {
        var user = await _db.Users.FirstOrDefaultAsync(u => u.Username == req.Username);
        if (user is null || !BCrypt.Net.BCrypt.Verify(req.Password, user.PasswordHash))
            throw new ValidationException("Invalid username or password.", "INVALID_CREDENTIALS");

        if (!user.IsActive)
            throw new ConflictException("Account is disabled.", "ACCOUNT_DISABLED");

        var (accessToken, refreshToken) = await IssueTokenPairAsync(user);
        return new LoginResponse(
            accessToken,
            refreshToken,
            user.Id,
            user.Username,
            user.FullName,
            user.Role.ToDbString());
    }

    public async Task<TokenResponse> RefreshAsync(RefreshRequest req)
    {
        var storedToken = await FindValidRefreshTokenAsync(req.RefreshToken);
        var user = await _db.Users.FindAsync(storedToken.UserId);
        if (user is null || !user.IsActive)
            throw new ValidationException("Invalid refresh token.", "INVALID_REFRESH_TOKEN");

        storedToken.RevokedAt = DateTimeOffset.UtcNow;

        var (accessToken, refreshToken) = await IssueTokenPairAsync(user, storedToken);
        await WriteAuthAuditEventAsync(user.Id, AuthAuditEventType.TokenRefreshed);

        return new TokenResponse(accessToken, refreshToken);
    }

    public async Task LogoutAsync(LogoutRequest req)
    {
        var storedToken = await FindValidRefreshTokenAsync(req.RefreshToken);
        storedToken.RevokedAt = DateTimeOffset.UtcNow;
        await WriteAuthAuditEventAsync(storedToken.UserId, AuthAuditEventType.UserLogout);
        await _db.SaveChangesAsync();
    }

    public async Task<User> GetCurrentUserAsync(Guid userId)
    {
        var user = await _db.Users.FindAsync(userId);
        if (user is null)
            throw new NotFoundException("User not found.", "USER_NOT_FOUND");
        return user;
    }

    private async Task<(string AccessToken, string RefreshToken)> IssueTokenPairAsync(
        User user,
        RefreshToken? replacedToken = null)
    {
        var accessToken = GenerateJwt(user);
        var refreshTokenValue = GenerateRefreshToken();
        var refreshTokenEntity = new RefreshToken
        {
            Id = Guid.NewGuid(),
            UserId = user.Id,
            TokenHash = HashToken(refreshTokenValue),
            ExpiresAt = DateTimeOffset.UtcNow.AddDays(_jwtOptions.RefreshTokenExpiryDays),
            CreatedAt = DateTimeOffset.UtcNow
        };

        if (replacedToken is not null)
        {
            replacedToken.ReplacedByTokenId = refreshTokenEntity.Id;
        }

        _db.RefreshTokens.Add(refreshTokenEntity);
        await _db.SaveChangesAsync();

        return (accessToken, refreshTokenValue);
    }

    private async Task<RefreshToken> FindValidRefreshTokenAsync(string refreshToken)
    {
        var tokenHash = HashToken(refreshToken);
        var storedToken = await _db.RefreshTokens
            .FirstOrDefaultAsync(t => t.TokenHash == tokenHash);

        if (storedToken is null
            || storedToken.RevokedAt is not null
            || storedToken.ExpiresAt <= DateTimeOffset.UtcNow)
        {
            throw new ValidationException("Invalid refresh token.", "INVALID_REFRESH_TOKEN");
        }

        return storedToken;
    }

    private async Task WriteAuthAuditEventAsync(Guid userId, AuthAuditEventType eventType)
    {
        _db.AuthAuditEvents.Add(new AuthAuditEvent
        {
            Id = Guid.NewGuid(),
            UserId = userId,
            EventType = eventType,
            OccurredAt = DateTimeOffset.UtcNow
        });
        await _db.SaveChangesAsync();
    }

    private string GenerateJwt(User user)
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.Secret));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Name, user.Username),
            new Claim(ClaimTypes.Role, user.Role.ToDbString()),
            new Claim("fullName", user.FullName)
        };

        var token = new JwtSecurityToken(
            issuer: _jwtOptions.Issuer,
            audience: _jwtOptions.Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(_jwtOptions.ExpiryMinutes),
            signingCredentials: creds);

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

    private static string GenerateRefreshToken()
    {
        var bytes = RandomNumberGenerator.GetBytes(64);
        return Convert.ToBase64String(bytes);
    }

    private static string HashToken(string token)
    {
        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
        return Convert.ToHexString(bytes).ToLowerInvariant();
    }
}
