package com.unicorn.hgzero.user.service;

import com.unicorn.hgzero.common.exception.BusinessException;
import com.unicorn.hgzero.common.exception.ErrorCode;
import com.unicorn.hgzero.user.config.jwt.JwtTokenProvider;
import com.unicorn.hgzero.user.domain.User;
import com.unicorn.hgzero.user.dto.*;
import com.unicorn.hgzero.user.repository.entity.UserEntity;
import com.unicorn.hgzero.user.repository.jpa.UserRepository;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
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.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.Filter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.crypto.SecretKey;
import javax.naming.directory.DirContext;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import io.jsonwebtoken.security.Keys;

/**
 * 사용자 인증 서비스 구현체
 * LDAP 인증, JWT 토큰 발급, Refresh Token 관리 기능 제공
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final LdapTemplate ldapTemplate;
    private final UserRepository userRepository;
    private final JwtTokenProvider jwtTokenProvider;
    private final RedisTemplate<String, String> redisTemplate;

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Value("${jwt.access-token-validity:3600}")
    private Long accessTokenValidity;

    @Value("${jwt.refresh-token-validity:604800}")
    private Long refreshTokenValidity;

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

    /**
     * 사용자 로그인
     * LDAP 인증 수행 후 JWT 토큰 발급
     */
    @Override
    @Transactional
    public LoginResponse login(LoginRequest request) {
        log.info("로그인 시도: userId={}", request.getUserId());

        // 사용자 조회 또는 생성
        UserEntity userEntity = userRepository.findById(request.getUserId())
                .orElse(null);

        // 계정 잠금 확인
        if (userEntity != null && userEntity.getLocked()) {
            // 30분 경과 시 자동 해제
            if (userEntity.getLockedAt() != null &&
                LocalDateTime.now().isAfter(userEntity.getLockedAt().plusMinutes(30))) {
                userEntity.unlock();
                userRepository.save(userEntity);
            } else {
                log.warn("계정 잠금: userId={}", request.getUserId());
                throw new BusinessException(ErrorCode.ACCOUNT_LOCKED);
            }
        }

        // LDAP 인증
        try {
            authenticateWithLdap(request.getUserId(), request.getPassword());
            log.info("LDAP 인증 성공: userId={}", request.getUserId());
        } catch (Exception e) {
            log.error("LDAP 인증 실패: userId={}, error={}", request.getUserId(), e.getMessage());

            // 로그인 실패 기록
            if (userEntity != null) {
                userEntity.recordLoginFailure();
                userRepository.save(userEntity);
            }

            throw new BusinessException(ErrorCode.AUTHENTICATION_FAILED, "인증에 실패했습니다.");
        }

        // 사용자 정보 조회 또는 생성
        if (userEntity == null) {
            // LDAP에서 추가 정보 조회 (실제 환경에서는 LDAP에서 이메일 등을 가져와야 함)
            userEntity = UserEntity.builder()
                    .userId(request.getUserId())
                    .username(request.getUserId()) // LDAP에서 가져온 실제 이름 사용
                    .email(request.getUserId() + "@example.com") // LDAP에서 가져온 실제 이메일 사용
                    .authority("USER")
                    .locked(false)
                    .failedLoginAttempts(0)
                    .build();
        }

        // 로그인 성공 기록
        userEntity.recordLoginSuccess();
        userEntity = userRepository.save(userEntity);

        User user = userEntity.toDomain();

        // JWT 토큰 생성
        String accessToken = generateAccessToken(user);
        String refreshToken = generateRefreshToken(user);

        // Refresh Token을 Redis에 저장
        saveRefreshToken(user.getUserId(), refreshToken);

        log.info("로그인 성공: userId={}", request.getUserId());

        return LoginResponse.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .tokenType("Bearer")
                .accessTokenValidity(accessTokenValidity)
                .refreshTokenValidity(refreshTokenValidity)
                .user(LoginResponse.UserInfo.builder()
                        .userId(user.getUserId())
                        .username(user.getUsername())
                        .email(user.getEmail())
                        .authority(user.getAuthority())
                        .build())
                .build();
    }

    /**
     * Access Token 갱신
     */
    @Override
    @Transactional(readOnly = true)
    public RefreshTokenResponse refresh(RefreshTokenRequest request) {
        log.info("토큰 갱신 시도");

        // Refresh Token 검증
        if (!jwtTokenProvider.validateToken(request.getRefreshToken())) {
            log.warn("유효하지 않은 Refresh Token");
            throw new BusinessException(ErrorCode.INVALID_REFRESH_TOKEN);
        }

        // Refresh Token에서 사용자 ID 추출
        String userId = jwtTokenProvider.getUserId(request.getRefreshToken());

        // Redis에서 저장된 Refresh Token 조회
        String savedRefreshToken = getRefreshToken(userId);
        if (savedRefreshToken == null || !savedRefreshToken.equals(request.getRefreshToken())) {
            log.warn("Refresh Token이 일치하지 않음: userId={}", userId);
            throw new BusinessException(ErrorCode.INVALID_REFRESH_TOKEN);
        }

        // 사용자 조회
        UserEntity userEntity = userRepository.findById(userId)
                .orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND, "사용자를 찾을 수 없습니다."));

        User user = userEntity.toDomain();

        // 새로운 Access Token 생성
        String newAccessToken = generateAccessToken(user);

        log.info("토큰 갱신 성공: userId={}", userId);

        return RefreshTokenResponse.builder()
                .accessToken(newAccessToken)
                .tokenType("Bearer")
                .accessTokenValidity(accessTokenValidity)
                .build();
    }

    /**
     * 로그아웃
     */
    @Override
    @Transactional
    public void logout(LogoutRequest request, String userId) {
        log.info("로그아웃: userId={}", userId);

        // Redis에서 Refresh Token 삭제
        deleteRefreshToken(userId);

        log.info("로그아웃 완료: userId={}", userId);
    }

    /**
     * Access Token 검증
     */
    @Override
    @Transactional(readOnly = true)
    public TokenValidateResponse validateToken(String token) {
        log.debug("토큰 검증 시작");

        try {
            // 토큰 유효성 검증
            if (!jwtTokenProvider.validateToken(token)) {
                return TokenValidateResponse.builder()
                        .valid(false)
                        .expired(jwtTokenProvider.isTokenExpired(token))
                        .errorMessage("유효하지 않은 토큰입니다.")
                        .build();
            }

            // 토큰에서 사용자 정보 추출
            String userId = jwtTokenProvider.getUserId(token);
            String username = jwtTokenProvider.getUsername(token);
            String authority = jwtTokenProvider.getAuthority(token);

            // 사용자 조회
            UserEntity userEntity = userRepository.findById(userId)
                    .orElse(null);

            return TokenValidateResponse.builder()
                    .valid(true)
                    .userId(userId)
                    .username(username)
                    .email(userEntity != null ? userEntity.getEmail() : null)
                    .authority(authority)
                    .expired(false)
                    .build();

        } catch (Exception e) {
            log.error("토큰 검증 실패: error={}", e.getMessage());
            return TokenValidateResponse.builder()
                    .valid(false)
                    .expired(false)
                    .errorMessage("토큰 검증 중 오류가 발생했습니다.")
                    .build();
        }
    }

    /**
     * LDAP 인증 수행
     */
    private void authenticateWithLdap(String userId, String password) {
        // LDAP DN 생성
        String userDn = userDnPattern.replace("{0}", userId);

        // LDAP 인증
        Filter filter = new EqualsFilter("uid", userId);
        boolean authenticated = ldapTemplate.authenticate("", filter.encode(), password);

        if (!authenticated) {
            throw new BusinessException(ErrorCode.AUTHENTICATION_FAILED);
        }
    }

    /**
     * Access Token 생성
     */
    private String generateAccessToken(User user) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + accessTokenValidity * 1000);

        SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));

        return Jwts.builder()
                .setSubject(user.getUserId())
                .claim("username", user.getUsername())
                .claim("authority", user.getAuthority())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(key)
                .compact();
    }

    /**
     * Refresh Token 생성
     */
    private String generateRefreshToken(User user) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + refreshTokenValidity * 1000);

        SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));

        return Jwts.builder()
                .setSubject(user.getUserId())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(key)
                .compact();
    }

    /**
     * Refresh Token을 Redis에 저장
     */
    private void saveRefreshToken(String userId, String refreshToken) {
        String key = "refresh_token:" + userId;
        redisTemplate.opsForValue().set(key, refreshToken, refreshTokenValidity, TimeUnit.SECONDS);
        log.debug("Refresh Token 저장: userId={}", userId);
    }

    /**
     * Redis에서 Refresh Token 조회
     */
    private String getRefreshToken(String userId) {
        String key = "refresh_token:" + userId;
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * Redis에서 Refresh Token 삭제
     */
    private void deleteRefreshToken(String userId) {
        String key = "refresh_token:" + userId;
        redisTemplate.delete(key);
        log.debug("Refresh Token 삭제: userId={}", userId);
    }
}
