package go.kr.project.login.service.impl;

import egovframework.configProperties.LoginProperties;
import egovframework.constant.MessageConstants;
import egovframework.constant.SessionConstants;
import egovframework.exception.MessageException;
import egovframework.util.EgovFileScrty;
import go.kr.project.login.mapper.LoginMapper;
import go.kr.project.login.model.LoginUserVO;
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.group.model.GroupVO;
import go.kr.project.system.loginLog.service.LoginLogService;
import go.kr.project.system.menu.model.MenuVO;
import go.kr.project.system.role.model.RoleVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * packageName    : go.kr.project.login.service.impl
 * fileName       : LoginServiceImpl
 * author         : 시스템 관리자
 * date           : 2025-05-13
 * description    : 로그인 관련 서비스 구현 클래스
 * ===========================================================
 * DATE              AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2025-05-13        시스템 관리자       최초 생성
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class LoginServiceImpl extends EgovAbstractServiceImpl implements LoginService {

    // 세션 키와 방문자 역할 ID는 SessionConstants 클래스에서 관리

    // 로그인 실패 시 계정 잠금 횟수 (기본값: 5)
    @Value("${login.lock.count:5}")
    private int lockCount;

    // 로그인 관련 데이터 접근 매퍼
    private final LoginMapper loginMapper;

    // 로그인 로그 기록 서비스
    private final LoginLogService loginLogService;

    // 로그인 관련 설정 정보
    private final LoginProperties loginProperties;

    /**
     * 로그인 처리
     * 사용자 인증 및 세션 생성을 담당하는 메소드
     * 
     * @param userAcnt 사용자 계정
     * @param passwd 비밀번호
     * @param request HTTP 요청 객체
     * @param response HTTP 응답 객체
     * @return 로그인 성공 시 SessionVO 객체, 실패 시 null
     * @throws Exception 예외 발생 시
     */
    @Override
    @Transactional
    public SessionVO login(String userAcnt, String passwd, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 비정상적인 로그인 패턴 감지 (과도한 로그인 시도, 다양한 IP에서의 접근 등)
        Map<String, Object> abnormalCheck = loginLogService.checkAbnormalLoginPattern(userAcnt, request);
        boolean isAbnormal = (boolean) abnormalCheck.get("isAbnormal");

        if (isAbnormal) {
            String reason = (String) abnormalCheck.get("reason");
            log.warn("비정상적인 로그인 패턴 감지: {} - {}", userAcnt, reason);

            // 로그인 실패 로그 기록
            loginLogService.recordLoginFailure(userAcnt, MessageConstants.LoginFailure.ABNORMAL_PATTERN_PREFIX + reason, request);

            // 비정상적인 패턴이 감지되면 추가적인 보안 조치를 취할 수 있음
            // 예: 계정 일시 잠금, 관리자에게 알림 등

            // 여기서는 로그인을 차단하지 않고 경고만 기록
        }

        // 사용자 계정으로 사용자 정보 조회
        LoginUserVO user = loginMapper.selectUserByAcnt(userAcnt);

        // 사용자 정보가 없는 경우
        if (user == null) {
            log.info("로그인 실패: 사용자 계정 없음 - {}", userAcnt);
            // 로그인 실패 로그 기록
            loginLogService.recordLoginFailure(userAcnt, MessageConstants.LoginFailure.USER_NOT_FOUND, request);
            return null;
        }

        // 계정이 잠긴 경우
        if ("Y".equals(user.getLockYn())) {
            log.info("로그인 실패: 계정 잠김 - {}", userAcnt);
            // 로그인 실패 로그 기록
            loginLogService.recordLoginFailure(userAcnt, MessageConstants.LoginFailure.ACCOUNT_LOCKED, request);
            throw new MessageException(MessageConstants.LoginFailure.MESSAGE_ACCOUNT_LOCKED);
        }

        // 비밀번호 암호화 및 검증 (사용자 ID를 솔트로 사용)
        String encryptedPassword = EgovFileScrty.encryptPassword(passwd, user.getUserId());
        log.debug("입력된 비밀번호 암호화 결과: {}", encryptedPassword);

        // 비밀번호가 일치하지 않는 경우
        if (!encryptedPassword.equals(user.getPasswd())) {
            // 비밀번호 실패 처리 (실패 횟수 증가 및 잠금 처리)
            handlePasswordFailure(user);
            log.info("로그인 실패: 비밀번호 불일치 - {}", userAcnt);
            // 로그인 실패 로그 기록
            loginLogService.recordLoginFailure(userAcnt, MessageConstants.LoginFailure.PASSWORD_MISMATCH, request);
            return null;
        }

        // 계정이 활성 상태가 아닌 경우
        if (!"ACTIVE".equals(user.getUserStatusCd())) {
            log.info("로그인 실패: 계정 비활성화 - {}", userAcnt);
            // 로그인 실패 로그 기록
            loginLogService.recordLoginFailure(userAcnt, MessageConstants.LoginFailure.ACCOUNT_INACTIVE, request);
            // 비활성화된 계정 정보를 담은 세션 객체 반환 (UI에서 비활성화 메시지 표시 용도)
            SessionVO sessionVO = new SessionVO();
            sessionVO.setUser(user);
            return sessionVO;
        }

        // 로그인 성공 시 세션 생성
        SessionVO sessionVO = new SessionVO();
        user.setPasswd(null); // 보안을 위해 세션 정보에서 비밀번호 제거
        sessionVO.setUser(user);

        // 사용자의 그룹 정보 조회
        if (user.getUserGroupId() != null && !user.getUserGroupId().isEmpty()) {
            // 그룹 정보 조회 및 세션에 설정
            GroupVO group = loginMapper.selectGroup(user.getUserGroupId());
            sessionVO.setGroup(group);

            // 그룹에 할당된 역할 정보 조회
            List<RoleVO> roles = loginMapper.selectRolesByUserGroupId(user.getUserGroupId());
            
            // ROLE_SYSTEM 포함 여부 확인하여 시스템 관리자 여부 설정
            boolean isSystem = roles.stream()
                    .anyMatch(role -> role.getRoleId().contains("ROLE_SYSTEM"));
            sessionVO.setSystem(isSystem);
            
            sessionVO.setRoles(roles);

            // 역할 ID 목록 추출 (메뉴 조회에 사용)
            List<String> roleIds = roles.stream()
                    .map(RoleVO::getRoleId)
                    .collect(Collectors.toList());

            // 역할에 따른 메뉴 정보 조회 (4단계 깊이까지 지원)
            if (!roleIds.isEmpty()) {
                // 1단계(루트) 메뉴 조회
                List<MenuVO> menus = loginMapper.selectMenusByRoleIds(roleIds, "ROOT");

                // 각 메뉴의 하위 메뉴를 재귀적으로 조회하여 트리 구조 구성
                menus.forEach(menu -> {
                    // 2단계 메뉴 조회
                    List<MenuVO> children = loginMapper.selectMenusByRoleIds(roleIds, menu.getMenuId());
                    if (!children.isEmpty()) {
                        menu.setChildren(children);

                        // 3단계 메뉴 조회
                        children.forEach(child -> {
                            List<MenuVO> grandChildren = loginMapper.selectMenusByRoleIds(roleIds, child.getMenuId());
                            if (!grandChildren.isEmpty()) {
                                child.setChildren(grandChildren);

                                // 4단계 메뉴 조회
                                grandChildren.forEach(grandChild -> {
                                    List<MenuVO> greatGrandChildren = loginMapper.selectMenusByRoleIds(roleIds, grandChild.getMenuId());
                                    if (!greatGrandChildren.isEmpty()) {
                                        grandChild.setChildren(greatGrandChildren);
                                    }
                                });
                            }
                        });
                    }
                });

                // 구성된 메뉴 트리를 세션에 설정
                sessionVO.setMenus(menus);
            }
        }

        // 로그인 성공 시 계정 잠금 해제
        this.unlockPassword(user.getUserId());

        // 동시 접속 제어 처리
        if (!loginProperties.isAllowMultipleLogin()) {
            // 동시 접속이 허용되지 않는 경우, 기존 세션을 무효화
            invalidateExistingSessions(request, user.getUserId());
            log.info("동시 접속 제한으로 기존 세션 무효화: {}", user.getUserAcnt());
        } else {
            log.info("동시 접속 허용: {}", user.getUserAcnt());
        }

        // 세션 상태 정보 설정
        sessionVO.setLogin(true);
        sessionVO.setCreationTime(System.currentTimeMillis());
        sessionVO.setLastAccessedTime(System.currentTimeMillis());

        // 세션에 로그인 정보 저장
        HttpSession session = request.getSession(true);
        session.setAttribute(SessionConstants.SESSION_KEY, sessionVO);

        // 동시 접속 제어를 위한 세션 정보 DB 저장 처리
        if (!loginProperties.isAllowMultipleLogin()) {
            try {
                // 세션 정보 객체 생성
                UserSessionVO userSessionVO = UserSessionVO.builder()
                        .sessionId(session.getId())
                        .userId(user.getUserId())
                        .userAcnt(user.getUserAcnt())
                        .loginDttm(LocalDateTime.now())
                        .lastAccessDttm(LocalDateTime.now())
                        .ipAddr(request.getRemoteAddr())
                        .userAgent(request.getHeader("User-Agent"))
                        .build();

                // 세션 정보를 DB에 저장
                loginMapper.insertUserSession(userSessionVO);
                log.info("세션 정보 DB 저장 성공 (동시 접속 제한): {}, 세션 ID: {}", userAcnt, session.getId());
            } catch (Exception e) {
                log.error("세션 정보 DB 저장 실패: {}", e.getMessage(), e);
                throw new RuntimeException("로그인 처리 중 에러가 발생하였습니다.");
            }
        } else {
            log.info("동시 접속 허용 모드: 세션 정보 DB 저장 생략: {}", userAcnt);
        }

        // 로그인 성공 로그 기록
        log.info("로그인 성공: {}", userAcnt);
        loginLogService.recordLoginSuccess(user.getUserId(), userAcnt, request);

        return sessionVO;
    }

    /**
     * 로그아웃 처리
     * 세션 정보를 무효화하고 필요한 경우 DB에서 세션 정보를 삭제
     * 
     * @param request HTTP 요청 객체
     * @param response HTTP 응답 객체
     * @throws Exception 예외 발생 시
     */
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 현재 세션 가져오기 (없으면 null 반환)
        HttpSession session = request.getSession(false);
        if (session != null) {
            // 세션에서 로그인 정보 가져오기
            SessionVO sessionVO = (SessionVO) session.getAttribute(SessionConstants.SESSION_KEY);
            if (sessionVO != null) {
                // 로그아웃 사용자 정보 로깅
                log.info("로그아웃: {}", sessionVO.isVisitor() ? "방문자" : sessionVO.getUser().getUserAcnt());

                // 방문자가 아니고 동시 접속 허용이 불가한 경우 DB에서 세션 정보 삭제
                if (!sessionVO.isVisitor() && !loginProperties.isAllowMultipleLogin()) {
                    try {
                        String sessionId = session.getId();
                        int result = loginMapper.deleteUserSessionBySessionId(sessionId);
                        log.info("세션 정보 DB 삭제 결과 (동시 접속 제한): {}, 세션 ID: {}", 
                                result > 0 ? "성공" : "실패", sessionId);
                    } catch (Exception e) {
                        log.error("세션 정보 DB 삭제 실패: {}", e.getMessage(), e);
                    }
                } else if (!sessionVO.isVisitor()) {
                    // 방문자가 아니지만 동시 접속이 허용된 경우
                    log.info("동시 접속 허용 모드: 세션 정보 DB 삭제 생략: {}", sessionVO.getUser().getUserAcnt());
                }
            }
            // 세션 무효화
            session.invalidate();
        }
    }

    /**
     * 현재 세션 정보 조회
     * 세션이 없는 경우 방문자 세션을 생성하여 반환
     * 
     * @param request HTTP 요청 객체
     * @return 세션 정보 (로그인된 사용자 또는 방문자)
     * @throws Exception 예외 발생 시
     */
    @Override
    public SessionVO getSessionInfo(HttpServletRequest request) throws Exception {
        // 현재 세션 가져오기 (없으면 null 반환)
        HttpSession session = request.getSession(false);
        if (session != null) {
            // 세션에서 로그인 정보 가져오기
            SessionVO sessionVO = (SessionVO) session.getAttribute(SessionConstants.SESSION_KEY);
            if (sessionVO != null) {
                // 마지막 접근 시간 업데이트
                sessionVO.setLastAccessedTime(System.currentTimeMillis());
                return sessionVO;
            }
        }

        // 세션이 없거나 세션에 로그인 정보가 없는 경우 방문자 세션 생성
        return createVisitorSession(request);
    }

    /**
     * 방문자 세션 생성
     * 로그인하지 않은 사용자를 위한 제한된 권한의 세션 생성
     * 
     * @param request HTTP 요청 객체
     * @return 방문자 세션 정보
     * @throws Exception 예외 발생 시
     */
    @Override
    public SessionVO createVisitorSession(HttpServletRequest request) throws Exception {
        // 방문자 세션 기본 정보 설정
        SessionVO sessionVO = new SessionVO();
        sessionVO.setVisitor(true);
        sessionVO.setLogin(false);
        sessionVO.setCreationTime(System.currentTimeMillis());
        sessionVO.setLastAccessedTime(System.currentTimeMillis());

        // 방문자 역할 설정
        List<RoleVO> roles = new ArrayList<>();
        RoleVO visitorRole = new RoleVO();
        visitorRole.setRoleId(SessionConstants.VISITOR_ROLE_ID);
        visitorRole.setRoleNm("방문자");
        roles.add(visitorRole);
        sessionVO.setRoles(roles);

        // 방문자 권한으로 접근 가능한 메뉴 조회
        List<String> roleIds = new ArrayList<>();
        roleIds.add(SessionConstants.VISITOR_ROLE_ID);

        // 1단계(루트) 메뉴 조회
        List<MenuVO> menus = loginMapper.selectMenusByRoleIds(roleIds, "ROOT");

        // 각 메뉴의 하위 메뉴를 재귀적으로 조회하여 트리 구조 구성
        menus.forEach(menu -> {
            // 2단계 메뉴 조회
            List<MenuVO> children = loginMapper.selectMenusByRoleIds(roleIds, menu.getMenuId());
            if (!children.isEmpty()) {
                menu.setChildren(children);

                // 3단계 메뉴 조회
                children.forEach(child -> {
                    List<MenuVO> grandChildren = loginMapper.selectMenusByRoleIds(roleIds, child.getMenuId());
                    if (!grandChildren.isEmpty()) {
                        child.setChildren(grandChildren);

                        // 4단계 메뉴 조회
                        grandChildren.forEach(grandChild -> {
                            List<MenuVO> greatGrandChildren = loginMapper.selectMenusByRoleIds(roleIds, grandChild.getMenuId());
                            if (!greatGrandChildren.isEmpty()) {
                                grandChild.setChildren(greatGrandChildren);
                            }
                        });
                    }
                });
            }
        });

        // 구성된 메뉴 트리를 세션에 설정
        sessionVO.setMenus(menus);

        // 세션에 방문자 정보 저장
        HttpSession session = request.getSession(true);
        session.setAttribute(SessionConstants.SESSION_KEY, sessionVO);

        return sessionVO;
    }

    /**
     * 비밀번호 실패 처리
     * 비밀번호 입력 실패 시 카운트를 증가시키고 설정된 횟수 이상 실패 시 계정을 잠금 처리
     * 
     * @param user 사용자 정보
     * @return 업데이트된 사용자 정보
     * @throws Exception 예외 발생 시
     */
    @Override
    @Transactional
    public LoginUserVO handlePasswordFailure(LoginUserVO user) throws Exception {
        // 잠금 카운트가 0 이하인 경우 잠금 처리하지 않음 (잠금 기능 비활성화)
        if (lockCount <= 0) {
            return user;
        }

        // 비밀번호 실패 횟수 증가
        int failCount = user.getLockCnt() + 1;
        user.setLockCnt(failCount);

        // 설정된 잠금 횟수 이상 실패 시 계정 잠금 처리
        if (failCount >= lockCount) {
            user.setLockYn("Y");
            user.setLockDttm(LocalDateTime.now());
            log.info("계정 잠김 처리: {} (실패 횟수: {})", user.getUserAcnt(), failCount);
        }

        // 사용자 정보 업데이트 (실패 횟수 및 잠금 상태)
        loginMapper.updatePasswordLock(user);

        return user;
    }

    /**
     * 비밀번호 잠금 해제
     * 계정의 잠금 상태를 해제하고 실패 카운트를 초기화
     * 
     * @param userId 사용자 ID
     * @return 성공 여부 (true: 성공, false: 실패)
     * @throws Exception 예외 발생 시
     */
    @Override
    @Transactional
    public boolean unlockPassword(String userId) throws Exception {
        // 사용자 정보 조회
        LoginUserVO user = loginMapper.selectUser(userId);
        if (user == null) {
            log.warn("계정 잠금 해제 실패: 사용자 정보 없음 - {}", userId);
            return false;
        }

        // 잠금 상태 해제 및 카운트 초기화
        user.setLockYn("N");
        user.setLockCnt(0);
        user.setLockDttm(null);

        // 사용자 정보 업데이트
        int result = loginMapper.updatePasswordUnlock(user);

        // 업데이트 결과 확인
        if (result > 0) {
            log.info("계정 잠금 해제 성공: {}", user.getUserAcnt());
            return true;
        }

        log.warn("계정 잠금 해제 실패: DB 업데이트 오류 - {}", user.getUserAcnt());
        return false;
    }

    /**
     * 동일 계정의 모든 기존 세션을 무효화합니다.
     * 
     * @param request HTTP 요청 객체
     * @param userId 사용자 ID
     */
    private void invalidateExistingSessions(HttpServletRequest request, String userId) {
        // DB에서 사용자의 기존 세션 정보를 조회
        List<UserSessionVO> existingSessions;
        try {
            existingSessions = loginMapper.selectUserSessionByUserId(userId);

            if (existingSessions != null && !existingSessions.isEmpty()) {
                log.info("동일 계정의 기존 세션 무효화: {}, 세션 수: {}", userId, existingSessions.size());

                // 현재 세션 ID
                String currentSessionId = request.getSession().getId();

                // DB에서 사용자의 모든 세션 정보 삭제
                int deletedCount = loginMapper.deleteUserSessionByUserId(userId);
                log.info("DB에서 삭제된 세션 수: {}", deletedCount);

                // 실제 세션 무효화 (현재 세션은 제외)
                for (UserSessionVO sessionVO : existingSessions) {
                    if (!sessionVO.getSessionId().equals(currentSessionId)) {
                        try {
                            // 여기서는 실제 세션 객체에 접근할 수 없으므로 DB에서만 삭제
                            log.info("세션 무효화: {}", sessionVO.getSessionId());
                        } catch (Exception e) {
                            log.warn("세션 무효화 중 오류 발생: {}", e.getMessage());
                        }
                    }
                }
            } else {
                log.info("동일 계정의 기존 세션이 없음: {}", userId);
            }
        } catch (Exception e) {
            log.error("세션 무효화 중 오류 발생: {}", e.getMessage(), e);
        }
    }

}
