package com.prudas.app.security;

import com.prudas.app.config.AppProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.UUID;

/**
 * Issues and validates short-lived, stateless JWTs.
 *
 * Access tokens (type=access) are what protected endpoints accept, TTL
 * measured in minutes. Refresh tokens (type=refresh) are only accepted by
 * POST /api/admin/auth/refresh, TTL measured in days. Keeping access tokens
 * short-lived limits the blast radius if one ever leaks (XSS, log capture,
 * a shared machine); the client is expected to silently refresh.
 *
 * Because this is a stateless design there is no server-side revocation
 * list — logging out simply discards the tokens client-side. If you need
 * true server-side revocation (kill a session immediately on compromise),
 * store each refresh token's jti in Postgres or Redis and check it here;
 * see docs/SECURITY.md for the extension point.
 */
@Component
@RequiredArgsConstructor
public class JwtService {

    private final AppProperties appProperties;

    public String generateAccessToken(String username, String role) {
        return buildToken(username, role, "access", Instant.now().plus(appProperties.jwt().accessTokenTtlMinutes(), ChronoUnit.MINUTES));
    }

    public String generateRefreshToken(String username, String role) {
        return buildToken(username, role, "refresh", Instant.now().plus(appProperties.jwt().refreshTokenTtlDays(), ChronoUnit.DAYS));
    }

    private String buildToken(String username, String role, String type, Instant expiry) {
        Instant now = Instant.now();
        return Jwts.builder()
                .id(UUID.randomUUID().toString())
                .subject(username)
                .issuer(appProperties.jwt().issuer())
                .issuedAt(Date.from(now))
                .expiration(Date.from(expiry))
                .claim("role", role)
                .claim("type", type)
                .signWith(signingKey())
                .compact();
    }

    public Claims parseAndValidate(String token) {
        return Jwts.parser()
                .verifyWith(signingKey())
                .requireIssuer(appProperties.jwt().issuer())
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    public boolean isRefreshToken(Claims claims) {
        return "refresh".equals(claims.get("type", String.class));
    }

    public boolean isAccessToken(Claims claims) {
        return "access".equals(claims.get("type", String.class));
    }

    public long accessTokenTtlSeconds() {
        return appProperties.jwt().accessTokenTtlMinutes() * 60L;
    }

    /**
     * True if the token is malformed, expired, or signed with the wrong key
     * — callers should treat that identically to "not authenticated"
     * rather than surfacing the specific reason to the client.
     */
    public boolean isInvalid(String token) {
        try {
            parseAndValidate(token);
            return false;
        } catch (JwtException | IllegalArgumentException ex) {
            return true;
        }
    }

    private SecretKey signingKey() {
        String secret = appProperties.jwt().secret();
        if (secret == null || secret.isBlank()) {
            // Dev-only fallback so `mvn spring-boot:run` works without any
            // env vars set. StartupSecretsValidator refuses to let this
            // path be reached in the "prod" profile.
            secret = "dev-only-insecure-default-secret-do-not-use-in-prod-32b";
        }
        return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    }
}
