package com.codebyte.api.auth.service;

import com.codebyte.api.auth.domain.RefreshToken;
import com.codebyte.api.auth.domain.User;
import com.codebyte.api.auth.repository.RefreshTokenRepository;
import com.codebyte.api.common.util.HashUtil;
import com.codebyte.api.config.AppProperties;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Base64;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Issues, rotates, and revokes opaque refresh tokens. Rotation is single-use: presenting a token
 * that was already rotated (revoked) is treated as theft and revokes the user's entire chain.
 */
@Service
@Transactional
public class RefreshTokenService {

    private static final int TOKEN_BYTES = 32;

    private final RefreshTokenRepository repository;
    private final AppProperties properties;
    private final SecureRandom secureRandom = new SecureRandom();

    public RefreshTokenService(RefreshTokenRepository repository, AppProperties properties) {
        this.repository = repository;
        this.properties = properties;
    }

    /** The user plus the freshly-issued raw refresh token produced by a rotation. */
    public record Rotation(User user, String refreshToken) {}

    /** Issues a new refresh token for a user, returning the raw (unhashed) value. */
    public String issue(User user, String userAgent, String ip) {
        return persistNew(user, userAgent, ip).raw();
    }

    /**
     * Rotates a valid refresh token: revokes it, issues a successor, and returns the user + new
     * token. Throws {@link InvalidRefreshTokenException} if unknown/expired and {@link
     * TokenReuseDetectedException} (after revoking the whole chain) if the token was already
     * rotated.
     */
    @Transactional(noRollbackFor = TokenReuseDetectedException.class)
    public Rotation rotate(String rawToken, String userAgent, String ip) {
        RefreshToken current =
                repository
                        .findByTokenHash(HashUtil.sha256Hex(rawToken))
                        .orElseThrow(InvalidRefreshTokenException::new);
        Instant now = Instant.now();
        if (current.isRevoked()) {
            if (current.getReplacedBy() != null) {
                // The token was already ROTATED and is being presented again: token theft.
                // Revoke the whole chain for this user.
                repository.revokeAllActiveForUser(current.getUser(), now);
                throw new TokenReuseDetectedException();
            }
            // Revoked for another reason (logout, prior chain revocation): simply invalid.
            throw new InvalidRefreshTokenException();
        }
        if (current.isExpired(now)) {
            throw new InvalidRefreshTokenException();
        }
        Issued successor = persistNew(current.getUser(), userAgent, ip);
        current.setRevokedAt(now);
        current.setReplacedBy(successor.entity());
        return new Rotation(current.getUser(), successor.raw());
    }

    /** Revokes a single refresh token (logout). Unknown or already-revoked tokens are a no-op. */
    public void revoke(String rawToken) {
        repository
                .findByTokenHash(HashUtil.sha256Hex(rawToken))
                .filter(token -> !token.isRevoked())
                .ifPresent(token -> token.setRevokedAt(Instant.now()));
    }

    /** Revokes every active refresh token for a user (used on password reset). */
    public void revokeAll(User user) {
        repository.revokeAllActiveForUser(user, Instant.now());
    }

    private Issued persistNew(User user, String userAgent, String ip) {
        String raw = randomToken();
        RefreshToken token =
                new RefreshToken(
                        user,
                        HashUtil.sha256Hex(raw),
                        Instant.now().plus(properties.jwt().refreshTtl()),
                        userAgent,
                        ip);
        return new Issued(repository.save(token), raw);
    }

    private String randomToken() {
        byte[] bytes = new byte[TOKEN_BYTES];
        secureRandom.nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }

    private record Issued(RefreshToken entity, String raw) {}
}
