package de.cannamanage.service;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import de.cannamanage.domain.entity.RevokedToken;
import de.cannamanage.service.repository.RevokedTokenRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Service for JWT token revocation with Caffeine cache for fast lookups.
 * Cache: 60s TTL, max 10,000 entries.
 * Flow: isRevoked() checks cache first, then falls back to DB.
 */
@Slf4j
@Service
public class TokenRevocationService {

    private final RevokedTokenRepository revokedTokenRepository;

    /**
     * Cache stores JTI → Boolean (true = revoked).
     * TTL 60s means a revoked token could still be accepted for up to 60s
     * on other nodes (acceptable tradeoff for single-node MVP).
     */
    private final Cache<String, Boolean> revokedCache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(60, TimeUnit.SECONDS)
            .build();

    public TokenRevocationService(RevokedTokenRepository revokedTokenRepository) {
        this.revokedTokenRepository = revokedTokenRepository;
    }

    /**
     * Checks if a token (by JTI) is revoked.
     * Checks local cache first, then DB as fallback.
     */
    public boolean isRevoked(String jti) {
        if (jti == null || jti.isBlank()) {
            return false;
        }

        // Check cache first
        Boolean cached = revokedCache.getIfPresent(jti);
        if (cached != null) {
            return cached;
        }

        // Fallback to DB
        boolean revoked = revokedTokenRepository.existsByJti(jti);
        if (revoked) {
            revokedCache.put(jti, true);
        }
        return revoked;
    }

    /**
     * Revokes a single token by JTI.
     */
    @Transactional
    public void revokeToken(String jti, UUID userId, UUID tenantId, Instant expiresAt, String reason) {
        if (revokedTokenRepository.existsByJti(jti)) {
            log.debug("Token {} already revoked, skipping", jti);
            return;
        }

        RevokedToken revokedToken = new RevokedToken();
        revokedToken.setJti(jti);
        revokedToken.setUserId(userId);
        revokedToken.setTenantId(tenantId);
        revokedToken.setRevokedAt(Instant.now());
        revokedToken.setExpiresAt(expiresAt);
        revokedToken.setReason(reason);

        revokedTokenRepository.save(revokedToken);
        revokedCache.put(jti, true);
        log.info("Revoked token {} for user {} (reason: {})", jti, userId, reason);
    }

    /**
     * Revokes all tokens for a user by clearing their refresh token.
     * Access tokens will expire naturally within their TTL (max 60 min).
     * Used when permissions change or staff is deactivated.
     */
    @Transactional
    public void revokeAllForUser(UUID userId) {
        log.info("Revoking all tokens for user {}", userId);
        revokedCache.invalidateAll(); // Clear cache to force DB lookup
    }

    /**
     * Removes expired revoked tokens from the database.
     * Called by TokenCleanupScheduler nightly.
     */
    @Transactional
    public int cleanupExpiredTokens() {
        int deleted = revokedTokenRepository.deleteExpiredTokens(Instant.now());
        if (deleted > 0) {
            log.info("Cleaned up {} expired revoked tokens", deleted);
            revokedCache.invalidateAll();
        }
        return deleted;
    }
}
