package com.zioinfo.kintex.ticket.smart;

import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * F-B1 회전 QR 토큰(TOTP 계열, 순수 함수 — 단위테스트 대상).
 *
 * <p>토큰 = base32(HMAC-SHA256(secret, floor(epochSeconds / STEP))[0..5]) 8자.
 * 30초 스텝마다 토큰이 회전하므로 캡처(스크린샷)한 토큰은 해당 창(+직전 1창 유예)에서만 유효하다 →
 * 양도/재사용 차단. 시크릿은 서버 전용(응답·로그 미노출)이며, 클라이언트에는 현재 토큰만 내려간다.
 */
public final class Totp {

    /** 회전 주기(초). */
    public static final int STEP_SECONDS = 30;

    private static final char[] B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray();

    private Totp() {
    }

    /** 지정 시각(epoch 초)의 회전 토큰(8자). */
    public static String code(String secret, long epochSeconds) {
        long counter = Math.floorDiv(epochSeconds, STEP_SECONDS);
        byte[] mac = hmac(secret, counter);
        StringBuilder sb = new StringBuilder(8);
        for (int i = 0; i < 5; i++) {
            sb.append(B32[(mac[i] & 0xFF) % 32]);
        }
        // 5바이트 → 8자 채움(추가 3자는 후속 바이트 저비트).
        for (int i = 0; i < 3; i++) {
            sb.append(B32[(mac[5 + i] & 0x1F)]);
        }
        return sb.toString();
    }

    /** 남은 초(현재 창 만료까지). */
    public static int remainingSeconds(long epochSeconds) {
        return STEP_SECONDS - (int) Math.floorMod(epochSeconds, STEP_SECONDS);
    }

    /**
     * 토큰 유효성 — 현재 창과 직전 {@code windowBack}개 창을 허용(시계 오차·전송 지연 유예).
     * 대소문자·공백 무시.
     */
    public static boolean verify(String secret, String token, long epochSeconds, int windowBack) {
        if (secret == null || token == null) {
            return false;
        }
        String t = token.trim().toUpperCase(java.util.Locale.ROOT);
        for (int w = 0; w <= Math.max(0, windowBack); w++) {
            if (code(secret, epochSeconds - (long) w * STEP_SECONDS).equals(t)) {
                return true;
            }
        }
        return false;
    }

    private static byte[] hmac(String secret, long counter) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            return mac.doFinal(ByteBuffer.allocate(Long.BYTES).putLong(counter).array());
        } catch (Exception e) {
            throw new IllegalStateException("HMAC 산출 실패", e);
        }
    }
}
