package com.takensoft.common.util;

import com.takensoft.cms.mber.vo.MberAuthorVO;
import com.takensoft.cms.mber.vo.MberVO;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author  : takensoft
 * @since   : 2025.01.22
 * @modification
 *     since    |    author    | description
 *  2025.01.22  |  takensoft   | 최초 등록
 *  2025.06.18  |  takensoft   | 토큰 추출 로직 통합 및 중복 제거
 *
 * JWT 토큰 생성 및 검증, 쿠키 생성 등의 유틸리티 - 토큰 추출 로직 통합
 */
@Component
public class JWTUtil {

    private static SecretKey JWT_SECRET_KEY;

    // 토큰 추출 우선순위 정의
    private static final String[] TOKEN_COOKIE_NAMES = {"Authorization", "refresh"};
    private static final String BEARER_PREFIX = "Bearer ";
    private static final String AUTHORIZATION_HEADER = "Authorization";

    /**
     * @param secret - JWT 서명을 위한 키 (application.yml에서 값을 읽어 옴)
     *
     * 기본 생성자
     */
    public JWTUtil(@Value("${jwt.secret}")String secret) {
        this.JWT_SECRET_KEY = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    /**
     * @param request - HTTP 요청 객체
     * @return 추출된 JWT 토큰 (Bearer 접두사 제거된 순수 토큰) 또는 null
     *
     * HTTP 요청에서 JWT 토큰을 추출하는 통합 메서드
     * 우선순위: Authorization 헤더 → Authorization 쿠키 → refresh 쿠키
     */
    public String extractTokenFromRequest(HttpServletRequest request) {
        if (request == null) {
            return null;
        }

        // 1. Authorization 헤더에서 토큰 추출 시도
        String authHeader = request.getHeader(AUTHORIZATION_HEADER);
        if (isValidTokenString(authHeader)) {
            return removeBearerPrefix(authHeader);
        }

        // 2. 쿠키에서 토큰 추출 시도
        String tokenFromCookie = extractTokenFromCookie(request);
        if (isValidTokenString(tokenFromCookie)) {
            return removeBearerPrefix(tokenFromCookie);
        }

        return null;
    }

    /**
     * @param authHeader - Authorization 헤더 문자열 또는 토큰 문자열
     * @return Bearer 접두사가 제거된 순수 토큰 문자열
     *
     * Bearer 접두사를 제거하는 기존 메서드 (하위 호환성 유지)
     */
    public String extractToken(String authHeader) {
        return removeBearerPrefix(authHeader);
    }

    /**
     * @param request - HTTP 요청 객체
     * @return 쿠키에서 추출된 토큰 문자열 또는 null
     *
     * 쿠키에서 토큰을 추출하는 전용 메서드
     * 우선순위: Authorization 쿠키 → refresh 쿠키
     */
    private String extractTokenFromCookie(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return null;
        }

        // 쿠키 우선순위에 따라 토큰 추출
        for (String cookieName : TOKEN_COOKIE_NAMES) {
            for (Cookie cookie : cookies) {
                if (cookieName.equals(cookie.getName())) {
                    String cookieValue = cookie.getValue();
                    if (isValidTokenString(cookieValue)) {
                        return cookieValue;
                    }
                }
            }
        }

        return null;
    }

    /**
     * @param tokenString - 검증할 토큰 문자열
     * @return 유효한 토큰 문자열인지 여부
     *
     * 토큰 문자열 유효성 검증
     */
    private boolean isValidTokenString(String tokenString) {
        return tokenString != null &&
                !tokenString.trim().isEmpty() &&
                !tokenString.equalsIgnoreCase("null") &&
                !tokenString.equalsIgnoreCase("undefined");
    }

