package com.zioinfo.mall.auth;

import com.zioinfo.mall.auth.dto.AuthHelperResult;
import com.zioinfo.mall.auth.dto.FindIdRequest;
import com.zioinfo.mall.auth.dto.FindIdResponse;
import com.zioinfo.mall.auth.dto.ResetPasswordRequest;
import com.zioinfo.mall.auth.dto.SignupRequest;
import com.zioinfo.mall.auth.mapper.UserMapper;
import com.zioinfo.mall.uiws.auth.TwoFactorService;
import com.zioinfo.mall.uiws.common.UiwsApiException;
import com.zioinfo.mall.uiws.common.UiwsErrorCode;
import com.zioinfo.mall.uiws.common.mail.MailSender;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

/**
 * GUARDiA Mall 인증 서비스.
 *
 * <p>★ 고객/운영 분리: Mall 은 {@code mall_account} 단일 테이블/단일 로그인이지만 역할로 구분된다.
 * <ul>
 *   <li>고객(USER) — 쇼핑 로그인: 2FA 미적용(기존 단일 JWT 흐름 그대로, 회귀 0).</li>
 *   <li>운영(ADMIN/MANAGER) — 관리자/매장 로그인: UIWS 2FA 레이어 적용(verify-token + 이메일코드 + 실패잠금).</li>
 * </ul>
 * 2FA 전역 토글({@code mall.uiws.auth.twofa-enabled})이 off 면 운영 로그인도 단일 JWT(회귀 0).
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {

    private static final SecureRandom RANDOM = new SecureRandom();
    /** 임시 비밀번호 문자셋(혼동 문자 0/O/1/l/I 제외). */
    private static final String TMP_PW_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789@#$%";

    private final UserMapper userMapper;
    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;
    private final TwoFactorService twoFactorService;
    private final MailSender mailSender;

    /** 운영(2FA 대상) 역할 여부 — 고객(USER)은 제외. */
    private static boolean isOperationsRole(String role) {
        return "ADMIN".equalsIgnoreCase(role) || "MANAGER".equalsIgnoreCase(role);
    }

    /**
     * 1차 로그인.
     * @return 고객 또는 2FA off: { token, type, twofa:"false" }
     *         운영 + 2FA on  : { twofa:"true", verifyToken, step:"EMAIL", maskedEmail }
     */
    public Map<String, String> login(String username, String password) {
        MallUser user = userMapper.findByUsername(username);

        // 잠금 우선 차단(존재하는 운영 계정에 한해 — 존재 여부 누설 최소화)
        if (user != null && isOperationsRole(user.getRole()) && twoFactorService.isLocked(user)) {
            throw new UiwsApiException(UiwsErrorCode.ACCOUNT_LOCKED);
        }
        if (user == null || !user.isActive()) {
            throw new RuntimeException("ERR-AUTH-001: 존재하지 않거나 비활성 계정");
        }
        // 회원가입 승인 게이트(로그인 보조 이식): signup 으로 가입한 운영자(approved=false)는 비번 일치 전 차단.
        // approved 가 NULL(기존 계정/고객 register 흐름)이면 게이트 미적용 → 회귀 0.
        if (isOperationsRole(user.getRole()) && Boolean.FALSE.equals(user.getApproved())) {
            throw new RuntimeException("ERR-AUTH-003: 승인 대기 중인 계정입니다. 관리자 승인 후 이용 가능합니다.");
        }

        boolean twofaTarget = twoFactorService.isEnabled() && isOperationsRole(user.getRole());

        if (!passwordEncoder.matches(password, user.getPasswordHash())) {
            // 운영 + 2FA 활성 시 실패 누적/잠금. 고객/비활성 시 기존 동작(메시지만) 유지.
            if (twofaTarget) {
                twoFactorService.recordLoginFailure(username);
                MallUser after = userMapper.findByUsername(username);
                if (after != null && Boolean.TRUE.equals(after.getLocked())) {
                    throw new UiwsApiException(UiwsErrorCode.ACCOUNT_LOCKED);
                }
            }
            throw new RuntimeException("ERR-AUTH-002: 비밀번호 불일치");
        }

        // 비밀번호 검증 통과
        if (twofaTarget) {
            Map<String, String> step1 = twoFactorService.beginTwoFactor(user);
            return Map.of(
                    "twofa", "true",
                    "verifyToken", step1.get("verifyToken"),
                    "step", step1.get("step"),
                    "maskedEmail", step1.getOrDefault("maskedEmail", ""));
        }

        // 고객(USER) 또는 2FA 비활성 — 기존 단일 로그인 흐름(회귀 0)
        if (isOperationsRole(user.getRole())) {
            userMapper.resetLoginFail(username);
        }
        String token = jwtUtil.generate(username, user.getRole());
        return Map.of("twofa", "false", "token", token, "type", "Bearer");
    }

    /** 고객 셀프 회원가입 — 항상 USER 역할로 생성(2FA 미적용 대상). */
    public Map<String, String> register(String username, String password, String displayName) {
        if (username == null || username.isBlank() || password == null || password.isBlank()) {
            throw new IllegalArgumentException("ERR-AUTH-400: username/password 필수");
        }
        if (userMapper.countByUsername(username) > 0) {
            throw new RuntimeException("ERR-AUTH-409: 이미 존재하는 아이디");
        }
        MallUser user = new MallUser();
        user.setUsername(username);
        user.setPasswordHash(passwordEncoder.encode(password));
        user.setRole("USER");
        user.setDisplayName(displayName == null || displayName.isBlank() ? username : displayName);
        user.setActive(true);
        userMapper.insert(user);
        String token = jwtUtil.generate(username, "USER");
        return Map.of("twofa", "false", "token", token, "type", "Bearer");
    }

    public Map<String, String> me(String token) {
        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);
        return Map.of("username", username, "role", role);
    }

    // ── 로그인 보조 이식: 회원가입 / 아이디찾기 / 비밀번호 초기화 ───────────────────
    //    대상: Mall 관리자/운영자 계정(mall_account, ADMIN/MANAGER). 고객(USER) register 흐름과 분리.

    /**
     * 운영자 회원가입(승인 대기). username/email 중복 검사 후 approved=false·role=MANAGER 로 INSERT.
     * 비밀번호는 BCrypt 저장. 승인 전까지 로그인 차단(login 의 승인 게이트).
     */
    @Transactional
    public AuthHelperResult signup(SignupRequest req) {
        if (req.username() == null || req.username().isBlank()
                || req.password() == null || req.password().length() < 4
                || req.email() == null || req.email().isBlank()) {
            return new AuthHelperResult(false, "아이디·비밀번호(4자 이상)·이메일은 필수입니다.");
        }
        if (userMapper.countByUsername(req.username()) > 0) {
            return new AuthHelperResult(false, "이미 사용 중인 아이디입니다.");
        }
        if (userMapper.countByEmail(req.email()) > 0) {
            return new AuthHelperResult(false, "이미 등록된 이메일입니다.");
        }
        MallUser u = new MallUser();
        u.setUsername(req.username());
        u.setPasswordHash(passwordEncoder.encode(req.password()));
        u.setDisplayName(req.displayName() != null && !req.displayName().isBlank()
                ? req.displayName() : req.username());
        u.setEmail(req.email());
        userMapper.signup(u);
        log.info("[auth-helper] signup pending approval: username={}", req.username());
        return new AuthHelperResult(true, "가입 신청이 접수되었습니다. 관리자 승인 후 로그인할 수 있습니다.");
    }

    /**
     * 아이디 찾기: 표시명+이메일 동시 일치 운영자 1건 조회. username 은 부분 마스킹 후 반환.
     * 미발견 시 found=false(원문 username 절대 미노출).
     */
    public FindIdResponse findId(FindIdRequest req) {
        if (req.displayName() == null || req.displayName().isBlank()
                || req.email() == null || req.email().isBlank()) {
            return new FindIdResponse(false, "");
        }
        MallUser u = userMapper.findByDisplayNameAndEmail(req.displayName(), req.email());
        if (u == null) {
            return new FindIdResponse(false, "");
        }
        return new FindIdResponse(true, maskUsername(u.getUsername()));
    }

    /**
     * 비밀번호 초기화: username+email 일치 검증 → 임시비번 생성·BCrypt 저장·잠금/실패카운트 해제.
     * 임시비번은 메일(미설정 시 LogMailSender 로그)로만 전달. API 응답·로그 메시지에 비번 미노출.
     * 대상 미존재여도 success=true(계정 열거 방지).
     */
    @Transactional
    public AuthHelperResult resetPassword(ResetPasswordRequest req) {
        final String okMsg = "임시 비밀번호를 등록된 이메일로 발송했습니다. 메일을 확인하세요.";
        if (req.username() == null || req.username().isBlank()
                || req.email() == null || req.email().isBlank()) {
            return new AuthHelperResult(false, "아이디와 이메일을 모두 입력하세요.");
        }
        MallUser u = userMapper.findByUsernameAndEmail(req.username(), req.email());
        if (u == null) {
            // 존재 여부 누설 방지 — 동일 성공 메시지 반환(실제 발송 없음).
            log.info("[auth-helper] reset-password no match (suppressed): username={}", req.username());
            return new AuthHelperResult(true, okMsg);
        }
        String tempPw = generateTempPassword();
        userMapper.updatePasswordHash(req.username(), passwordEncoder.encode(tempPw));

        String subject = "[GUARDiA Mall] 임시 비밀번호 안내";
        String body = String.format(
                "안녕하세요 %s 님,\n임시 비밀번호는 [%s] 입니다.\n로그인 후 즉시 비밀번호를 변경하세요.",
                u.getDisplayName() != null ? u.getDisplayName() : u.getUsername(), tempPw);
        // 메일 본문에만 임시비번 포함. mailSender 미설정 환경은 LogMailSender 폴백(서버 로그).
        mailSender.send(u.getEmail(), subject, body);
        log.info("[auth-helper] reset-password issued temp pw (sent via mail/log): username={}", req.username());
        return new AuthHelperResult(true, okMsg);
    }

    private static String generateTempPassword() {
        StringBuilder sb = new StringBuilder(10);
        for (int i = 0; i < 10; i++) {
            sb.append(TMP_PW_CHARS.charAt(RANDOM.nextInt(TMP_PW_CHARS.length())));
        }
        return sb.toString();
    }

    /** username 부분 마스킹: 앞 2자만 노출(예: admin → ad***). 2자 이하는 첫 글자만. */
    private static String maskUsername(String username) {
        if (username == null || username.isBlank()) {
            return "";
        }
        if (username.length() <= 2) {
            return username.charAt(0) + "*";
        }
        return username.substring(0, 2) + "*".repeat(Math.max(1, username.length() - 2));
    }
}
