package com.zioinfo.kintex.auth;

import com.zioinfo.kintex.auth.dto.ForgotPasswordRequest;
import com.zioinfo.kintex.auth.dto.MessageResponse;
import com.zioinfo.kintex.auth.dto.RegisterRequest;
import com.zioinfo.kintex.auth.dto.RegisterResponse;
import com.zioinfo.kintex.auth.dto.ResetPasswordRequest;
import com.zioinfo.kintex.auth.mapper.PublicAuthMapper;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.security.SecureRandom;
import java.time.OffsetDateTime;
import java.util.Map;
import java.util.UUID;

/**
 * 공개 인증 서비스 — 회원가입·비밀번호 찾기/초기화(§C-1 프론트 계약 정합).
 *
 * <p>기존 {@link AuthServiceImpl}(로그인)·Phase B(TotpService·LoginAttempt) 로직은 <b>교체하지 않고</b>
 * 순증한다. 비밀번호/재설정 코드/해시는 응답·로그에 절대 노출하지 않으며(계약 §0-3),
 * 비밀번호 찾기는 계정 존재 여부와 무관하게 항상 성공 안내를 반환한다(사용자 열거 방지).
 */
@Service
public class PublicAuthService {

    private static final Logger log = LoggerFactory.getLogger(PublicAuthService.class);
    private static final SecureRandom RANDOM = new SecureRandom();

    /** 재설정 코드 유효시간(분). */
    private static final int RESET_TTL_MINUTES = 10;
    /** 재설정 코드 검증 최대 시도(초과 시 코드 무효화). */
    private static final int RESET_MAX_ATTEMPTS = 5;
    private static final String GENERIC_FORGOT_MESSAGE =
            "입력하신 이메일이 등록되어 있으면 비밀번호 재설정 안내를 보내드립니다.";

    private final PublicAuthMapper mapper;
    private final PasswordEncoder passwordEncoder;
    private final com.zioinfo.kintex.mail.MailService mailService;
    private final com.zioinfo.kintex.mail.MailProperties mailProperties;

    public PublicAuthService(PublicAuthMapper mapper, PasswordEncoder passwordEncoder,
                             com.zioinfo.kintex.mail.MailService mailService,
                             com.zioinfo.kintex.mail.MailProperties mailProperties) {
        this.mapper = mapper;
        this.passwordEncoder = passwordEncoder;
        this.mailService = mailService;
        this.mailProperties = mailProperties;
    }

    /** 회원가입 — 이메일 중복 시 409, 성공 시 민감정보 제외 요약 반환. */
    @Transactional
    public RegisterResponse register(RegisterRequest req) {
        String email = normalize(req.email());
        Integer existing = mapper.countByEmail(email);
        if (existing != null && existing > 0) {
            throw new ApiException(ErrorCode.EMAIL_TAKEN);
        }
        String userId = "usr-" + UUID.randomUUID().toString().replace("-", "").substring(0, 20);
        String hash = passwordEncoder.encode(req.password());
        String companyName = blankToNull(req.companyName());
        mapper.insertUser(userId, email, req.displayName().trim(), hash, companyName);

        String joinedEvent = tryInviteMapping(userId, req.inviteCode());
        return new RegisterResponse(userId, email, req.displayName().trim(), joinedEvent);
    }

    /** inviteCode → 행사 event_member 매핑(best-effort). 실패해도 회원가입은 성립. */
    private String tryInviteMapping(String userId, String inviteCode) {
        String code = blankToNull(inviteCode);
        if (code == null) {
            return null;
        }
        try {
            String eventId = mapper.findEventIdByInvite(code);
            if (eventId == null) {
                return null; // 매칭되는 행사 없음 → 무시(가입은 정상).
            }
            mapper.insertEventMember("em-" + UUID.randomUUID().toString().replace("-", "").substring(0, 20),
                    eventId, userId);
            return eventId;
        } catch (RuntimeException e) {
            // 초대 매핑 실패는 가입 실패로 확산시키지 않는다(요약만 로그).
            log.warn("초대코드 매핑 건너뜀(가입은 정상): {}", e.getMessage());
            return null;
        }
    }

