package com.spring.infra.security.jwt;

import java.security.Key;
import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.spring.infra.security.domain.UserPrincipal;
import com.spring.infra.security.error.SecurityAuthException;
import com.spring.infra.security.error.SecurityExceptionRule;
import com.spring.infra.security.service.RefreshTokenService;
import com.spring.infra.security.service.UserAuthenticationService;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

/**
 * JWT 토큰 관련 서비스를 제공하는 클래스입니다.
 * 
 * <p>이 클래스는 JWT 토큰의 생성, 검증, 해석 등 다양한 토큰 관련 기능을 제공합니다.</p>
 * 
 * @author mindol
 * @version 1.0
 */
@Service
public class JwtTokenService {

    private final JwtTokenUtil jwtTokenUtil;
    private final JwtTokenGenerator jwtTokenGenerator;
    private final UserAuthenticationService userAuthenticationService;
    private final RefreshTokenService refreshTokenService;
    private final Key accessSecretKey;
    private final Key refreshSecretKey;
    private final long accessExpiration;
    private final long refreshExpiration;

    public JwtTokenService(
        JwtTokenUtil jwtTokenUtil,
        JwtTokenGenerator jwtTokenGenerator,
        UserAuthenticationService userAuthenticationService,
        RefreshTokenService refreshTokenService,
        JwtProperties jwtProperties
    ) {
        this.jwtTokenUtil = jwtTokenUtil;
        this.jwtTokenGenerator = jwtTokenGenerator;
        this.userAuthenticationService = userAuthenticationService;
        this.refreshTokenService = refreshTokenService;
        this.accessSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret());
        this.refreshSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getRefreshToken().getSecret());
        this.accessExpiration = jwtProperties.getAccessToken().getExpiration();
        this.refreshExpiration = jwtProperties.getRefreshToken().getExpiration();
    }

    /**
     * 액세스 토큰을 생성하고 응답 헤더에 설정합니다.
     * 
     * @param response HTTP 응답 객체
     * @param authentication 인증 정보
     * @return 생성된 액세스 토큰
     */
    public String generateAccessToken(HttpServletResponse response, Authentication authentication) {
        String accessToken = jwtTokenGenerator.generateAccessToken(authentication);
        ResponseCookie cookie = setTokenToCookie(JwtTokenRule.ACCESS_PREFIX.getValue(), accessToken, accessExpiration);
        response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString());
        response.setHeader(HttpHeaders.AUTHORIZATION, JwtTokenRule.BEARER_PREFIX.getValue() + accessToken);
        return accessToken;
    }

    /**
     * 리프레시 토큰을 생성하고 쿠키에 저장한다.
     * 
     * @param response HTTP 응답 객체
     * @param authentication 인증 정보
     * @return 생성된 리프레시 토큰
     */
    @Transactional
    public String generateRefreshToken(HttpServletResponse response, Authentication authentication) {
        String refreshToken = jwtTokenGenerator.generateRefreshToken(authentication);
        ResponseCookie cookie = setTokenToCookie(JwtTokenRule.REFRESH_PREFIX.getValue(), refreshToken, refreshExpiration);
        response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString());
        return refreshToken;
    }

    /**
     * 토큰을 쿠키로 설정합니다.
     * 
     * @param tokenPrefix 토큰 접두사
     * @param token 토큰 값
     * @param maxAgeSeconds 쿠키 유효 시간(초)
     * @return 생성된 ResponseCookie 객체
     */
    private ResponseCookie setTokenToCookie(String tokenPrefix, String token, long maxAgeMinutes) {
        return ResponseCookie.from(tokenPrefix, token)
                .path("/")
                .maxAge(Duration.ofMinutes(maxAgeMinutes))
                .httpOnly(true)
                .sameSite("None")
                .secure(true)
                .build();
    }

    /**
     * 액세스 토큰의 유효성을 검증합니다.
     * 
     * @param token 검증할 토큰
     */
    public void validateToken(String token) {
        jwtTokenUtil.tokenStatus(token, accessSecretKey);
    }

    /**
     * 액세스 토큰의 유효성을 검증합니다.
     * 
     * @param token 검증할 토큰
     * @return 토큰의 유효성 여부
     */
    public boolean validateAccessToken(String token) {
        return jwtTokenUtil.getTokenStatus(token, accessSecretKey) == JwtTokenStatus.AUTHENTICATED;
    }

    /**
     * 리프레시 토큰의 유효성을 검증합니다.
     * 
     * @param token 검증할 토큰
     * @return 토큰의 유효성 여부
     */
    public boolean validateRefreshToken(String token) {
        return jwtTokenUtil.getTokenStatus(token, refreshSecretKey) == JwtTokenStatus.AUTHENTICATED;
    }

    /**
     * 쿠키에서 토큰을 추출합니다.
     * 
     * @param request HTTP 요청 객체
     * @param tokenPrefix 토큰 접두사
     * @return 추출된 토큰
     * @throws SecurityAuthException 토큰이 없을 경우 발생
     */
    public String resolveTokenFromCookie(HttpServletRequest request, JwtTokenRule tokenPrefix) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            throw new SecurityAuthException(SecurityExceptionRule.JWT_TOKEN_NOT_FOUND);
        }
        return jwtTokenUtil.resolveTokenFromCookie(cookies, tokenPrefix);
    }

    /**
     * JWT 토큰으로부터 인증 정보를 생성합니다.
     * 
     * @param token JWT 토큰
     * @return 생성된 Authentication 객체
     */
    public Authentication getAccessAuthentication(String token) {
        Claims claims = getUserClaims(token, accessSecretKey);
        String userId = claims.get(JwtTokenRule.USER_ID.getValue(), String.class);
        String userName = claims.get(JwtTokenRule.USER_NAME.getValue(), String.class);
        List<SimpleGrantedAuthority> auths = claims.keySet().stream()
            .filter(key -> key.equals(JwtTokenRule.AUTHORITIES_KEY.getValue()))
            .flatMap(key -> ((List<?>) claims.get(key)).stream())
            .map(String::valueOf)
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());
        return new UsernamePasswordAuthenticationToken(UserPrincipal.of(userId, userName), "", auths);
    }

    /**
     * JWT 토큰으로부터 인증 정보를 생성합니다.
     * 
     * @param token JWT 토큰
     * @return 생성된 Authentication 객체
     */
    public Authentication getAuthentication(String token) {
        UserDetails user = userAuthenticationService.getUserDetails(getUserPk(token));
        return new UsernamePasswordAuthenticationToken(user, "", null);
    }

    public void saveRefreshToken(String userId, String token) {
        refreshTokenService.saveRefreshToken(userId, token);
    }

    public String getRefreshToken(String token) {
        return refreshTokenService.getRefreshToken(getUserPk(token));
    }

    public void deleteRefreshToken(String key) {
        refreshTokenService.deleteRefreshToken(key);
    }

    /**
     * 토큰에서 사용자 식별자를 추출합니다.
     * 
     * @param token JWT 토큰
     * @return 추출된 사용자 식별자
     */
    private Claims getUserClaims(String token, Key key) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 토큰에서 사용자 식별자를 추출합니다.
     * 
     * @param token JWT 토큰
     * @return 추출된 사용자 식별자
     */
    public String getUserPk(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(refreshSecretKey)
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
    
    /**
     * 리프레시 토큰 쿠키를 삭제합니다.
     * 
     * @param response HTTP 응답 객체
     */
    public void deleteCookie(HttpServletResponse response) {
        Cookie accessCookie = jwtTokenUtil.resetToken(JwtTokenRule.ACCESS_PREFIX);
        Cookie refreshCookie = jwtTokenUtil.resetToken(JwtTokenRule.REFRESH_PREFIX);
        response.addCookie(accessCookie);
        response.addCookie(refreshCookie);
    }
    
}
