package kr.co.accura.wbx.spring.auth;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import kr.co.accura.wbx.spring.common.BusinessException;
import kr.co.accura.wbx.spring.config.WbxSpringProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * MFA/TOTP 서비스 — 표준 구현
 * <p>
 * wbx.spring.mfa.enabled=true 일 때 활성화.
 * TOTP 라이브러리 의존성이 없어도 동작하도록
 * HMAC-SHA1 기반 RFC 6238 직접 구현.
 * <p>
 * 고객 환경에 따라 Google Authenticator / MS Authenticator 호환.
 */
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(name = "wbx.spring.mfa.enabled", havingValue = "true")
public class MfaService {

    private final TotpSecretRepository totpRepo;
    private final WbxUserRepository userRepo;
    private final WbxSpringProperties props;
    private final PasswordEncoder passwordEncoder;
    private final ObjectMapper objectMapper;

    private static final SecureRandom RANDOM = new SecureRandom();
    private static final String AES_ALGORITHM = "AES";

    // ===== TOTP Setup =====

    /**
     * MFA 설정 시작 — QR 코드용 시크릿 + otpauth URI 반환
     */
    @Transactional
    public Map<String, String> setupMfa(Long userId) {
        WbxUser user = userRepo.findById(userId)
                .orElseThrow(() -> new BusinessException("User not found"));

        // 이미 설정된 경우 기존 삭제 후 재설정
        totpRepo.findByUserId(userId).ifPresent(existing -> totpRepo.delete(existing));

        String secret = generateSecret();
        String encrypted = encrypt(secret);

        WbxTotpSecret totp = WbxTotpSecret.builder()
                .userId(userId)
                .encryptedSecret(encrypted)
                .verified(false)
                .build();
        totpRepo.save(totp);

        String issuer = props.getMfa().getTotpIssuer();
        String otpauthUri = String.format("otpauth://totp/%s:%s?secret=%s&issuer=%s&digits=%d&period=%d",
                issuer, user.getEmail(), secret, issuer,
                props.getMfa().getTotpDigits(), props.getMfa().getTotpPeriod());

        return Map.of(
                "secret", secret,
                "otpauth_uri", otpauthUri,
                "qr_data", otpauthUri  // 프론트엔드에서 QR 렌더링
        );
    }

    /**
     * MFA 설정 확인 — 코드 검증 후 활성화 + 백업 코드 발급
     */
    @Transactional
    public Map<String, Object> verifySetup(Long userId, String code) {
        WbxTotpSecret totp = totpRepo.findByUserId(userId)
                .orElseThrow(() -> new BusinessException("MFA not initialized. Call setup first."));

        String secret = decrypt(totp.getEncryptedSecret());
        if (!verifyCode(secret, code)) {
            throw new BusinessException("Invalid TOTP code", "MFA_INVALID_CODE");
        }

        // 백업 코드 생성
        List<String> backupCodes = generateBackupCodes(8);
        List<String> hashedCodes = backupCodes.stream()
                .map(passwordEncoder::encode)
                .collect(Collectors.toList());

        try {
            totp.setBackupCodes(objectMapper.writeValueAsString(hashedCodes));
        } catch (Exception e) {
            throw new BusinessException("Failed to save backup codes");
        }
        totp.setVerified(true);
        totpRepo.save(totp);

        // 사용자 mfaEnabled 활성화
        WbxUser user = userRepo.findById(userId).orElseThrow();
        user.setMfaEnabled(true);
        userRepo.save(user);

        log.info("MFA enabled for user: {}", user.getEmail());
        return Map.of(
                "detail", "MFA enabled successfully",
                "backup_codes", backupCodes  // 한 번만 표시 — 프론트에서 다운로드 안내
        );
    }

    // ===== TOTP 검증 (로그인 2단계) =====

    /**
     * 로그인 시 TOTP 코드 검증
     */
    public boolean verifyLogin(Long userId, String code) {
        WbxTotpSecret totp = totpRepo.findByUserId(userId)
                .orElseThrow(() -> new BusinessException("MFA not configured"));

        if (!totp.isVerified()) {
            throw new BusinessException("MFA setup not completed");
        }

        String secret = decrypt(totp.getEncryptedSecret());
        return verifyCode(secret, code);
    }

    /**
     * 백업 코드로 로그인 (1회용)
     */
    @Transactional
    public boolean verifyBackupCode(Long userId, String backupCode) {
        WbxTotpSecret totp = totpRepo.findByUserId(userId)
                .orElseThrow(() -> new BusinessException("MFA not configured"));

        try {
            List<String> hashedCodes = objectMapper.readValue(
                    totp.getBackupCodes(), new TypeReference<List<String>>() {});

            for (int i = 0; i < hashedCodes.size(); i++) {
                if (passwordEncoder.matches(backupCode, hashedCodes.get(i))) {
                    // 사용된 코드 제거
                    hashedCodes.remove(i);
                    totp.setBackupCodes(objectMapper.writeValueAsString(hashedCodes));
                    totpRepo.save(totp);
                    log.info("Backup code used for user ID: {}", userId);
                    return true;
                }
            }
        } catch (Exception e) {
            log.error("Failed to verify backup code", e);
        }
        return false;
    }