    /** 비밀번호 찾기 — 항상 200. 존재 시에만 재설정 코드 생성·저장(dev 로깅으로 발송 대체). */
    @Transactional
    public MessageResponse forgotPassword(ForgotPasswordRequest req) {
        String email = normalize(req.email());
        String userId = mapper.findActiveUserIdByEmail(email);
        if (userId != null) {
            String code = generateCode();
            String codeHash = passwordEncoder.encode(code);
            mapper.insertResetCode(email, codeHash,
                    OffsetDateTime.now().plusMinutes(RESET_TTL_MINUTES));
            // 코드 값은 마스킹 로그(원문·해시 노출 금지, 계약 §0-3).
            log.info("비밀번호 재설정 코드 발급(email={}, code={}, ttl={}m)",
                    maskEmail(email), maskCode(code), RESET_TTL_MINUTES);
            // 재설정 코드 이메일 발송(@Async·실패 격리 — MAIL_ENABLED=false면 스킵 로그). 프론트 흐름은 이메일+코드 입력.
            //   링크는 안내용(프론트 reset 화면은 코드 6자리 입력을 사용).
            String link = mailProperties.getWebBaseUrl() + "/login?reset=1";
            String html = com.zioinfo.kintex.mail.MailTemplates.passwordReset(code, link, RESET_TTL_MINUTES);
            mailService.send("PASSWORD_RESET", null, email, "[KINTEX] 비밀번호 재설정 인증코드", html);
        }
        // 존재 여부와 무관하게 동일 응답(사용자 열거 방지).
        return new MessageResponse(GENERIC_FORGOT_MESSAGE);
    }

    /** 비밀번호 초기화 — 코드 검증(만료·1회성·시도제한) 성공 시 해시 갱신. */
    @Transactional
    public MessageResponse resetPassword(ResetPasswordRequest req) {
        String email = normalize(req.email());
        Map<String, Object> row = mapper.findLatestActiveReset(email);
        if (row == null) {
            throw new ApiException(ErrorCode.OTP_INVALID, "재설정 코드가 올바르지 않거나 만료되었습니다.");
        }
        long id = toLong(row.get("id"));
        int attempts = toInt(row.get("attempts"));
        OffsetDateTime expiresAt = toTime(row.get("expiresAt"));

        if (attempts >= RESET_MAX_ATTEMPTS || expiresAt == null
                || expiresAt.isBefore(OffsetDateTime.now())) {
            mapper.markResetUsed(id); // 만료·시도초과 코드는 소진 처리.
            throw new ApiException(ErrorCode.OTP_INVALID, "재설정 코드가 올바르지 않거나 만료되었습니다.");
        }

        String codeHash = str(row.get("codeHash"));
        if (codeHash == null || !passwordEncoder.matches(req.code(), codeHash)) {
            mapper.incrementResetAttempts(id);
            if (attempts + 1 >= RESET_MAX_ATTEMPTS) {
                mapper.markResetUsed(id); // 시도 초과 → 코드 무효화.
            }
            throw new ApiException(ErrorCode.OTP_INVALID, "재설정 코드가 올바르지 않거나 만료되었습니다.");
        }

        // 성공: 새 해시 저장 + 실패 카운트/잠금 해제 + 코드 1회성 소진.
        mapper.updatePasswordByEmail(email, passwordEncoder.encode(req.newPassword()));
        mapper.markResetUsed(id);
        log.info("비밀번호 재설정 완료(email={})", maskEmail(email));
        return new MessageResponse("비밀번호가 변경되었습니다. 새 비밀번호로 로그인해 주세요.");
    }

    // ── helpers ─────────────────────────────────────────────────────────────
    private static String generateCode() {
        return String.format("%06d", RANDOM.nextInt(1_000_000));
    }

    private static String normalize(String email) {
        return email == null ? null : email.trim().toLowerCase();
    }

    private static String blankToNull(String s) {
        return (s == null || s.isBlank()) ? null : s.trim();
    }

    private static String maskEmail(String email) {
        if (email == null) {
            return "***";
        }
        int at = email.indexOf('@');
        if (at <= 1) {
            return "***";
        }
        return email.charAt(0) + "***" + email.substring(at);
    }

    private static String maskCode(String code) {
        if (code == null || code.length() < 2) {
            return "****";
        }
        return code.charAt(0) + "****" + code.charAt(code.length() - 1);
    }

    private static String str(Object o) {
        return o == null ? null : String.valueOf(o);
    }

    private static long toLong(Object o) {
        if (o instanceof Number n) {
            return n.longValue();
        }
        try {
            return o == null ? 0L : Long.parseLong(String.valueOf(o));
        } catch (NumberFormatException e) {
            return 0L;
        }
    }

    private static int toInt(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;
        }
    }

    private static OffsetDateTime toTime(Object o) {
        if (o instanceof OffsetDateTime odt) {
            return odt;
        }
        if (o instanceof java.time.LocalDateTime ldt) {
            return ldt.atOffset(OffsetDateTime.now().getOffset());
        }
        if (o instanceof java.sql.Timestamp ts) {
            return ts.toInstant().atOffset(OffsetDateTime.now().getOffset());
        }
        return null;
    }
}
