package com.codebyte.api.auth.service;

import com.codebyte.api.auth.domain.OneTimeToken;
import com.codebyte.api.auth.domain.TokenType;
import com.codebyte.api.auth.domain.User;
import com.codebyte.api.auth.repository.OneTimeTokenRepository;
import com.codebyte.api.common.util.HashUtil;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Issues and consumes single-use tokens. Only the SHA-256 hash of a token is persisted;
 * verification hashes the presented token and looks it up, so the raw token is never stored or
 * compared directly.
 */
@Service
@Transactional
public class TokenService {

    private static final int TOKEN_BYTES = 32;

    private final OneTimeTokenRepository repository;
    private final SecureRandom secureRandom = new SecureRandom();

    public TokenService(OneTimeTokenRepository repository) {
        this.repository = repository;
    }

    /** Issues a new token of {@code type} for {@code user}, returning the raw (unhashed) value. */
    public String issue(User user, TokenType type, Duration ttl) {
        String rawToken = randomToken();
        OneTimeToken token =
                new OneTimeToken(user, HashUtil.sha256Hex(rawToken), type, Instant.now().plus(ttl));
        repository.save(token);
        return rawToken;
    }

    /**
     * Consumes a token, returning its user. Throws {@link InvalidTokenException} if the token is
     * unknown, already consumed, or expired.
     */
    public User consume(String rawToken, TokenType type) {
        OneTimeToken token =
                repository
                        .findByTokenHashAndType(HashUtil.sha256Hex(rawToken), type)
                        .orElseThrow(() -> new InvalidTokenException(type));
        Instant now = Instant.now();
        if (token.isConsumed() || token.isExpired(now)) {
            throw new InvalidTokenException(type);
        }
        token.setConsumedAt(now);
        return token.getUser();
    }

    /** Invalidates all still-active tokens of a kind for a user (used by resend / reset). */
    public void invalidateActive(User user, TokenType type) {
        repository.consumeAllActive(user, type, Instant.now());
    }

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