package com.zioinfo.kintex.ticket;

import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.common.text.Masking;
import com.zioinfo.kintex.ticket.dto.TicketDtos.IssuedTicketDto;
import com.zioinfo.kintex.ticket.dto.TicketDtos.LookupResultDto;
import com.zioinfo.kintex.ticket.dto.TicketDtos.OrderRequest;
import com.zioinfo.kintex.ticket.dto.TicketDtos.OrderResultDto;
import com.zioinfo.kintex.ticket.dto.TicketDtos.TicketProductDto;
import com.zioinfo.kintex.ticket.payment.PaymentGateway;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * 입장권 서비스 — 판매 상품 조회 · 예매(재고차감→PG승인→발권) · 조회.
 * <p>서버 권위: 수량 상한·판매기간·재고를 서버에서 재검증한다(클라이언트 신뢰 금지).
 * <p>PII 최소화(§0-3): 구매자 원문은 저장·응답 어디에도 남기지 않고 마스킹/해시만 보관한다.
 */
@Service
public class TicketService {

    private static final int ABS_MAX_QTY = 100;
    private final SecureRandom random = new SecureRandom();

    private final TicketMapper mapper;
    private final PaymentGateway paymentGateway;

    public TicketService(TicketMapper mapper, PaymentGateway paymentGateway) {
        this.mapper = mapper;
        this.paymentGateway = paymentGateway;
    }

    // ── 판매 상품 목록 ──────────────────────────────────────────────────────────
    @Transactional(readOnly = true)
    public List<TicketProductDto> listProducts(String eventId) {
        if (eventId == null || eventId.isBlank()) {
            throw new ApiException(ErrorCode.VALIDATION, "행사 식별자가 필요합니다.");
        }
        return mapper.findProducts(eventId).stream().map(TicketService::toProduct).toList();
    }

    // ── 예매(주문) ──────────────────────────────────────────────────────────────
    @Transactional
    public OrderResultDto createOrder(String eventId, OrderRequest req) {
        if (eventId == null || eventId.isBlank()) {
            throw new ApiException(ErrorCode.VALIDATION, "행사 식별자가 필요합니다.");
        }
        if (req.agreePrivacy() == null || !req.agreePrivacy()) {
            throw new ApiException(ErrorCode.VALIDATION, "개인정보 수집·이용 동의가 필요합니다.");
        }
        int qty = req.qty() == null ? 0 : req.qty();
        if (qty < 1 || qty > ABS_MAX_QTY) {
            throw new ApiException(ErrorCode.VALIDATION, "예매 수량이 올바르지 않습니다.");
        }

        Map<String, Object> product = mapper.findProduct(req.productId());
        if (product == null || !eventId.equals(str(product.get("eventId")))) {
            throw new ApiException(ErrorCode.NOT_FOUND, "선택한 입장권을 찾을 수 없습니다.");
        }
        if (!bool(product.get("onSale"))) {
            throw new ApiException(ErrorCode.CONFLICT, "판매 기간이 아닌 입장권입니다.");
        }
        int maxPerOrder = toInt(product.get("maxPerOrder"), ABS_MAX_QTY);
        if (qty > maxPerOrder) {
            throw new ApiException(ErrorCode.VALIDATION,
                    "1회 최대 " + maxPerOrder + "매까지 예매할 수 있습니다.");
        }

        // 재고 차감(조건부 UPDATE) — 실패 시 매진. 이후 어떤 예외든 트랜잭션 롤백으로 재고 원복.
        int updated = mapper.decrementStock(req.productId(), qty);
        if (updated != 1) {
            throw new ApiException(ErrorCode.CONFLICT, "잔여 수량이 부족합니다.");
        }

        long unitPrice = toLong(product.get("price"), 0);
        long total = unitPrice * qty;
        String productName = str(product.get("name"));
        String orderNo = generateOrderNo();
        String payMethod = normalizeMethod(req.payMethod(), total);

        // Mock PG 승인 — 실패 시 예외 → 트랜잭션 롤백(재고 원복).
        PaymentGateway.PaymentResult pay = paymentGateway.authorize(payMethod, total, orderNo);
        if (!pay.approved()) {
            throw new ApiException(ErrorCode.CONFLICT, "결제 승인에 실패했습니다. 다시 시도해 주세요.");
        }

        String orderId = "to-" + UUID.randomUUID();
        String nameMasked = Masking.maskName(req.buyerName());
        String contactMasked = Masking.maskPhone(req.buyerContact());
        String emailMasked = Masking.maskEmail(req.buyerEmail());
        String contactHash = sha256Hex(digitsOnly(req.buyerContact()));

        Map<String, Object> order = new java.util.HashMap<>();
        order.put("id", orderId);
        order.put("orderNo", orderNo);
        order.put("eventId", eventId);
        order.put("productId", req.productId());
        order.put("productName", productName);
        order.put("qty", qty);
        order.put("unitPrice", unitPrice);
        order.put("totalAmount", total);
        order.put("status", "PAID");
        order.put("payMethod", payMethod);
        order.put("payApprovalNo", pay.approvalNo());
        order.put("buyerNameMasked", nameMasked);
        order.put("buyerContactMasked", contactMasked);
        order.put("buyerEmailMasked", emailMasked);
        order.put("buyerContactHash", contactHash);
        mapper.insertOrder(order);

        List<IssuedTicketDto> tickets = new ArrayList<>(qty);
        for (int i = 1; i <= qty; i++) {
            String code = generateTicketCode(orderNo, i);
            Map<String, Object> issue = new java.util.HashMap<>();
            issue.put("id", "ti-" + UUID.randomUUID());
            issue.put("orderId", orderId);
            issue.put("ticketCode", code);
            issue.put("seq", i);
            mapper.insertIssue(issue);
            tickets.add(new IssuedTicketDto(code, i, false));
        }

        return new OrderResultDto(orderNo, "PAID", eventId, productName, qty, unitPrice, total,
                payMethod, pay.approvalNo(), nameMasked, contactMasked, emailMasked, null, tickets);
    }