    // ===== MFA 비활성화 (SA 전용) =====

    @Transactional
    public void disableMfa(Long userId) {
        totpRepo.deleteByUserId(userId);
        userRepo.findById(userId).ifPresent(user -> {
            user.setMfaEnabled(false);
            userRepo.save(user);
        });
        log.info("MFA disabled for user ID: {}", userId);
    }

    /**
     * 사용자의 MFA 필수 여부 판별
     */
    public boolean isMfaRequired(WbxUser user) {
        if (!props.getMfa().isEnabled()) return false;
        if (user.isMfaEnabled()) return true;
        // 외부 사용자 강제 MFA
        if (props.getMfa().isForceForExternal() && user.getSsoProvider() == null) return true;
        // 내부 사용자 강제 MFA
        return props.getMfa().isForceForInternal();
    }

    // ===== RFC 6238 TOTP 구현 =====

    private boolean verifyCode(String secret, String code) {
        int digits = props.getMfa().getTotpDigits();
        int period = props.getMfa().getTotpPeriod();
        long timeStep = System.currentTimeMillis() / 1000 / period;

        // 현재 ± 1 윈도우 허용 (시간 오차 대응)
        for (int i = -1; i <= 1; i++) {
            String generated = generateTotpCode(secret, timeStep + i, digits);
            if (generated.equals(code)) return true;
        }
        return false;
    }

    private String generateTotpCode(String base32Secret, long timeStep, int digits) {
        try {
            byte[] key = base32Decode(base32Secret);
            byte[] data = new byte[8];
            for (int i = 7; i >= 0; i--) {
                data[i] = (byte) (timeStep & 0xFF);
                timeStep >>= 8;
            }

            javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA1");
            mac.init(new javax.crypto.spec.SecretKeySpec(key, "HmacSHA1"));
            byte[] hash = mac.doFinal(data);

            int offset = hash[hash.length - 1] & 0x0F;
            int binary = ((hash[offset] & 0x7F) << 24)
                    | ((hash[offset + 1] & 0xFF) << 16)
                    | ((hash[offset + 2] & 0xFF) << 8)
                    | (hash[offset + 3] & 0xFF);

            int otp = binary % (int) Math.pow(10, digits);
            return String.format("%0" + digits + "d", otp);
        } catch (Exception e) {
            throw new BusinessException("TOTP generation failed");
        }
    }

    // ===== Helper Methods =====

    private String generateSecret() {
        byte[] bytes = new byte[20]; // 160 bits
        RANDOM.nextBytes(bytes);
        return base32Encode(bytes);
    }

    private List<String> generateBackupCodes(int count) {
        return IntStream.range(0, count)
                .mapToObj(i -> {
                    int code = 10000000 + RANDOM.nextInt(90000000); // 8자리 숫자
                    return String.valueOf(code);
                })
                .collect(Collectors.toList());
    }

    /**
     * AES-256 암호화 (JWT secret 앞 32바이트를 키로 사용)
     */
    private String encrypt(String plainText) {
        try {
            byte[] key = getAesKey();
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, AES_ALGORITHM));
            return Base64.getEncoder().encodeToString(cipher.doFinal(plainText.getBytes()));
        } catch (Exception e) {
            throw new BusinessException("Encryption failed");
        }
    }

    private String decrypt(String cipherText) {
        try {
            byte[] key = getAesKey();
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, AES_ALGORITHM));
            return new String(cipher.doFinal(Base64.getDecoder().decode(cipherText)));
        } catch (Exception e) {
            throw new BusinessException("Decryption failed");
        }
    }

    private byte[] getAesKey() {
        String secret = props.getJwt().getSecret();
        byte[] keyBytes = secret.getBytes();
        return Arrays.copyOf(keyBytes, 32); // AES-256 = 32 bytes
    }

    // Base32 encode/decode (RFC 4648)
    private static final String BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

    private String base32Encode(byte[] data) {
        StringBuilder result = new StringBuilder();
        int buffer = 0, bitsLeft = 0;
        for (byte b : data) {
            buffer = (buffer << 8) | (b & 0xFF);
            bitsLeft += 8;
            while (bitsLeft >= 5) {
                result.append(BASE32_CHARS.charAt((buffer >> (bitsLeft - 5)) & 0x1F));
                bitsLeft -= 5;
            }
        }
        if (bitsLeft > 0) {
            result.append(BASE32_CHARS.charAt((buffer << (5 - bitsLeft)) & 0x1F));
        }
        return result.toString();
    }

    private byte[] base32Decode(String base32) {
        String upper = base32.toUpperCase().replaceAll("[^A-Z2-7]", "");
        byte[] result = new byte[upper.length() * 5 / 8];
        int buffer = 0, bitsLeft = 0, index = 0;
        for (char c : upper.toCharArray()) {
            buffer = (buffer << 5) | BASE32_CHARS.indexOf(c);
            bitsLeft += 5;
            if (bitsLeft >= 8) {
                result[index++] = (byte) (buffer >> (bitsLeft - 8));
                bitsLeft -= 8;
            }
        }
        return Arrays.copyOf(result, index);
    }
}
