package com.zioinfo.kintex.ticket.smart;

import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.ticket.smart.SmartTicketDtos.ActivateResultDto;
import com.zioinfo.kintex.ticket.smart.SmartTicketDtos.RotatingTokenDto;
import com.zioinfo.kintex.ticket.smart.SmartTicketDtos.VerifyResultDto;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.HexFormat;
import java.util.Map;

/**
 * F-B1 스마트티켓 서비스 — 디바이스 바인딩 + 30초 회전 QR 토큰 발급/검증.
 *
 * <p>흐름: ①activate(예매번호+연락처 본인확인 → 티켓을 기기에 바인딩, 서버 시크릿 발급)
 * → ②token(바인딩 기기에서만 현재 회전 토큰 발급) → ③verify(게이트 스캐너가 QR 검증).
 * 캡처 스크린샷은 토큰 회전으로 30초(+직전 1창) 후 무효 → 양도/재사용 차단.
 *
 * <p>보안(§0-3): deviceId 원문 미보관(sha256), secret 은 응답·로그에 절대 노출하지 않는다.
 */
@Service
public class SmartTicketService {

    /** 검증 유예 창 수(현재+직전 N) — 전송/시계 오차 흡수. */
    private static final int VERIFY_WINDOW_BACK = 1;

    private final SmartTicketMapper mapper;
    private final SecureRandom random = new SecureRandom();

    public SmartTicketService(SmartTicketMapper mapper) {
        this.mapper = mapper;
    }

    /** 스마트티켓 활성화 — 본인확인(예매번호+연락처) 후 발권 티켓을 기기에 바인딩. */
    @Transactional
    public ActivateResultDto activate(String orderNo, String contact, String ticketCode, String deviceId) {
        if (blank(orderNo) || blank(contact) || blank(ticketCode) || blank(deviceId)) {
            throw new ApiException(ErrorCode.VALIDATION, "활성화에 필요한 정보가 부족합니다.");
        }
        Map<String, Object> order = mapper.findOrderIdForLookup(orderNo.trim(), contactHash(contact));
        if (order == null) {
            // 존재 여부 노출 금지 — 불일치도 NOT_FOUND 통일.
            throw new ApiException(ErrorCode.NOT_FOUND, "예매 정보를 확인할 수 없습니다. 예매번호와 연락처를 확인해 주세요.");
        }
        String orderId = String.valueOf(order.get("orderId"));
        if (mapper.ticketBelongsToOrder(orderId, ticketCode.trim()) != 1) {
            throw new ApiException(ErrorCode.NOT_FOUND, "해당 예매의 티켓이 아닙니다.");
        }
        String secret = newSecret();
        mapper.upsertBinding(ticketCode.trim(), deviceHash(deviceId), secret);
        return new ActivateResultDto(ticketCode.trim(), true, Totp.STEP_SECONDS);
    }

    /** 회전 토큰 발급 — 바인딩된 기기에서만. QR 페이로드 = ticketCode|token. */
    @Transactional(readOnly = true)
    public RotatingTokenDto currentToken(String ticketCode, String deviceId) {
        if (blank(ticketCode) || blank(deviceId)) {
            throw new ApiException(ErrorCode.VALIDATION, "티켓/기기 정보가 필요합니다.");
        }
        Map<String, Object> b = mapper.findBinding(ticketCode.trim());
        if (b == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "활성화되지 않은 티켓입니다. 스마트티켓을 먼저 활성화해 주세요.");
        }
        if (!deviceHash(deviceId).equals(String.valueOf(b.get("deviceHash")))) {
            // 다른 기기 — 양도/캡처 방지. 존재 노출 없이 권한 거절.
            throw new ApiException(ErrorCode.FORBIDDEN, "이 기기에서 발급할 수 없는 티켓입니다.");
        }
        long now = nowSeconds();
        String token = Totp.code(String.valueOf(b.get("secret")), now);
        return new RotatingTokenDto(
                ticketCode.trim(), token, ticketCode.trim() + "|" + token,
                Totp.STEP_SECONDS, Totp.remainingSeconds(now));
    }

    /** 게이트 검증 — QR 페이로드(ticketCode|token)의 회전 토큰 유효성. */
    @Transactional
    public VerifyResultDto verify(String qrPayload) {
        if (blank(qrPayload) || !qrPayload.contains("|")) {
            return new VerifyResultDto(false, null, "형식이 올바르지 않은 코드입니다.");
        }
        int sep = qrPayload.lastIndexOf('|');
        String ticketCode = qrPayload.substring(0, sep).trim();
        String token = qrPayload.substring(sep + 1).trim();
        Map<String, Object> b = mapper.findBinding(ticketCode);
        if (b == null) {
            return new VerifyResultDto(false, null, "미등록 또는 만료된 티켓입니다.");
        }
        boolean ok = Totp.verify(String.valueOf(b.get("secret")), token, nowSeconds(), VERIFY_WINDOW_BACK);
        if (!ok) {
            return new VerifyResultDto(false, ticketCode, "만료된 코드입니다. 앱에서 최신 QR을 다시 제시해 주세요.");
        }
        mapper.markUsed(ticketCode);
        return new VerifyResultDto(true, ticketCode, "정상 입장");
    }

    // ── helpers ──
    protected long nowSeconds() {
        return System.currentTimeMillis() / 1000L;
    }

    private String newSecret() {
        byte[] b = new byte[24];
        random.nextBytes(b);
        return HexFormat.of().formatHex(b);
    }

    private static String deviceHash(String deviceId) {
        return sha256Hex(deviceId.trim());
    }

    private static String contactHash(String contact) {
        return sha256Hex(contact.replaceAll("[^0-9]", ""));
    }

    private static boolean blank(String s) {
        return s == null || s.isBlank();
    }

    private static String sha256Hex(String s) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            return HexFormat.of().formatHex(md.digest(s.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            throw new ApiException(ErrorCode.INTERNAL);
        }
    }
}
