package com.codebyte.api.auth.service;

import com.codebyte.api.auth.api.dto.ActivateRequest;
import com.codebyte.api.auth.api.dto.LoginRequest;
import com.codebyte.api.auth.api.dto.LogoutRequest;
import com.codebyte.api.auth.api.dto.RefreshRequest;
import com.codebyte.api.auth.api.dto.RegisterRequest;
import com.codebyte.api.auth.api.dto.ResendActivationRequest;
import com.codebyte.api.auth.api.dto.TokenResponse;
import com.codebyte.api.auth.domain.TokenType;
import com.codebyte.api.auth.domain.User;
import com.codebyte.api.auth.repository.UserRepository;
import com.codebyte.api.auth.security.JwtService;
import com.codebyte.api.config.AppProperties;
import java.time.Instant;
import java.util.Locale;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/** Registration, activation, and login/refresh/logout use cases. */
@Service
@Transactional
public class AuthService {

    private static final String DEFAULT_ROLE = "ROLE_USER";

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final TokenService tokenService;
    private final RefreshTokenService refreshTokenService;
    private final JwtService jwtService;
    private final MailService mailService;
    private final AppProperties properties;

    /**
     * Lazily-computed hash used to equalize timing when the email is unknown (anti-enumeration).
     */
    private volatile String dummyHash;

    public AuthService(
            UserRepository userRepository,
            PasswordEncoder passwordEncoder,
            TokenService tokenService,
            RefreshTokenService refreshTokenService,
            JwtService jwtService,
            MailService mailService,
            AppProperties properties) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.tokenService = tokenService;
        this.refreshTokenService = refreshTokenService;
        this.jwtService = jwtService;
        this.mailService = mailService;
        this.properties = properties;
    }

    /**
     * Registers a new, inactive account and emails an activation token. If the email already
     * exists, nothing happens — but the outcome is identical, so a caller cannot enumerate
     * accounts.
     */
    public void register(RegisterRequest request) {
        String email = normalize(request.email());
        if (userRepository.existsByEmailIgnoreCase(email)) {
            return;
        }
        User user = new User(email, passwordEncoder.encode(request.password()));
        user.addRole(DEFAULT_ROLE);
        userRepository.save(user);

        String token =
                tokenService.issue(
                        user, TokenType.ACCOUNT_ACTIVATION, properties.activation().ttl());
        mailService.sendActivationEmail(email, token);
    }

    /** Activates the account tied to a valid, unused, unexpired activation token. */
    public void activate(ActivateRequest request) {
        User user = tokenService.consume(request.token(), TokenType.ACCOUNT_ACTIVATION);
        user.activate(Instant.now());
    }

    /**
     * Resends an activation email for an unactivated account, invalidating prior activation tokens.
     * Does nothing observable if the email is unknown or already activated — always 202.
     */
    public void resendActivation(ResendActivationRequest request) {
        userRepository
                .findByEmailIgnoreCase(normalize(request.email()))
                .filter(user -> !user.isActivatedAccount())
                .ifPresent(
                        user -> {
                            tokenService.invalidateActive(user, TokenType.ACCOUNT_ACTIVATION);
                            String token =
                                    tokenService.issue(
                                            user,
                                            TokenType.ACCOUNT_ACTIVATION,
                                            properties.activation().ttl());
                            mailService.sendActivationEmail(user.getEmail(), token);
                        });
    }

    /**
     * Authenticates and issues a token pair. Unknown email and wrong password are indistinguishable
     * ({@code 401 INVALID_CREDENTIALS}); locked and unactivated accounts are rejected. The failed-
     * attempt counter must survive the thrown 401, hence {@code noRollbackFor}.
     */
    @Transactional(noRollbackFor = InvalidCredentialsException.class)
    public TokenResponse login(LoginRequest request, String userAgent, String ip) {
        Instant now = Instant.now();
        User user = userRepository.findByEmailIgnoreCase(normalize(request.email())).orElse(null);
        if (user == null) {
            passwordEncoder.matches(request.password(), dummyHash()); // equalize timing
            throw new InvalidCredentialsException();
        }
        if (user.isLocked(now)) {
            throw new AccountLockedException();
        }
        if (!passwordEncoder.matches(request.password(), user.getPassword())) {
            user.recordFailedLogin(
                    properties.login().maxFailedAttempts(), properties.login().lockDuration(), now);
            throw new InvalidCredentialsException();
        }
        if (!user.isActivatedAccount()) {
            throw new AccountNotActivatedException();
        }
        user.recordSuccessfulLogin();
        return issueTokens(user, userAgent, ip);
    }

    /**
     * Rotates a refresh token, returning a new pair. Reuse of an already-rotated token revokes the
     * whole chain; that revocation must survive the thrown 401, hence {@code noRollbackFor}.
     */
    @Transactional(noRollbackFor = TokenReuseDetectedException.class)
    public TokenResponse refresh(RefreshRequest request, String userAgent, String ip) {
        RefreshTokenService.Rotation rotation =
                refreshTokenService.rotate(request.refreshToken(), userAgent, ip);
        String accessToken = jwtService.issueAccessToken(rotation.user());
        return new TokenResponse(
                accessToken, rotation.refreshToken(), jwtService.accessTokenTtlSeconds());
    }

    /** Revokes the presented refresh token only. */
    public void logout(LogoutRequest request) {
        refreshTokenService.revoke(request.refreshToken());
    }

    private String dummyHash() {
        String local = dummyHash;
        if (local == null) {
            local = passwordEncoder.encode("timing-equalization-placeholder");
            dummyHash = local;
        }
        return local;
    }

    private TokenResponse issueTokens(User user, String userAgent, String ip) {
        String accessToken = jwtService.issueAccessToken(user);
        String refreshToken = refreshTokenService.issue(user, userAgent, ip);
        return new TokenResponse(accessToken, refreshToken, jwtService.accessTokenTtlSeconds());
    }

    private static String normalize(String email) {
        return email.trim().toLowerCase(Locale.ROOT);
    }
}