    /**
     * @param authHeader - Bearer 접두사가 포함될 수 있는 토큰 문자열
     * @return Bearer 접두사가 제거된 순수 토큰 문자열
     *
     * Bearer 접두사 제거 로직
     */
    private String removeBearerPrefix(String authHeader) {
        if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
            return authHeader.substring(BEARER_PREFIX.length());
        }
        return authHeader;
    }

    /**
     * @param category  토큰의 카테고리 정보 (Authorization, refresh)
     * @param mbrId 사용자 ID
     * @param lgnId 로그인 ID
     * @param mbrNm 사용자 이름
     * @param roles  사용자 권한 목록
     * @param expiredMs 토큰 만료 시간 (밀리초)
     * @return 생성된 JWT 토큰 (String)
     *
     * JWT 토큰 생성
     */
    public String createJwt(String category, String mbrId, String lgnId, String mbrNm, List<String> roles, long expiredMs) {
        return Jwts.builder()
                .claim("category", category)
                .claim("mbrId", mbrId)
                .claim("lgnId", lgnId)
                .claim("mbrNm", mbrNm)
                .claim("roles", roles)
                .issuedAt(new Date(System.currentTimeMillis())) // 토큰 발행 시간
                .expiration(new Date(System.currentTimeMillis() + expiredMs))   // 토큰 소멸 시간
                .signWith(JWT_SECRET_KEY)
                .compact();
    }

    /**
     * @param key 쿠키 키 값
     * @param value 쿠키 값
     * @param time 쿠키의 생명주기 (초 단위)
     * @return 생성된 Cookie 객체
     *
     * 쿠키 생성
     */
    public Cookie createCookie(String key, String value, int time) {
        // 쿠키 생성
        Cookie cookie = new Cookie(key, value);
        cookie.setMaxAge(time); // 생명주기
        //cookie.setSecure(true); // https 통신을 할 경우 true로 사용
        cookie.setPath("/"); // 쿠키 적용 범위
        cookie.setHttpOnly(true); // front에서 script로 접근 방지
        return cookie;
    }

    /**
     * @param tkn JWT 토큰 문자열
     * @param knd 조회할 데이터의 종류 (예: ctgry, userId, lgnId 등)
     * @return 조회된 클레임 데이터 (종류에 따라 String, Date, List 등으로 반환)
     * @throws IllegalArgumentException 유효하지 않은 knd 값일 경우 예외 발생
     *
     * 클레임 조회
     */
    public Object getClaim(String tkn, String knd) {
        Claims claims;
        try {
            // 토큰 값 검증
            if (tkn == null || tkn.trim().isEmpty()) {
                throw new IllegalArgumentException("Token is null or empty");
            } else {
                tkn = extractToken(tkn);
            }

            claims = Jwts.parser()
                    .verifyWith(JWT_SECRET_KEY)
                    .build()
                    .parseSignedClaims(tkn)
                    .getPayload();
        } catch (ExpiredJwtException e) {
            // 만료된 토큰이라도 claims 꺼내기 가능
            claims = e.getClaims();
        } catch (JwtException | IllegalArgumentException e) {
            // 토큰 자체가 잘못된 경우
            throw new IllegalArgumentException("Invalid token");
        }

        switch (knd) {
            case "category":
                return claims.get("category", String.class);
            case "mbrId":
                return claims.get("mbrId", String.class);
            case "lgnId":
                return claims.get("lgnId", String.class);
            case "mbrNm":
                return claims.get("mbrNm", String.class);
            case "roles":
                List<Map<String, Object>> roles = claims.get("roles", List.class);
                List<MberAuthorVO> authorList = new ArrayList<>();
                if (roles != null && !roles.isEmpty()) {
                    for (Map<String, Object> role : roles) {
                        MberAuthorVO userAuthor = new MberAuthorVO(role.get("authority").toString());
                        authorList.add(userAuthor);
                    }
                }
                return authorList;
            case "isExpired":
                return claims.getExpiration().before(new Date());
            case "expired":
                return claims.getExpiration();
            default:
                throw new IllegalArgumentException("Invalid knd : " + knd);
        }
    }
}