package com.unicorn.hgzero.user.config.ldap;

import com.unicorn.hgzero.common.exception.BusinessException;
import com.unicorn.hgzero.common.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.Filter;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.stereotype.Component;

import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import java.util.List;

/**
 * LDAP 인증 및 사용자 정보 조회 클래스
 *
 * LDAP 서버와의 인증 처리 및 사용자 속성 조회 기능 제공
 * - LDAPS (포트 636) 사용
 * - 타임아웃: 5초
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class LdapAuthenticator {

    private final LdapTemplate ldapTemplate;

    @Value("${spring.ldap.user-dn-pattern:uid={0},ou=people}")
    private String userDnPattern;

    @Value("${spring.ldap.user-search-base:ou=people}")
    private String userSearchBase;

    @Value("${spring.profiles.active:dev}")
    private String activeProfile;

    /**
     * LDAP 인증 및 사용자 정보 조회
     *
     * @param username 사용자 ID
     * @param password 비밀번호
     * @return LDAP 사용자 정보
     * @throws BusinessException 인증 실패 시
     */
    public LdapUserDetails validateCredentials(String username, String password) {
        log.debug("LDAP 인증 시도: username={}, profile={}", username, activeProfile);

        // 개발 환경에서는 LDAP 인증 건너뛰기
        if ("dev".equals(activeProfile)) {
            log.info("개발 환경 - LDAP 인증 건너뛰기: username={}", username);
            return createDefaultUserDetails(username);
        }

        try {
            // 1. LDAP bind (인증)
            boolean authenticated = authenticate(username, password);
            if (!authenticated) {
                log.warn("LDAP 인증 실패: username={}", username);
                throw new BusinessException(ErrorCode.AUTHENTICATION_FAILED);
            }

            log.info("LDAP 인증 성공: username={}", username);

            // 2. 사용자 정보 조회
            LdapUserDetails userDetails = searchUser(username);
            log.debug("LDAP 사용자 정보 조회 완료: username={}, email={}", username, userDetails.getEmail());

            return userDetails;

        } catch (BusinessException e) {
            throw e;
        } catch (Exception e) {
            log.error("LDAP 인증 중 오류 발생: username={}, error={}", username, e.getMessage(), e);
            throw new BusinessException(ErrorCode.AUTHENTICATION_FAILED, "LDAP 인증 중 오류가 발생했습니다.");
        }
    }

    /**
     * LDAP 인증 수행
     *
     * @param username 사용자 ID
     * @param password 비밀번호
     * @return 인증 성공 여부
     */
    private boolean authenticate(String username, String password) {
        try {
            // EqualsFilter를 사용하여 uid로 검색
            Filter filter = new EqualsFilter("uid", username);

            // LDAP 인증 수행
            // Protocol: LDAPS (636), Timeout: 5s (LdapTemplate 설정에서 관리)
            boolean authenticated = ldapTemplate.authenticate(
                    userSearchBase,
                    filter.encode(),
                    password
            );

            return authenticated;

        } catch (Exception e) {
            log.error("LDAP bind 실패: username={}, error={}", username, e.getMessage());
            return false;
        }
    }

    /**
     * LDAP에서 사용자 정보 조회
     *
     * 조회 속성:
     * - cn (Common Name): 사용자 이름
     * - mail: 이메일
     * - department: 부서
     * - title: 직급
     *
     * @param username 사용자 ID
     * @return LDAP 사용자 정보
     */
    private LdapUserDetails searchUser(String username) {
        log.debug("LDAP 사용자 정보 조회 시작: username={}", username);

        try {
            // uid로 사용자 검색
            Filter filter = new EqualsFilter("uid", username);

            // 사용자 속성 조회
            List<LdapUserDetails> userList = ldapTemplate.search(
                    userSearchBase,
                    filter.encode(),
                    new UserAttributesMapper(username)
            );

            if (userList.isEmpty()) {
                log.warn("LDAP에서 사용자를 찾을 수 없음: username={}", username);
                // 사용자를 찾을 수 없어도 기본 정보로 생성
                return createDefaultUserDetails(username);
            }

            return userList.get(0);

        } catch (Exception e) {
            log.error("LDAP 사용자 정보 조회 실패: username={}, error={}", username, e.getMessage(), e);
            // 오류 발생 시 기본 정보로 생성
            return createDefaultUserDetails(username);
        }
    }

    /**
     * 기본 사용자 정보 생성 (LDAP 조회 실패 시)
     *
     * @param username 사용자 ID
     * @return 기본 사용자 정보
     */
    private LdapUserDetails createDefaultUserDetails(String username) {
        return LdapUserDetails.builder()
                .userId(username)
                .username(username)
                .email(username + "@example.com")
                .department("미지정")
                .title("미지정")
                .build();
    }

    /**
     * LDAP 속성을 LdapUserDetails로 매핑하는 Mapper
     */
    private static class UserAttributesMapper implements AttributesMapper<LdapUserDetails> {
        private final String userId;

        public UserAttributesMapper(String userId) {
            this.userId = userId;
        }

        @Override
        public LdapUserDetails mapFromAttributes(Attributes attributes) throws NamingException {
            return LdapUserDetails.builder()
                    .userId(userId)
                    .username(getAttributeValue(attributes, "cn", userId))
                    .email(getAttributeValue(attributes, "mail", userId + "@example.com"))
                    .department(getAttributeValue(attributes, "department", "미지정"))
                    .title(getAttributeValue(attributes, "title", "미지정"))
                    .build();
        }

        /**
         * 속성 값 조회 (속성이 없으면 기본값 반환)
         *
         * @param attributes 속성 목록
         * @param attributeName 속성 이름
         * @param defaultValue 기본값
         * @return 속성 값
         */
        private String getAttributeValue(Attributes attributes, String attributeName, String defaultValue) {
            try {
                if (attributes.get(attributeName) != null) {
                    Object value = attributes.get(attributeName).get();
                    return value != null ? value.toString() : defaultValue;
                }
            } catch (NamingException e) {
                // 속성이 없거나 오류 발생 시 기본값 반환
            }
            return defaultValue;
        }
    }
}
