package com.takensoft.common.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.takensoft.cms.loginPolicy.service.LoginModeService;
import com.takensoft.cms.loginPolicy.service.LoginPolicyService;
import com.takensoft.cms.mber.service.LgnHstryService;
import com.takensoft.cms.mber.vo.MberVO;
import com.takensoft.cms.token.service.RefreshTokenService;
import com.takensoft.cms.token.vo.RefreshTknVO;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author takensoft
 * @since 2025.03.21
 * @modification
 *     since    |    author    | description
 *  2025.03.21  |  takensoft   | 최초 등록
 *  2025.05.28  |  takensoft   | 통합 로그인 적용, 문제 해결
 *  2025.05.29  |  takensoft   | Redis 통합 중복로그인 관리
 *  2025.06.04  |  takensoft   | Redis 트랜잭션 및 타이밍 이슈 해결
 *  2025.06.09  |  takensoft   | 중복로그인 처리 로직 개선
 *  2025.06.18  |  takensoft   | 로그인 이력 저장 로직 통합 및 중복 제거
 *
 * 통합 로그인 유틸리티 - 로그인 이력 저장 로직 중복 제거
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class LoginUtil {
    private final LgnHstryService lgnHstryService;
    private final HttpRequestUtil httpRequestUtil;
    private final LoginModeService loginModeService;
    private final RefreshTokenService refreshTokenService;
    private final LoginPolicyService loginPolicyService;
    private final JWTUtil jwtUtil;
    private final SessionUtil sessionUtil;
    private final RedisTemplate<String, String> redisTemplate;

    @Value("${jwt.accessTime}")
    private long JWT_ACCESSTIME;
    @Value("${jwt.refreshTime}")
    private long JWT_REFRESHTIME;
    @Value("${cookie.time}")
    private int COOKIE_TIME;

    /**
     * 통합 로그인 성공 처리
     */
    public void successLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException {
        String loginMode = loginModeService.getLoginMode();
        boolean allowMultipleLogin = loginPolicyService.getPolicy();

        res.setHeader("loginMode", loginMode); // J, S
        res.setHeader("policyMode", allowMultipleLogin ? "Y" : "N"); // Y, N

        // 통합된 로그인 이력 저장 로직 사용
        String loginType = (String) req.getAttribute("loginType");
        if (!"OAUTH2".equals(loginType)) {
            // 시스템 로그인인 경우에만 이력 저장 (OAuth2는 핸들러에서 처리)
            lgnHstryService.saveSystemLoginHistory(mber, req);
        }

        if ("S".equals(loginMode)) {
            handleSessionLogin(mber, req, res);
        } else {
            handleJwtLogin(mber, req, res);
        }
    }

    /**
     * 세션 모드 로그인 처리 - 중복로그인 개선
     */
    private void handleSessionLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException {
        String mbrId = mber.getMbrId();

        // 중복로그인 비허용 시 기존 세션 정리
        if (!loginPolicyService.getPolicy()) {
            forceLogoutExistingSession(mbrId);
        }

        // JWT 토큰은 생성하되 세션에만 저장
        String accessToken = jwtUtil.createJwt("Authorization", mbrId, mber.getLgnId(),
                mber.getMbrNm(), (List) mber.getAuthorities(), JWT_ACCESSTIME);

        // 세션 생성 및 정보 저장
        HttpSession session = req.getSession(true);
        session.setAttribute("JWT_TOKEN", accessToken);
        session.setAttribute("mbrId", mbrId);
        session.setAttribute("mbrNm", mber.getMbrNm());
        session.setAttribute("lgnId", mber.getLgnId());
        session.setAttribute("roles", mber.getAuthorList());
        session.setAttribute("loginType", req.getAttribute("loginType") != null ? req.getAttribute("loginType") : "S");

        // Redis에 세션 토큰 저장 (중복로그인 관리용)
        if (!loginPolicyService.getPolicy()) {
            saveSessionTokenToRedis(mbrId, accessToken, session.getMaxInactiveInterval());
        }

        // SessionUtil에 등록
        sessionUtil.registerSession(mbrId, session);

        // OAuth2가 아닌 경우 JSON 응답 전송
        String loginType = (String) req.getAttribute("loginType");
        if (!"OAUTH2".equals(loginType)) {
            sendSessionLoginResponse(res, mber);
        }
    }

    /**
     * JWT 모드 로그인 처리 - 중복로그인 개선
     */
    private void handleJwtLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException {
        String mbrId = mber.getMbrId();

        // 중복로그인 비허용 시 기존 토큰 정리
        if (!loginPolicyService.getPolicy()) {
            forceLogoutExistingJWT(mbrId);
        }

        // JWT 토큰 생성
        String accessToken = jwtUtil.createJwt("Authorization", mbrId, mber.getLgnId(),
                mber.getMbrNm(), (List) mber.getAuthorities(), JWT_ACCESSTIME);
        String refreshToken = jwtUtil.createJwt("refresh", mbrId, mber.getLgnId(),
                mber.getMbrNm(), (List) mber.getAuthorities(), JWT_REFRESHTIME);

        // Refresh 토큰 처리
        RefreshTknVO refresh = new RefreshTknVO();
        refresh.setMbrId(mbrId);

        // 기존 refresh 토큰 삭제
        if (refreshTokenService.findByCheckRefresh(req, refresh)) {
            refreshTokenService.delete(req, refresh);
        }
        refresh.setToken(refreshToken);

        // 응답 헤더 및 쿠키 설정
        res.setHeader("Authorization", accessToken);
        res.addCookie(jwtUtil.createCookie("refresh", refreshToken, COOKIE_TIME));

        // Redis에 JWT 저장 (중복로그인 관리용)
        if (!loginPolicyService.getPolicy()) {
            saveJWTTokenToRedis(mbrId, accessToken);
        }

        // Refresh 토큰 저장
        refreshTokenService.saveRefreshToken(req, res, refresh, JWT_REFRESHTIME);

        // OAuth2가 아닌 경우만 상태 코드 설정
        String loginType = (String) req.getAttribute("loginType");
        if (!"OAUTH2".equals(loginType)) {
            res.setStatus(HttpStatus.OK.value());
        }
    }

    /**
     * 기존 세션 강제 로그아웃
     */
    private void forceLogoutExistingSession(String mbrId) {
        // 1. SessionUtil에서 기존 세션 제거
        sessionUtil.removeSession(mbrId);

        // 2. Redis에서 기존 세션 토큰 삭제
        String sessionTokenKey = "session_token:" + mbrId;
        String existingToken = redisTemplate.opsForValue().get(sessionTokenKey);

        if (existingToken != null) {
            redisTemplate.delete(sessionTokenKey);
        }
        // 3. 기타 세션 관련 키 정리
        cleanupSessionRedisKeys(mbrId);
    }

    /**
     * 기존 JWT 강제 로그아웃
     */
    private void forceLogoutExistingJWT(String mbrId) {
        String jwtKey = "jwt:" + mbrId;
        String existingToken = redisTemplate.opsForValue().get(jwtKey);

        if (existingToken != null) {
            redisTemplate.delete(jwtKey);
        }
    }

    /**
     * Redis에 세션 토큰 저장
     */
    private void saveSessionTokenToRedis(String mbrId, String accessToken, int sessionTimeout) {
        String tokenKey = "session_token:" + mbrId;
        redisTemplate.opsForValue().set(tokenKey, accessToken, Duration.ofSeconds(sessionTimeout));
    }

    /**
     * Redis에 JWT 토큰 저장
     */
    private void saveJWTTokenToRedis(String mbrId, String accessToken) {
        String jwtKey = "jwt:" + mbrId;
        redisTemplate.opsForValue().set(jwtKey, accessToken, JWT_ACCESSTIME, TimeUnit.MILLISECONDS);
    }

    /**
     * 세션 관련 Redis 키 정리
     */
    private void cleanupSessionRedisKeys(String mbrId) {
        Set<String> sessionKeys = redisTemplate.keys("session*:" + mbrId);
        if (sessionKeys != null && !sessionKeys.isEmpty()) {
            redisTemplate.delete(sessionKeys);
        }
    }

    /**
     * 세션 로그인 응답 전송
     */
    private void sendSessionLoginResponse(HttpServletResponse res, MberVO mber) throws IOException {
        Map<String, Object> result = new HashMap<>();
        result.put("mbrId", mber.getMbrId());
        result.put("mbrNm", mber.getMbrNm());
        result.put("roles", mber.getAuthorList());

        res.setContentType("application/json;charset=UTF-8");
        res.setCharacterEncoding("UTF-8");
        res.setStatus(HttpStatus.OK.value());

        String jsonResponse = new ObjectMapper().writeValueAsString(result);
        res.getWriter().write(jsonResponse);
        res.getWriter().flush();
    }
}