package com.zioinfo.kintex.security;

import com.zioinfo.kintex.auth.EventRole;
import com.zioinfo.kintex.auth.JwtService;
import com.zioinfo.kintex.auth.dto.LoginResponse;
import com.zioinfo.kintex.auth.dto.WorkspaceDto;
import com.zioinfo.kintex.auth.mapper.UserMapper;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.security.dto.OtpSetupResponse;
import com.zioinfo.kintex.security.dto.OtpStatusResponse;
import com.zioinfo.kintex.security.dto.SecureLoginResponse;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 2차 인증(OTP)·로그인 실패 잠금 오케스트레이션 레이어(§5B).
 * <p>기존 {@code AuthServiceImpl}/{@code JwtService}는 <b>교체하지 않고</b> 이 레이어만 얹는다 —
 * 자격검증·토큰발급은 기존 빈(UserMapper·PasswordEncoder·JwtService)을 그대로 재사용한다.
 * 보안 불변(계약 §0-3): password_hash·otp_secret 은 응답에 절대 노출하지 않는다.
 */
@Service
public class TwoFactorService {

    private final AccountSecurityMapper securityMapper;
    private final UserMapper userMapper;
    private final JwtService jwtService;
    private final PasswordEncoder passwordEncoder;
    private final TotpService totpService;
    private final LoginAttemptService loginAttempts;
    private final OtpChallengeStore challengeStore;
    private final SecretCipher secretCipher;
    private final SecurityPolicyProperties props;

    public TwoFactorService(AccountSecurityMapper securityMapper, UserMapper userMapper,
                            JwtService jwtService, PasswordEncoder passwordEncoder,
                            TotpService totpService, LoginAttemptService loginAttempts,
                            OtpChallengeStore challengeStore, SecretCipher secretCipher,
                            SecurityPolicyProperties props) {
        this.securityMapper = securityMapper;
        this.userMapper = userMapper;
        this.jwtService = jwtService;
        this.passwordEncoder = passwordEncoder;
        this.totpService = totpService;
        this.loginAttempts = loginAttempts;
        this.challengeStore = challengeStore;
        this.secretCipher = secretCipher;
        this.props = props;
    }

    /** 1단계: 잠금 확인 → 자격검증 → (OTP등록자=챌린지, 필수역할 미등록=등록강제, 그 외=토큰). */
    public SecureLoginResponse secureLogin(String email, String rawPassword) {
        Map<String, Object> row = securityMapper.findAuthSecurityByEmail(email);
        loginAttempts.assertNotLocked(row);
        String hash = row == null ? null : str(row.get("passwordHash"));
        if (row == null || hash == null || !passwordEncoder.matches(rawPassword, hash)) {
            loginAttempts.recordFailure(email, row);
            throw new ApiException(ErrorCode.UNAUTHORIZED, "이메일 또는 비밀번호가 올바르지 않습니다.");
        }
        String userId = str(row.get("userId"));
        loginAttempts.recordSuccess(userId);
        boolean hallManager = bool(row.get("hallManager"));
        if (Boolean.TRUE.equals(bool(row.get("otpEnabled")))) {
            return SecureLoginResponse.otpRequired(challengeStore.issue(userId));
        }
        // 미등록 — 필수 역할이면 등록 강제(프리오스 챌린지), 관람객 등은 선택(즉시 토큰).
        if (props.getOtp().isEnforce()
                && requiresOtp(userId, hallManager, str(row.get("roleCode")))) {
            return SecureLoginResponse.otpEnroll(challengeStore.issue(userId));
        }
        return SecureLoginResponse.ok(buildLoginResponse(userId, str(row.get("displayName")),
                hallManager, str(row.get("roleCode")), str(row.get("tenantId"))));
    }

