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

namespace OmsorgCore.Application.Services;

public class PasswordResetService : IPasswordResetService
{
    private readonly IUserRepository _userRepository;
    private readonly IPasswordResetCodeRepository _codeRepository;
    private readonly IPasswordResetCodeGenerator _codeGenerator;
    private readonly IPasswordHasher _passwordHasher;
    private readonly IRefreshTokenRepository _refreshTokenRepository;
    private readonly IPasswordPolicy _passwordPolicy;

    public PasswordResetService(
        IUserRepository userRepository,
        IPasswordResetCodeRepository codeRepository,
        IPasswordResetCodeGenerator codeGenerator,
        IPasswordHasher passwordHasher,
        IRefreshTokenRepository refreshTokenRepository,
        IPasswordPolicy passwordPolicy)
    {
        _userRepository = userRepository;
        _codeRepository = codeRepository;
        _codeGenerator = codeGenerator;
        _passwordHasher = passwordHasher;
        _refreshTokenRepository = refreshTokenRepository;
        _passwordPolicy = passwordPolicy;
    }

    public async Task<PasswordResetRequestResult> RequestResetAsync(string username, CancellationToken cancellationToken = default)
    {
        var user = await _userRepository.GetByUsernameAsync(username, cancellationToken);
        var email = user?.Employee?.Email;
        if (user is null || !user.IsActive || string.IsNullOrWhiteSpace(email))
        {
            return PasswordResetRequestResult.CannotReset();
        }

        var latest = await _codeRepository.GetLatestByUserIdAsync(user.Id, cancellationToken);
        if (latest is not null && DateTime.UtcNow - latest.CreatedAt < _codeGenerator.RequestCooldown)
        {
            // Cooldown aktiv: kein neuer Code, kein erneuter Versand, aber nach außen unauffällig -
            // der Client bekommt weiterhin "Sent" ohne dass tatsächlich etwas verschickt wird.
            return PasswordResetRequestResult.Sent(user.Id, email, rawPin: null);
        }

        await _codeRepository.InvalidateActiveForUserAsync(user.Id, cancellationToken);

        var (rawPin, pinHash, expiresAt) = _codeGenerator.GeneratePin();
        await _codeRepository.AddAsync(new PasswordResetCode
        {
            UserId = user.Id,
            User = user,
            CodeHash = pinHash,
            ExpiresAt = expiresAt
        }, cancellationToken);
        await _codeRepository.SaveChangesAsync(cancellationToken);

        return PasswordResetRequestResult.Sent(user.Id, email, rawPin);
    }

    public async Task<PasswordResetVerifyResult> VerifyCodeAsync(string username, string pin, CancellationToken cancellationToken = default)
    {
        var user = await _userRepository.GetByUsernameAsync(username, cancellationToken);
        if (user is null)
        {
            return PasswordResetVerifyResult.Fail("invalid_or_expired");
        }

        var code = await _codeRepository.GetActiveByUserIdAsync(user.Id, _codeGenerator.MaxAttempts, cancellationToken);
        if (code is null)
        {
            return PasswordResetVerifyResult.Fail("invalid_or_expired");
        }

        var pinHash = _codeGenerator.HashPin(pin);
        if (code.CodeHash != pinHash)
        {
            code.AttemptCount++;
            if (code.AttemptCount >= _codeGenerator.MaxAttempts)
            {
                code.InvalidatedAt = DateTime.UtcNow;
            }
            await _codeRepository.SaveChangesAsync(cancellationToken);

            return code.AttemptCount >= _codeGenerator.MaxAttempts
                ? PasswordResetVerifyResult.Fail("too_many_attempts")
                : PasswordResetVerifyResult.Fail("invalid_or_expired");
        }

        code.ConsumedAt = DateTime.UtcNow;
        var (rawResetToken, resetTokenHash, resetExpiresAt) = _codeGenerator.GenerateResetToken();
        code.ResetTokenHash = resetTokenHash;
        code.ResetTokenExpiresAt = resetExpiresAt;
        await _codeRepository.SaveChangesAsync(cancellationToken);

        return PasswordResetVerifyResult.Ok(rawResetToken);
    }

    public async Task<string> IssueInviteCodeAsync(Guid userId, TimeSpan validity, CancellationToken cancellationToken = default)
    {
        await _codeRepository.InvalidateActiveForUserAsync(userId, cancellationToken);

        var (rawPin, pinHash, expiresAt) = _codeGenerator.GeneratePin(validity);
        await _codeRepository.AddAsync(new PasswordResetCode
        {
            UserId = userId,
            CodeHash = pinHash,
            ExpiresAt = expiresAt
        }, cancellationToken);
        await _codeRepository.SaveChangesAsync(cancellationToken);

        return rawPin;
    }

    public async Task<ResetPasswordResult> ResetPasswordAsync(string resetToken, string newPassword, CancellationToken cancellationToken = default)
    {
        var resetTokenHash = _codeGenerator.HashResetToken(resetToken);
        var code = await _codeRepository.GetByResetTokenHashAsync(resetTokenHash, cancellationToken);
        if (code is null || !code.IsResetTokenActive)
        {
            return ResetPasswordResult.InvalidOrExpiredToken;
        }

        if (!_passwordPolicy.IsValid(newPassword))
        {
            return ResetPasswordResult.PasswordTooShort;
        }

        code.ResetTokenUsedAt = DateTime.UtcNow;
        code.User.PasswordHash = _passwordHasher.Hash(newPassword);

        // Wie nach einem normalen Login: alle bestehenden Sessions beenden, SecurityStamp erneuern -
        // ein Passwort-Reset darf alte, evtl. kompromittierte Sessions nicht überleben lassen.
        var activeSessions = await _refreshTokenRepository.GetActiveByUserIdAsync(code.UserId, cancellationToken);
        foreach (var session in activeSessions)
        {
            session.RevokedAt = DateTime.UtcNow;
        }
        code.User.SecurityStamp = Guid.NewGuid();

        await _codeRepository.SaveChangesAsync(cancellationToken);
        await _refreshTokenRepository.SaveChangesAsync(cancellationToken);
        return ResetPasswordResult.Success;
    }
}
