package com.zioinfo.mall.uiws.auth;

import com.zioinfo.mall.auth.JwtUtil;
import com.zioinfo.mall.auth.mapper.UserMapper;
import com.zioinfo.mall.auth.MallUser;
import com.zioinfo.mall.uiws.common.UiwsApiException;
import com.zioinfo.mall.uiws.common.UiwsErrorCode;
import com.zioinfo.mall.uiws.common.mail.MailSender;
import com.zioinfo.mall.uiws.config.UiwsProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Map;

/**
 * UIWS 2FA(이메일 코드) 레이어. ERP 기존 단일 로그인을 보존하면서 2단계 인증을 추가한다.
 *
 * 흐름:
 *  1) 1차 로그인 성공 → {@link #beginTwoFactor}: verify-token 발급 + 이메일 인증코드 발송(LogMailSender 폴백) + 감사 기록.
 *  2) {@code POST /api/auth/verify}(verifyToken + code) → {@link #verify}: 코드 검증 후 access/refresh 발급.
 *  3) 로그인 실패 누적 max-login-fail 회 → 계정 잠금({@link #recordLoginFailure}).
 *
 * 보안:
 *  - 인증코드는 메일/감사 채널로만 전달. API 응답·로그 메시지에 코드/비밀번호/자격증명 절대 미노출(불변규칙).
 *  - 외부 API 호출 없음(MailSender 폴백은 로그). Ollama 외 외부 통신 금지 준수.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class TwoFactorService {

    private static final SecureRandom RANDOM = new SecureRandom();
    private static final String SYSTEM = "SYSTEM";

    private final UserMapper userMapper;
    private final LoginVerifyMapper loginVerifyMapper;
    private final JwtUtil jwtUtil;
    private final MailSender mailSender;
    private final UiwsProperties properties;

    public boolean isEnabled() {
        return properties.getAuth().isTwofaEnabled();
    }

    /** 잠금 여부(1차 로그인 전 차단용). */
    public boolean isLocked(MallUser user) {
        return Boolean.TRUE.equals(user.getLocked());
    }

    /**
     * 1차 로그인 성공 후 2차 인증 시작: verify-token 발급 + 이메일코드 발송 + 감사 기록 + 실패카운트 초기화.
     * @return { verifyToken, step:"EMAIL", maskedEmail }
     */
    @Transactional
    public Map<String, String> beginTwoFactor(MallUser user) {
        long codeValidity = properties.getAuth().getEmailCodeValiditySeconds();
        long tokenValidity = properties.getAuth().getVerifyTokenValiditySeconds();

        String code = String.format("%06d", RANDOM.nextInt(1_000_000));
        LocalDateTime expire = LocalDateTime.now().plusSeconds(codeValidity);

        // user 테이블에 코드/만료 저장(실패카운트 초기화) + 감사 이력 기록
        userMapper.saveEmailCode(user.getUsername(), code, expire);
        recordVerifyAttempt(user.getUsername(), code, expire);

        // 이메일 발송(미설정 환경은 LogMailSender 폴백). 코드는 메일 본문에만.
        String subject = "[GUARDiA Mall] 로그인 2차 인증 코드";
        String body = String.format(
                "안녕하세요 %s 님,\n로그인 2차 인증 코드는 [%s] 입니다.\n유효시간: %d초",
                user.getDisplayName() != null ? user.getDisplayName() : user.getUsername(), code, codeValidity);
        if (user.getEmail() != null && !user.getEmail().isBlank()) {
            mailSender.send(user.getEmail(), subject, body);
        } else {
            log.warn("[2FA] no email for user={} — code logged only", user.getUsername());
        }

        String verifyToken = jwtUtil.generateVerifyToken(user.getUsername(), tokenValidity);
        // 응답에는 코드 미포함 — verifyToken/step/maskedEmail 만.
        return Map.of(
                "verifyToken", verifyToken,
                "step", "EMAIL",
                "maskedEmail", maskEmail(user.getEmail()));
    }

    /**
     * 2차 검증: verify-token + code 검증 → access/refresh 발급. 코드 폐기 + 감사 이력 검증완료.
     * @return { token, refreshToken, username, role }
     */
    @Transactional
    public Map<String, String> verify(String verifyToken, String code) {
        String username = jwtUtil.parseVerifyTokenUsername(verifyToken);
        if (username == null) {
            throw new UiwsApiException(UiwsErrorCode.VERIFY_TOKEN_INVALID);
        }
        MallUser user = userMapper.findByUsername(username);
        if (user == null) {
            throw new UiwsApiException(UiwsErrorCode.VERIFY_TOKEN_INVALID);
        }
        if (Boolean.TRUE.equals(user.getLocked())) {
            throw new UiwsApiException(UiwsErrorCode.ACCOUNT_LOCKED);
        }

        boolean codeOk = user.getEmailVerifyCode() != null
                && user.getEmailVerifyCode().equals(code)
                && user.getEmailVerifyExpire() != null
                && user.getEmailVerifyExpire().isAfter(LocalDateTime.now());
        if (!codeOk) {
            throw new UiwsApiException(UiwsErrorCode.VERIFY_CODE_INVALID);
        }

        // 코드 폐기 + 감사 검증완료
        userMapper.clearEmailCode(username);
        loginVerifyMapper.markLatestVerified(username);

        String access = jwtUtil.generate(user.getUsername(), user.getRole());
        // refresh 토큰: ERP 기존 access 토큰 정책 재사용(별도 refresh 정책 부재 → 동일 발급).
        String refresh = jwtUtil.generate(user.getUsername(), user.getRole());
        return Map.of(
                "token", access,
                "refreshToken", refresh,
                "type", "Bearer",
                "username", user.getUsername(),
                "role", user.getRole() == null ? "" : user.getRole());
    }

    /** 로그인 비밀번호 실패 시 누적/잠금 처리. */
    @Transactional
    public void recordLoginFailure(String username) {
        userMapper.incrementLoginFail(username, properties.getAuth().getMaxLoginFail());
    }

    private void recordVerifyAttempt(String username, String code, LocalDateTime expire) {
        UiwsLoginVerify v = new UiwsLoginVerify();
        v.setUserId(username);
        v.setVerifyMethod("EMAIL");
        v.setVerifyCode(code);
        v.setExpireAt(expire);
        v.setVerifiedYn("N");
        v.setCreatedBy(SYSTEM);
        v.setCreatedAt(LocalDateTime.now());
        loginVerifyMapper.insert(v);
    }

    /** 이메일 마스킹(자격증명 보호): ab****@domain. */
    private static String maskEmail(String email) {
        if (email == null || email.isBlank() || !email.contains("@")) {
            return "";
        }
        int at = email.indexOf('@');
        String local = email.substring(0, at);
        String domain = email.substring(at);
        if (local.length() <= 2) {
            return local.charAt(0) + "*" + domain;
        }
        return local.substring(0, 2) + "*".repeat(Math.max(1, local.length() - 2)) + domain;
    }
}
