package de.effigenix.infrastructure.security;

import de.effigenix.domain.usermanagement.Permission;
import de.effigenix.domain.usermanagement.UserId;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * JWT Token Provider using JJWT 0.12.5 library.
 *
 * Responsibilities:
 * - Generate JWT access tokens with user information and permissions
 * - Generate JWT refresh tokens for session renewal
 * - Validate tokens (signature, expiration)
 * - Extract claims from tokens
 *
 * JWT Structure:
 * - Header: Algorithm (HS256) and token type
 * - Payload: userId, username, permissions, branchId, issued-at, expiration
 * - Signature: HMAC-SHA256 with secret key
 *
 * Infrastructure Layer → Used by JwtSessionManager
 */
@Component
public class JwtTokenProvider {

    private final SecretKey secretKey;
    private final long accessTokenExpiration;
    private final long refreshTokenExpiration;

    /**
     * Constructor with configuration from application.yml.
     *
     * @param secret JWT secret key (minimum 256 bits for HS256)
     * @param accessTokenExpiration Access token expiration in milliseconds
     * @param refreshTokenExpiration Refresh token expiration in milliseconds
     */
    public JwtTokenProvider(
            @Value("${jwt.secret}") String secret,
            @Value("${jwt.expiration}") long accessTokenExpiration,
            @Value("${jwt.refresh-expiration}") long refreshTokenExpiration
    ) {
        this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        this.accessTokenExpiration = accessTokenExpiration;
        this.refreshTokenExpiration = refreshTokenExpiration;
    }

    /**
     * Generates a JWT access token for a user.
     *
     * Claims:
     * - sub (subject): userId
     * - username: username
     * - permissions: comma-separated list of permissions
     * - branchId: user's assigned branch (optional)
     * - iat (issued-at): token creation timestamp
     * - exp (expiration): token expiration timestamp
     *
     * @param userId User's unique identifier
     * @param username User's username
     * @param permissions User's permissions from all assigned roles
     * @param branchId User's assigned branch (nullable)
     * @return JWT access token
     */
    public String generateAccessToken(UserId userId, String username, Set<Permission> permissions, String branchId) {
        Instant now = Instant.now();
        Instant expiration = now.plusMillis(accessTokenExpiration);

        // Convert permissions to comma-separated string
        String permissionsString = permissions.stream()
                .map(Permission::name)
                .collect(Collectors.joining(","));

        var builder = Jwts.builder()
                .subject(userId.value())
                .claim("username", username)
                .claim("permissions", permissionsString)
                .issuedAt(Date.from(now))
                .expiration(Date.from(expiration))
                .signWith(secretKey);

        // Add branchId only if present
        if (branchId != null && !branchId.isBlank()) {
            builder.claim("branchId", branchId);
        }

        return builder.compact();
    }

    /**
     * Generates a JWT refresh token for session renewal.
     * Refresh tokens contain minimal information (only userId).
     *
     * @param userId User's unique identifier
     * @return JWT refresh token
     */
    public String generateRefreshToken(UserId userId) {
        Instant now = Instant.now();
        Instant expiration = now.plusMillis(refreshTokenExpiration);

        return Jwts.builder()
                .subject(userId.value())
                .claim("type", "refresh")
                .issuedAt(Date.from(now))
                .expiration(Date.from(expiration))
                .signWith(secretKey)
                .compact();
    }

    /**
     * Validates a JWT token and returns its claims.
     *
     * @param token JWT token to validate
     * @return Claims if valid
     * @throws io.jsonwebtoken.JwtException if token is invalid or expired
     */
    public Claims validateToken(String token) {
        return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    /**
     * Extracts the userId from a validated token.
     *
     * @param token JWT token
     * @return UserId
     * @throws io.jsonwebtoken.JwtException if token is invalid
     */
    public UserId extractUserId(String token) {
        Claims claims = validateToken(token);
        return UserId.of(claims.getSubject());
    }

    /**
     * Extracts the username from a validated token.
     *
     * @param token JWT token
     * @return username
     * @throws io.jsonwebtoken.JwtException if token is invalid
     */
    public String extractUsername(String token) {
        Claims claims = validateToken(token);
        return claims.get("username", String.class);
    }

    /**
     * Extracts the permissions from a validated token.
     *
     * @param token JWT token
     * @return Set of permissions
     * @throws io.jsonwebtoken.JwtException if token is invalid
     */
    public Set<Permission> extractPermissions(String token) {
        Claims claims = validateToken(token);
        String permissionsString = claims.get("permissions", String.class);

        if (permissionsString == null || permissionsString.isBlank()) {
            return Set.of();
        }

        return Set.of(permissionsString.split(","))
                .stream()
                .map(Permission::valueOf)
                .collect(Collectors.toSet());
    }

    /**
     * Extracts the branchId from a validated token (if present).
     *
     * @param token JWT token
     * @return BranchId or null if not present
     * @throws io.jsonwebtoken.JwtException if token is invalid
     */
    public String extractBranchId(String token) {
        Claims claims = validateToken(token);
        return claims.get("branchId", String.class);
    }

    /**
     * Returns the access token expiration time in milliseconds.
     */
    public long getAccessTokenExpiration() {
        return accessTokenExpiration;
    }

    /**
     * Returns the refresh token expiration time in milliseconds.
     */
    public long getRefreshTokenExpiration() {
        return refreshTokenExpiration;
    }
}