    /**
     * 2단계: 챌린지 토큰 + OTP 코드 검증 → 최종 로그인 토큰.
     * 챌린지는 검증 성공 시에만 소비한다 — 오입력 1회가 세션을 태우면 사용자가 "코드 틀림→세션 만료"
     * 루프에 갇힌다(2026-07-12 라이브 장애). 재시도는 챌린지당 실패 5회·TTL 5분 내로 제한(브루트포스 방어).
     */
    public LoginResponse verifyOtp(String challengeToken, String code) {
        String userId = challengeStore.peek(challengeToken);
        if (userId == null) {
            throw new ApiException(ErrorCode.OTP_REQUIRED, "2차 인증 세션이 만료되었습니다. 다시 로그인해 주세요.");
        }
        Map<String, Object> sec = securityMapper.findSecurityById(userId);
        if (sec == null || !totpService.verify(secretCipher.open(str(sec.get("otpSecret"))), code)) {
            boolean alive = challengeStore.recordFailure(challengeToken);
            throw new ApiException(ErrorCode.OTP_INVALID,
                    alive ? "OTP 코드가 올바르지 않습니다. 다시 입력해 주세요."
                          : "시도 횟수를 초과했습니다. 다시 로그인해 주세요.");
        }
        challengeStore.consume(challengeToken);
        return buildLoginResponse(userId, str(sec.get("displayName")), bool(sec.get("hallManager")),
                str(sec.get("roleCode")), str(sec.get("tenantId")));
    }

    /** 로그인 중 등록 강제 — setup 단계(프리오스 챌린지 유지). 시크릿 발급·QR 반환. */
    public OtpSetupResponse enrollSetup(String challengeToken) {
        String userId = challengeStore.peek(challengeToken);
        if (userId == null) {
            throw new ApiException(ErrorCode.OTP_REQUIRED, "2차 인증 세션이 만료되었습니다. 다시 로그인해 주세요.");
        }
        Map<String, Object> sec = securityMapper.findSecurityById(userId);
        String label = sec != null ? str(sec.get("email")) : userId;
        return setupOtp(userId, label != null ? label : userId);
    }

    /** 로그인 중 등록 강제 — verify 단계(검증 성공 시에만 챌린지 소비·활성화 후 최종 토큰). */
    public LoginResponse enrollVerify(String challengeToken, String code) {
        String userId = challengeStore.peek(challengeToken);
        if (userId == null) {
            throw new ApiException(ErrorCode.OTP_REQUIRED, "2차 인증 세션이 만료되었습니다. 다시 로그인해 주세요.");
        }
        Map<String, Object> sec = securityMapper.findSecurityById(userId);
        if (sec == null || !totpService.verify(secretCipher.open(str(sec.get("otpSecret"))), code)) {
            boolean alive = challengeStore.recordFailure(challengeToken);
            throw new ApiException(ErrorCode.OTP_INVALID,
                    alive ? "OTP 코드가 올바르지 않습니다. 다시 입력해 주세요."
                          : "시도 횟수를 초과했습니다. 다시 로그인해 주세요.");
        }
        challengeStore.consume(challengeToken);
        securityMapper.enableOtp(userId);
        return buildLoginResponse(userId, str(sec.get("displayName")), bool(sec.get("hallManager")),
                str(sec.get("roleCode")), str(sec.get("tenantId")));
    }

    /** OTP 등록 시작 — 시크릿 발급(봉인 저장)·QR 반환(미활성, confirm 필요). */
    public OtpSetupResponse setupOtp(String userId, String userLabel) {
        String secret = totpService.generateSecret();
        securityMapper.saveOtpSecret(userId, secretCipher.seal(secret)); // 저장은 암호문
        return new OtpSetupResponse(secret,                               // 화면 1회 표기는 평문
                totpService.otpAuthUri(secret, userLabel),
                totpService.qrImageDataUri(secret, userLabel));
    }

