using OmsorgCore.Application.Abstractions;
using OmsorgCore.Application.Models;
using OmsorgCore.Domain.Entities;

namespace OmsorgCore.Application.Services;

public class AuthService : IAuthService
{
    private readonly IUserRepository _userRepository;
    private readonly IRefreshTokenRepository _refreshTokenRepository;
    private readonly IPasswordHasher _passwordHasher;
    private readonly IJwtTokenGenerator _tokenGenerator;
    private readonly IRefreshTokenGenerator _refreshTokenGenerator;
    private readonly IPermissionService _permissionService;
    private readonly ILoginAttemptRepository _loginAttemptRepository;
    private readonly ILoginLockoutPolicy _loginLockoutPolicy;

    public AuthService(
        IUserRepository userRepository,
        IRefreshTokenRepository refreshTokenRepository,
        IPasswordHasher passwordHasher,
        IJwtTokenGenerator tokenGenerator,
        IRefreshTokenGenerator refreshTokenGenerator,
        IPermissionService permissionService,
        ILoginAttemptRepository loginAttemptRepository,
        ILoginLockoutPolicy loginLockoutPolicy)
    {
        _userRepository = userRepository;
        _refreshTokenRepository = refreshTokenRepository;
        _passwordHasher = passwordHasher;
        _tokenGenerator = tokenGenerator;
        _refreshTokenGenerator = refreshTokenGenerator;
        _permissionService = permissionService;
        _loginAttemptRepository = loginAttemptRepository;
        _loginLockoutPolicy = loginLockoutPolicy;
    }

    public async Task<AuthResult> LoginAsync(string username, string password, string ipAddress, CancellationToken cancellationToken = default)
    {
        var lockoutSince = DateTime.UtcNow - _loginLockoutPolicy.LockoutWindow;
        var recentFailures = await _loginAttemptRepository.CountRecentFailuresAsync(ipAddress, lockoutSince, cancellationToken);
        if (recentFailures >= _loginLockoutPolicy.MaxFailures)
        {
            return AuthResult.LockedOut();
        }

        var user = await _userRepository.GetByUsernameAsync(username, cancellationToken);
        if (user is null || !user.IsActive || !_passwordHasher.Verify(password, user.PasswordHash))
        {
            await _loginAttemptRepository.AddAsync(new LoginAttempt
            {
                IpAddress = ipAddress,
                Username = username,
                Succeeded = false
            }, cancellationToken);
            await _loginAttemptRepository.SaveChangesAsync(cancellationToken);

            return AuthResult.Fail();
        }

        await _loginAttemptRepository.ClearFailuresAsync(ipAddress, cancellationToken);
        await _loginAttemptRepository.SaveChangesAsync(cancellationToken);

        await EndOtherSessionsAsync(user, cancellationToken);

        return await IssueTokenPairAsync(user, cancellationToken);
    }

    // Erzwingt "ein User = eine aktive Session": widerruft alle bisherigen Refresh-Tokens dieses
    // Users und würfelt den SecurityStamp neu, damit auch bereits ausgestellte Access-Tokens
    // anderer Sessions sofort ungültig werden (derselbe Killswitch-Mechanismus wie SessionAdminService).
    private async Task EndOtherSessionsAsync(User user, CancellationToken cancellationToken)
    {
        var activeSessions = await _refreshTokenRepository.GetActiveByUserIdAsync(user.Id, cancellationToken);
        foreach (var session in activeSessions)
        {
            session.RevokedAt = DateTime.UtcNow;
        }

        user.SecurityStamp = Guid.NewGuid();
    }

    public async Task<AuthResult> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default)
    {
        var tokenHash = _refreshTokenGenerator.HashToken(refreshToken);
        var existing = await _refreshTokenRepository.GetByTokenHashAsync(tokenHash, cancellationToken);
        if (existing is null || !existing.IsActive)
        {
            return AuthResult.Fail();
        }

        existing.RevokedAt = DateTime.UtcNow;

        return await IssueTokenPairAsync(existing.User, cancellationToken, replaces: existing);
    }

    public async Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default)
    {
        var tokenHash = _refreshTokenGenerator.HashToken(refreshToken);
        var existing = await _refreshTokenRepository.GetByTokenHashAsync(tokenHash, cancellationToken);
        if (existing is null || existing.RevokedAt is not null)
        {
            return;
        }

        existing.RevokedAt = DateTime.UtcNow;
        await _refreshTokenRepository.SaveChangesAsync(cancellationToken);
    }

    public async Task<UserProfile?> GetProfileAsync(Guid userId, CancellationToken cancellationToken = default)
    {
        var user = await _userRepository.GetByIdWithPermissionsAsync(userId, cancellationToken);
        if (user is null || !user.IsActive)
        {
            return null;
        }

        var permissions = await _permissionService.GetGrantedPermissionsAsync(userId, cancellationToken);
        return new UserProfile(
            user.Id,
            user.Username,
            user.Role.Name,
            permissions,
            user.EmployeeId,
            user.Employee?.FirstName,
            user.Employee?.LastName,
            user.Employee?.Email,
            user.Employee?.PhoneNumber,
            user.Employee?.AvatarFileName);
    }

    private async Task<AuthResult> IssueTokenPairAsync(User user, CancellationToken cancellationToken, RefreshToken? replaces = null)
    {
        var (accessToken, accessTokenExpiresAt) = _tokenGenerator.GenerateToken(user);
        var (rawRefreshToken, refreshTokenHash, refreshTokenExpiresAt) = _refreshTokenGenerator.GenerateToken();

        var newToken = new RefreshToken
        {
            UserId = user.Id,
            User = user,
            TokenHash = refreshTokenHash,
            ExpiresAt = refreshTokenExpiresAt
        };
        await _refreshTokenRepository.AddAsync(newToken, cancellationToken);

        if (replaces is not null)
        {
            replaces.ReplacedByTokenId = newToken.Id;
        }

        await _refreshTokenRepository.SaveChangesAsync(cancellationToken);

        return AuthResult.Ok(user.Id, user.Username, accessToken, rawRefreshToken, accessTokenExpiresAt, user.MustChangePassword);
    }
}
