package com.zioinfo.kintex.ticket.cancel;

import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.ticket.cancel.TicketCancelDtos.BracketDto;
import com.zioinfo.kintex.ticket.cancel.TicketCancelDtos.RefundQuoteDto;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * F-B2 티켓 취소·환불 서비스 — 다구간 취소수수료 + 예매수수료 별도 규정, 서버 권위 산출.
 *
 * <p>예상 환불({@link #quote})은 조회만(멱등), 실제 취소({@link #cancel})는 상태 전이·재고 원복·스냅샷 저장.
 * 금액 산식은 {@link TicketCancelCalc}(순수 함수) 위임 — 클라이언트 금액을 신뢰하지 않는다.
 */
@Service
public class TicketCancelService {

    private static final String DEFAULT_POLICY = "__default__";

    private final TicketCancelMapper mapper;

    public TicketCancelService(TicketCancelMapper mapper) {
        this.mapper = mapper;
    }

    /** 예상 환불 조회(취소 전) — 본인확인(예매번호+연락처). */
    @Transactional(readOnly = true)
    public RefundQuoteDto quote(String orderNo, String contact) {
        return build(loadOrder(orderNo, contact), false);
    }

    /** 실제 취소 처리 — 상태 CANCELLED 전이·재고 원복·환불 스냅샷 저장. */
    @Transactional
    public RefundQuoteDto cancel(String orderNo, String contact) {
        Map<String, Object> order = loadOrder(orderNo, contact);
        String orderId = str(order.get("id"));
        if (!"PAID".equals(str(order.get("status")))) {
            throw new ApiException(ErrorCode.CONFLICT, "취소할 수 없는 예매입니다(이미 취소되었거나 사용됨).");
        }
        if (mapper.countCancellation(orderId) > 0) {
            throw new ApiException(ErrorCode.CONFLICT, "이미 취소 처리된 예매입니다.");
        }
        RefundQuoteDto q = build(order, true);

        int updated = mapper.cancelOrder(orderId);
        if (updated != 1) {
            throw new ApiException(ErrorCode.CONFLICT, "취소 처리에 실패했습니다. 다시 시도해 주세요.");
        }
        mapper.restock(str(order.get("productId")), toInt(order.get("qty"), 0));

        Map<String, Object> row = new java.util.HashMap<>();
        row.put("id", "tc-" + UUID.randomUUID());
        row.put("orderId", orderId);
        row.put("orderNo", q.orderNo());
        row.put("daysBefore", q.daysBefore());
        row.put("ticketAmount", q.ticketAmount());
        row.put("bookingFee", q.bookingFee());
        row.put("refundRate", q.refundRatePercent());
        row.put("adminFee", q.adminFee());
        row.put("refundAmount", q.refundAmount());
        row.put("forfeitAmount", q.forfeitAmount());
        row.put("policyVersion", q.policyVersion());
        mapper.insertCancellation(row);

        // 상태를 CANCELLED 로 갱신해 응답.
        return new RefundQuoteDto(q.orderNo(), "CANCELLED", q.productName(), q.qty(),
                q.ticketAmount(), q.bookingFee(), q.daysBefore(), q.refundRatePercent(), q.bracketLabel(),
                q.ticketRefund(), q.adminFee(), q.bookingFeeRefund(), q.refundAmount(), q.forfeitAmount(),
                q.policyVersion(), false, q.brackets());
    }

    // ── 내부 산정 ──
    private RefundQuoteDto build(Map<String, Object> order, boolean applying) {
        String eventId = str(order.get("eventId"));
        int qty = toInt(order.get("qty"), 0);
        long ticketAmount = toLong(order.get("totalAmount"), 0);

        // 정책 로드(행사 우선 → 기본).
        Map<String, Object> policy = mapper.findFeePolicy(eventId);
        if (policy == null) {
            policy = mapper.findFeePolicy(DEFAULT_POLICY);
        }
        long bookingFeePerTicket = policy == null ? 0 : toLong(policy.get("bookingFeePerTicket"), 0);
        int bookingFeeRefundMinDays = policy == null ? 7 : toInt(policy.get("bookingFeeRefundMinDays"), 7);
        double adminPct = policy == null ? 0 : toDouble(policy.get("cancelAdminFeePercent"), 0);
        String version = policy == null ? "cancel-v1.0" : str(policy.getOrDefault("version", "cancel-v1.0"));
        long bookingFee = bookingFeePerTicket * qty;

        // 구간 로드(행사 특화 → 기본).
        List<Map<String, Object>> raw = mapper.findEventBrackets(eventId);
        if (raw == null || raw.isEmpty()) {
            raw = mapper.findDefaultBrackets();
        }
        List<TicketCancelCalc.Bracket> brackets = new ArrayList<>();
        List<BracketDto> bracketDtos = new ArrayList<>();
        for (Map<String, Object> b : raw) {
            int min = toInt(b.get("minDaysBefore"), 0);
            double rate = toDouble(b.get("refundRatePercent"), 0);
            String label = str(b.get("label"));
            brackets.add(new TicketCancelCalc.Bracket(min, rate, label));
            bracketDtos.add(new BracketDto(min, rate, label));
        }

        int daysBefore = daysBefore(eventId);
        TicketCancelCalc.Result r = TicketCancelCalc.compute(
                daysBefore, ticketAmount, bookingFee, brackets, bookingFeeRefundMinDays, adminPct);

        boolean cancellable = "PAID".equals(str(order.get("status")));
        return new RefundQuoteDto(
                str(order.get("orderNo")), str(order.get("status")), str(order.get("productName")), qty,
                r.ticketAmount(), r.bookingFee(), r.daysBefore(), r.refundRatePercent(), r.bracketLabel(),
                r.ticketRefund(), r.adminFee(), r.bookingFeeRefund(), r.refundAmount(), r.forfeitAmount(),
                version, cancellable, bracketDtos);
    }

    private Map<String, Object> loadOrder(String orderNo, String contact) {
        if (blank(orderNo) || blank(contact)) {
            throw new ApiException(ErrorCode.VALIDATION, "예매번호와 연락처를 입력해 주세요.");
        }
        Map<String, Object> order = mapper.findOrderForCancel(orderNo.trim(), contactHash(contact));
        if (order == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "일치하는 예매 내역이 없습니다.");
        }
        return order;
    }

    /** 개막까지 남은 일수 — 미확정이면 0(환불 불가 안전값). */
    private int daysBefore(String eventId) {
        String start = mapper.findEventStartDate(eventId);
        if (start == null || start.isBlank()) {
            return 0;
        }
        try {
            LocalDate s = LocalDate.parse(start, DateTimeFormatter.ISO_LOCAL_DATE);
            return (int) (s.toEpochDay() - LocalDate.now().toEpochDay());
        } catch (Exception e) {
            return 0;
        }
    }

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

    private static String contactHash(String contact) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            return HexFormat.of().formatHex(md.digest(contact.replaceAll("[^0-9]", "").getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            throw new ApiException(ErrorCode.INTERNAL);
        }
    }

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

    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;
    }

    private static double toDouble(Object o, double def) {
        return o instanceof Number n ? n.doubleValue() : def;
    }
}