    // ── 조회(주문번호 + 연락처) ─────────────────────────────────────────────────
    @Transactional(readOnly = true)
    public LookupResultDto lookup(String orderNo, String contact) {
        if (orderNo == null || orderNo.isBlank() || contact == null || contact.isBlank()) {
            throw new ApiException(ErrorCode.VALIDATION, "예매번호와 연락처를 입력해 주세요.");
        }
        String hash = sha256Hex(digitsOnly(contact));
        Map<String, Object> o = mapper.findOrderForLookup(orderNo.trim(), hash);
        if (o == null) {
            // 존재 여부 노출 금지 — 불일치도 NOT_FOUND 로 통일.
            throw new ApiException(ErrorCode.NOT_FOUND, "일치하는 예매 내역이 없습니다. 예매번호와 연락처를 확인해 주세요.");
        }
        List<IssuedTicketDto> tickets = mapper.findIssuesByOrder(str(o.get("id"))).stream()
                .map(m -> new IssuedTicketDto(str(m.get("ticketCode")), toInt(m.get("seq"), 0), bool(m.get("used"))))
                .toList();
        return new LookupResultDto(
                str(o.get("orderNo")), str(o.get("status")), str(o.get("eventId")), str(o.get("productName")),
                toInt(o.get("qty"), 0), toLong(o.get("unitPrice"), 0), toLong(o.get("totalAmount"), 0),
                str(o.get("payMethod")), str(o.get("buyerNameMasked")), str(o.get("buyerContactMasked")),
                str(o.get("buyerEmailMasked")), str(o.get("orderedAt")), str(o.get("paidAt")), tickets);
    }

    // ── helpers ──────────────────────────────────────────────────────────────
    private String generateOrderNo() {
        int year = java.time.Year.now().getValue();
        for (int attempt = 0; attempt < 6; attempt++) {
            String no = "KTX-" + year + "-" + String.format("%06d", random.nextInt(1_000_000));
            if (mapper.countOrderNo(no) == 0) {
                return no;
            }
        }
        // 극히 드문 충돌 — UUID 조각으로 확정 유니크.
        return "KTX-" + year + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
    }

    private String generateTicketCode(String orderNo, int seq) {
        String tail = orderNo.length() >= 6 ? orderNo.substring(orderNo.length() - 6) : orderNo;
        String rand = HexFormat.of().formatHex(randomBytes()).substring(0, 4).toUpperCase();
        return "TK-" + tail + "-" + String.format("%02d", seq) + "-" + rand;
    }

    private byte[] randomBytes() {
        byte[] b = new byte[3];
        random.nextBytes(b);
        return b;
    }

    private static String normalizeMethod(String method, long total) {
        if (total == 0) {
            return "free";
        }
        if (method == null || method.isBlank()) {
            return "card";
        }
        String m = method.trim().toLowerCase(java.util.Locale.ROOT);
        return switch (m) {
            case "card", "easy", "bank" -> m;
            default -> "card";
        };
    }

    private static String digitsOnly(String s) {
        return s == null ? "" : s.replaceAll("[^0-9]", "");
    }

    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 (NoSuchAlgorithmException e) {
            throw new ApiException(ErrorCode.INTERNAL);
        }
    }

    private static TicketProductDto toProduct(Map<String, Object> m) {
        return new TicketProductDto(
                str(m.get("id")), str(m.get("eventId")), str(m.get("code")), str(m.get("kind")),
                str(m.get("name")), str(m.get("description")), toLong(m.get("price"), 0), str(m.get("currency")),
                str(m.get("saleStart")), str(m.get("saleEnd")), toInt(m.get("totalQty"), 0),
                Math.max(0, toInt(m.get("remaining"), 0)), toInt(m.get("maxPerOrder"), 0),
                bool(m.get("onSale")), bool(m.get("soldOut")), toInt(m.get("sortOrder"), 0));
    }

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

    private static boolean bool(Object o) {
        return o instanceof Boolean b && b;
    }

    private static int toInt(Object o, int def) {
        return (o instanceof Number n) ? n.intValue() : def;
    }

    private static long toLong(Object o, long def) {
        return (o instanceof Number n) ? n.longValue() : def;
    }
}