    /** OTP 등록 확인 — 코드 검증 후 활성화. */
    public void confirmOtp(String userId, String code) {
        Map<String, Object> sec = securityMapper.findSecurityById(userId);
        if (sec == null || !totpService.verify(secretCipher.open(str(sec.get("otpSecret"))), code)) {
            throw new ApiException(ErrorCode.OTP_INVALID);
        }
        securityMapper.enableOtp(userId);
    }

    /**
     * 필수 역할 판정(계약 §5B): 홀매니저·플랫폼 ADMIN/MANAGER·행사 역할 보유자(주최자/참가/장치)는 OTP 필수,
     * 순수 관람객(행사 역할 없음)만 선택. 행사 역할 유무는 event_member 로 판정.
     */
    private boolean requiresOtp(String userId, boolean hallManager, String roleCode) {
        if (hallManager) {
            return true;
        }
        if (roleCode != null) {
            String r = roleCode.toUpperCase();
            if (r.equals("ADMIN") || r.equals("MANAGER")) {
                return true;
            }
        }
        List<Map<String, Object>> roleRows = userMapper.findEventRoles(userId);
        return roleRows != null && !roleRows.isEmpty();
    }

    /** OTP 초기화(본인 해제 또는 관리자 리셋). */
    public void resetOtp(String userId) {
        securityMapper.resetOtp(userId);
    }

    /** OTP 상태(마이페이지). 시크릿 미포함. */
    public OtpStatusResponse otpStatus(String userId) {
        Map<String, Object> sec = securityMapper.findSecurityById(userId);
        boolean enabled = sec != null && Boolean.TRUE.equals(bool(sec.get("otpEnabled")));
        return new OtpStatusResponse(enabled, enabled ? "OTP" : "EMAIL");
    }

    /**
     * 사용자 역할 → 토큰 + 워크스페이스 카드 조립(AuthServiceImpl 로직 정합, 토큰만 재발급).
     * globalRoleCode = app_user.role_code(전역 역할, null 허용). 주의: 여기서 r.get("roleCode")는
     * event_member.role_code(행사 역할)이며 전역 role_code와 별개 — 혼동 금지.
     */
    private LoginResponse buildLoginResponse(String userId, String displayName, boolean hallManager,
                                             String globalRoleCode, String tenantId) {
        List<Map<String, Object>> roleRows = userMapper.findEventRoles(userId);
        Map<String, EventRole> eventRoles = new LinkedHashMap<>();
        if (roleRows != null) {
            for (Map<String, Object> r : roleRows) {
                String eventId = str(r.get("eventId"));
                EventRole role = parseRole(str(r.get("roleCode")));
                if (eventId != null && role != null) {
                    eventRoles.put(eventId, role);
                }
            }
        }
        String token = jwtService.issue(userId, displayName, eventRoles, hallManager, globalRoleCode, tenantId);
        List<WorkspaceDto> workspaces = new ArrayList<>();
        if (roleRows != null) {
            for (Map<String, Object> r : roleRows) {
                workspaces.add(new WorkspaceDto(str(r.get("eventId")), str(r.get("eventName")),
                        str(r.get("startDate")), str(r.get("endDate")), str(r.get("hallLabel")),
                        str(r.get("roleCode")), intVal(r.get("dday"))));
            }
        }
        return new LoginResponse(token, jwtService.ttlSeconds(),
                new LoginResponse.UserSummary(userId, displayName, hallManager, globalRoleCode), workspaces);
    }

    private static EventRole parseRole(String code) {
        if (code == null) return null;
        try { return EventRole.valueOf(code); } catch (IllegalArgumentException e) { return null; }
    }
    private static String str(Object o) { return o == null ? null : String.valueOf(o); }
    private static Boolean bool(Object o) {
        if (o == null) return null;
        if (o instanceof Boolean b) return b;
        return Boolean.parseBoolean(String.valueOf(o));
    }
    private static int intVal(Object o) {
        if (o instanceof Number n) return n.intValue();
        try { return o == null ? 0 : Integer.parseInt(String.valueOf(o)); }
        catch (NumberFormatException e) { return 0; }
    }
}
