package egovframework.interceptor;

import com.fasterxml.jackson.databind.ObjectMapper;
import egovframework.configProperties.InterceptorProperties;
import egovframework.configProperties.LoginProperties;
import egovframework.constant.SessionConstants;
import egovframework.exception.AccessDeniedException;
import egovframework.filter.XssUtil;
import egovframework.util.ApiResponseEntity;
import egovframework.util.HttpServletUtil;
import go.kr.project.login.mapper.LoginMapper;
import go.kr.project.login.model.SessionVO;
import go.kr.project.login.model.UserSessionVO;
import go.kr.project.login.service.LoginService;
import go.kr.project.system.menu.model.MenuVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * packageName    : egovframework.interceptor
 * fileName       : AuthInterceptor
 * author         : 시스템 관리자
 * date           : 2025-05-13
 * description    : 접근 제어를 위한 인터셉터
 * ===========================================================
 * DATE              AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2025-05-13        시스템 관리자       최초 생성
 */
@Slf4j
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {

    private final LoginService loginService;

    // URL 패턴 매칭을 위한 AntPathMatcher
    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    // XSS 방지 유틸리티
    private final XssUtil xssUtil = new XssUtil();

    // JSON 변환을 위한 ObjectMapper
    private final ObjectMapper objectMapper = new ObjectMapper();

    // Referer 검사에서 제외할 URL 패턴 목록 설정
    @Autowired
    private InterceptorProperties interceptorProperties;

    // 로그인 관련 설정
    @Autowired
    private LoginProperties loginProperties;

    // 로그인 매퍼
    @Autowired
    private LoginMapper loginMapper;

    /**
     * 컨트롤러 실행 전 접근 제어 처리
     * @param request 요청 객체
     * @param response 응답 객체
     * @param handler 핸들러 객체
     * @return 접근 허용 여부
     * @throws Exception 예외 발생 시
     */
    @Override
    public boolean preHandle(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Object handler) throws Exception {
        String requestURI = request.getRequestURI();

        // 접근 제어 예외 URL 패턴은 WebMvcConfigurer에서 처리됨
        // 이 메서드가 호출된다는 것은 이미 예외 패턴이 아니라는 의미

        // Referer 헤더 검사 - URL을 브라우저 주소창에 직접 입력해서 접근하는 것을 방지
        // 첫 페이지 접근이나 로그인 페이지 등 일부 URL은 Referer 검사에서 제외
        if (!isRefererCheckExcluded(requestURI)) {
            String referer = request.getHeader("Referer");
            if (referer == null || referer.isEmpty()) {
                // Referer 헤더가 없는 경우 (직접 URL 입력 또는 북마크 등)
                log.warn("Referer 헤더 없음: {}", requestURI);

                // AJAX 요청인 경우 JSON 응답 반환
                if (isAjaxRequest(request)) {
                    handleRefererMissing(response);
                    return false;
                }

                // 일반 요청인 경우 로그인 페이지로 리다이렉트
                redirectToLoginPageWithPopupCheck(request, response, "잘못된 접근입니다. 로그인 페이지로 이동합니다.");
                return false;
            }
        }

        try {
            // 세션 정보 조회
            SessionVO sessionVO = loginService.getSessionInfo(request);

            // 세션이 있고 로그인 상태인 경우, 동시 접속 허용 불가일 때만 세션 정보를 DB에서 확인
            if (sessionVO != null && sessionVO.isLogin() && !loginProperties.isAllowMultipleLogin()) {
                HttpSession session = request.getSession(false);
                if (session != null) {
                    String sessionId = session.getId();
                    String userId = sessionVO.getUser().getUserId();

                    // DB에서 세션 정보 조회
                    UserSessionVO userSessionVO = loginMapper.selectUserSessionBySessionId(sessionId);

                    if (userSessionVO == null) {
                        // DB에 세션 정보가 없으면 현재 세션 무효화
                        log.warn("DB에 세션 정보가 없음. 세션 무효화 (동시 접속 제한): {}", sessionId);
                        session.invalidate();

                        // AJAX 요청인 경우 JSON 응답 반환
                        if (isAjaxRequest(request)) {
                            handleAjaxSessionExpired(response);
                            return false;
                        }

                        // 일반 요청인 경우 로그인 페이지로 리다이렉트
                        redirectToLoginPageWithPopupCheck(request, response, "세션이 종료되었습니다. 로그인 페이지로 이동합니다.");

                        return false;
                    } else {
                        // DB에 세션 정보가 있으면 마지막 접속 시간 업데이트
                        userSessionVO.setLastAccessDttm(LocalDateTime.now());
                        loginMapper.updateUserSession(userSessionVO);
                        log.debug("세션 정보 업데이트 (동시 접속 제한): {}", sessionId);
                    }
                }
            }

            // 세션이 없거나 로그인 상태가 아닌 경우
            if (sessionVO == null || !sessionVO.isLogin()) {
                // 방문자 권한 확인
                if (sessionVO != null && sessionVO.isVisitor()) {
                    // 방문자 권한으로 접근 가능한지 확인
                    if (hasAccess(sessionVO, requestURI)) {
                        return true;
                    }
                }

                // AJAX 요청인 경우 JSON 응답 반환
                if (isAjaxRequest(request)) {
                    handleAjaxSessionExpired(response);
                    return false;
                }

                // 일반 요청인 경우 로그인 페이지로 리다이렉트
                redirectToLoginPageWithPopupCheck(request, response, "세션이 종료되었습니다. 로그인 페이지로 이동합니다.");

                return false;
            }

            // 비밀번호 초기화 여부 확인
            if ("Y".equals(sessionVO.getUser().getPasswdInitYn())) {
                // 비밀번호 변경 페이지 또는 비밀번호 변경 처리 URL인 경우에만 접근 허용
                if (requestURI.equals("/mypage/password.do") || 
                    requestURI.equals("/mypage/password.ajax") || 
                    requestURI.equals("/mypage/check-password.ajax") ||
                    requestURI.equals("/mypage/password-reset.do")) {
                    return true;
                } else {
                    // 비밀번호 변경 페이지로 리다이렉트
                    if (isAjaxRequest(request)) {
                        // AJAX 요청인 경우 JSON 응답 반환
                        sendJsonResponse(response, HttpStatus.UNAUTHORIZED, false, "비밀번호를 변경해야 합니다.", "PASSWORD_RESET_REQUIRED");
                        return false;
                    } else {
                        // 일반 요청인 경우 비밀번호 변경 페이지로 리다이렉트
                        redirectToPage(request, response, "비밀번호를 변경해야 합니다.", "/mypage/password-reset.do");
                        return false;
                    }
                }
            }

            // 로그인 상태인 경우 접근 권한 확인
            if (hasAccess(sessionVO, requestURI)) {
                return true;
            }

            // 접근 권한이 없는 경우 예외 발생
            log.warn("접근 권한 없음: {} - {}", sessionVO.getUser().getUserAcnt(), requestURI);
            throw new AccessDeniedException(sessionVO.getUser().getUserAcnt(), requestURI);
            //response.sendError(HttpServletResponse.SC_FORBIDDEN, "접근 권한이 없습니다.");
            //return false;
        } catch (AccessDeniedException e) {
            throw e;
        } catch (Exception e) {
            log.error("접근 제어 처리 중 오류 발생", e);

            // AJAX 요청인 경우 JSON 응답 반환
            if (isAjaxRequest(request)) {
                try {
                    handleAjaxSessionExpired(response);
                } catch (IOException ex) {
                    log.error("AJAX 응답 처리 중 오류 발생", ex);
                }
                return false;
            }

            // 일반 요청인 경우 로그인 페이지로 리다이렉트
            try {
                redirectToLoginPageWithPopupCheck(request, response, "세션이 종료되었습니다. 로그인 페이지로 이동합니다.");
            } catch (IOException ex) {
                log.error("리다이렉트 처리 중 오류 발생", ex);
            }

            return false;
        }
    }

    /**
     * 접근 권한이 있는지 확인
     * @param sessionVO 세션 정보
     * @param requestURI 요청 URI
     * @return 접근 권한 여부
     */
    private boolean hasAccess(SessionVO sessionVO, String requestURI) {
        // 메뉴 트리를 1차원 리스트로 변환
        List<MenuVO> menus = flattenMenuTree(sessionVO.getMenus());
        if (menus.isEmpty()) {
            return false;
        }

        // 메뉴의 URL 패턴과 일치하는지 확인
        for (MenuVO menu : menus) {
            if (menu.getUrlPattern() != null && !menu.getUrlPattern().isEmpty()) {
                // XSS 방지를 위한 이스케이프 처리된 URL 패턴을 원래대로 복원하고 쉼표로 구분된 패턴을 분리
                String[] patterns = xssUtil.unescape(menu.getUrlPattern()).split(",");
                for (String pattern : patterns) {
                    // 각 패턴과 요청 URI를 비교하여 일치하면 접근 허용
                    if (pathMatcher.match(pattern.trim(), requestURI)) {
                        return true;
                    }
                }
            }
        }

        // 일치하는 패턴이 없으면 접근 불가
        return false;
    }

    /**
     * 메뉴 트리 구조를 1차원 리스트로 변환
     * 재귀적으로 모든 하위 메뉴를 포함하는 평면화된 리스트를 생성
     *
     * @param menus 메뉴 트리 구조
     * @return 1차원으로 변환된 메뉴 리스트
     */
    private List<MenuVO> flattenMenuTree(List<MenuVO> menus) {
        List<MenuVO> flatList = new ArrayList<>();
        if (menus != null) {
            for (MenuVO menu : menus) {
                // 현재 메뉴를 리스트에 추가
                flatList.add(menu);

                // 하위 메뉴가 있으면 재귀적으로 처리하여 리스트에 추가
                if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
                    flatList.addAll(flattenMenuTree(menu.getChildren()));
                }
            }
        }
        return flatList;
    }

    /**
     * AJAX 요청에 대한 세션 만료 응답 처리
     * 클라이언트에게 세션 만료 정보를 JSON 형식으로 반환
     * 
     * @param response HTTP 응답 객체
     * @throws IOException 입출력 예외 발생 시
     */
    private void handleAjaxSessionExpired(HttpServletResponse response) throws IOException {
        sendJsonResponse(response, HttpStatus.UNAUTHORIZED, false, 
                "세션이 만료되었습니다. 로그인 페이지로 이동합니다.", 
                SessionConstants.SESSION_EXPIRED);
    }

    /**
     * Referer 헤더가 없는 AJAX 요청에 대한 응답 처리
     * 직접 URL 입력 등 비정상적인 접근 시도에 대한 응답 처리
     * 
     * @param response HTTP 응답 객체
     * @throws IOException 입출력 예외 발생 시
     */
    private void handleRefererMissing(HttpServletResponse response) throws IOException {
        sendJsonResponse(response, HttpStatus.FORBIDDEN, false, 
                "잘못된 접근입니다. 로그인 페이지로 이동합니다.", 
                SessionConstants.INVALID_ACCESS);
    }

    /**
     * JSON 응답을 생성하여 클라이언트에게 반환
     * 
     * @param response HTTP 응답 객체
     * @param status HTTP 상태 코드
     * @param result 결과 성공 여부
     * @param message 메시지
     * @param errorCode 에러 코드
     * @throws IOException 입출력 예외 발생 시
     */
    private void sendJsonResponse(HttpServletResponse response, HttpStatus status, boolean result, 
                                 String message, String errorCode) throws IOException {
        // 응답 상태 코드 및 컨텐츠 타입 설정
        response.setStatus(status.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");

        // 응답 객체 생성
        ApiResponseEntity<Void> responseBody = ApiResponseEntity.<Void>builder()
                .result(result)
                .message(message)
                .errorCode(errorCode)
                .build();

        // JSON 변환 및 응답 출력
        response.getWriter().write(objectMapper.writeValueAsString(responseBody));
    }

    /**
     * Referer 검사에서 제외할 URL 패턴인지 확인
     * 로그인 페이지, 메인 페이지 등 직접 접근이 허용되는 URL인지 확인
     * 
     * @param requestURI 요청 URI
     * @return Referer 검사 제외 여부 (true: 제외, false: 검사 필요)
     */
    private boolean isRefererCheckExcluded(String requestURI) {
        // 설정된 제외 패턴과 요청 URI를 비교
        for (String pattern : interceptorProperties.getRefererExclude()) {
            if (pathMatcher.match(pattern, requestURI)) {
                return true; // 제외 패턴에 해당하면 Referer 검사 제외
            }
        }

        // 제외 패턴에 해당하지 않으면 Referer 검사 필요
        return false;
    }

    /**
     * AJAX 요청인지 확인
     * 
     * @param request HTTP 요청 객체
     * @return AJAX 요청 여부
     */
    private boolean isAjaxRequest(HttpServletRequest request) {
        return HttpServletUtil.isAjaxRequest(request) || HttpServletUtil.isRealAjaxRequest(request);
    }

    /**
     * 일반 페이지로 리다이렉트
     *
     * @param request HTTP 요청 객체
     * @param response HTTP 응답 객체
     * @param message 알림 메시지
     * @param targetUrl 리다이렉트 대상 URL
     * @throws IOException 입출력 예외 발생 시
     */
    private void redirectToPage(HttpServletRequest request, HttpServletResponse response,
                               String message, String targetUrl) throws IOException {
        response.setContentType("text/html; charset=UTF-8");
        response.getWriter().write("<script type='text/javascript'>");
        response.getWriter().write("alert('" + message + "');");
        response.getWriter().write("location.href = '" + request.getContextPath() + targetUrl + "';");
        response.getWriter().write("</script>");
    }

    /**
     * 팝업 창 확인 후 로그인 페이지로 리다이렉트
     *
     * @param request HTTP 요청 객체
     * @param response HTTP 응답 객체
     * @param message 알림 메시지
     * @throws IOException 입출력 예외 발생 시
     */
    private void redirectToLoginPageWithPopupCheck(HttpServletRequest request, HttpServletResponse response,
                                                 String message) throws IOException {
        try {
            // 메시지를 request attribute로 설정
            request.setAttribute("errorMessage", message);
            // 401.jsp로 forward하여 handleSessionExpired() 함수 호출
            request.getRequestDispatcher("/WEB-INF/views/error/401.jsp").forward(request, response);
        } catch (Exception e) {
            log.error("401 페이지 forward 실패, 기존 방식으로 처리", e);
            // forward 실패 시 기존 방식으로 fallback
            response.setContentType("text/html; charset=UTF-8");
            response.getWriter().write("<script type='text/javascript'>");
            response.getWriter().write("alert('" + message + "');");
            response.getWriter().write("if(window.opener) {");
            response.getWriter().write("  var topWindow = window;");
            response.getWriter().write("  while(topWindow.opener && !topWindow.opener.closed) {");
            response.getWriter().write("    topWindow = topWindow.opener;");
            response.getWriter().write("  }");
            response.getWriter().write("  topWindow.location.href = '" + request.getContextPath() + loginProperties.getUrl() + "';");
            response.getWriter().write("  window.close();");
            response.getWriter().write("} else {");
            response.getWriter().write("  location.href = '" + request.getContextPath() + loginProperties.getUrl() + "';");
            response.getWriter().write("}");
            response.getWriter().write("</script>");
        }
    }
}
