package com.zioinfo.kintex.parking;

import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.parking.dto.ParkingDtos.ParkingLotStatusDto;
import com.zioinfo.kintex.parking.dto.ParkingDtos.ParkingPassDto;
import com.zioinfo.kintex.parking.dto.ParkingDtos.PurchasePassRequest;
import com.zioinfo.kintex.ticket.payment.PaymentGateway;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.security.SecureRandom;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * 주차 서비스 기본 구현 — 결정론적 시뮬레이션 어댑터(F-C2).
 *
 * <p>실시간 점유는 외부 시스템(iparking 등) 미연동 → 시각·주차장별 결정론 곡선 + 발급된 사전 주차권 수로
 * 산출한다(하드웨어/외부 호출 없음). 구매는 mock 결제({@link PaymentGateway})로 승인한다.
 *
 * <p>TODO(게이트): 외부 주차관제(iparking) 실연동은 게이트 대상 — 승인 후 별도 어댑터
 * ({@code IparkingParkingService implements ParkingService})로 이 빈을 대체한다.
 * 외부 API 키/엔드포인트는 env 주입, 실패 시 본 시뮬레이션으로 폴백한다. 지금은 외부 호출 금지.
 */
@Service
public class SimulatedParkingService implements ParkingService {

    /** 혼잡 임계(점유율 %) — 여유<60 ≤ 보통<90 ≤ 혼잡. */
    private static final int PCT_BUSY = 90;
    private static final int PCT_NORMAL = 60;

    private static final DateTimeFormatter TS = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
    private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    private final SecureRandom random = new SecureRandom();
    private final ParkingMapper mapper;
    private final PaymentGateway paymentGateway;

    public SimulatedParkingService(ParkingMapper mapper, PaymentGateway paymentGateway) {
        this.mapper = mapper;
        this.paymentGateway = paymentGateway;
    }

    // ── 주차 현황(공개) ─────────────────────────────────────────────────────────
    @Override
    @Transactional(readOnly = true)
    public List<ParkingLotStatusDto> getLotStatus(String eventId) {
        String today = LocalDate.now().format(DATE);
        int hour = LocalDateTime.now().getHour();
        String now = LocalDateTime.now().format(TS);

        List<ParkingLotStatusDto> out = new ArrayList<>();
        for (Map<String, Object> r : mapper.findActiveLots()) {
            String lotId = str(r.get("lotId"));
            String code = str(r.get("code"));
            int capacity = toInt(r.get("totalCapacity"), 0);

            // 결정론 점유 = 시각 곡선 + 주차장 시드 편차, 사전 주차권 수를 가산(cap).
            long reserved = mapper.countActivePasses(lotId, today);
            int occupied = simulateOccupancy(code, capacity, hour, reserved);
            int available = Math.max(0, capacity - occupied);
            int pct = capacity > 0 ? (int) Math.round(occupied * 100.0 / capacity) : 0;

            out.add(new ParkingLotStatusDto(
                    lotId, code, str(r.get("name")), toIntOrNull(r.get("exhibitionCenter")),
                    capacity, occupied, available, pct,
                    levelCode(pct), levelLabel(pct),
                    toLong(r.get("hourlyRate"), 0), toLongOrNull(r.get("dailyMax")),
                    toLong(r.get("passPrice"), 0), str(r.get("note")), now));
        }
        return out;
    }

    /**
     * 결정론 점유 시뮬레이션 — 시각 벨커브(정오 피크) + 주차장 코드 해시 편차. 사전 예약분 가산 후 cap.
     * 외부 실측이 아닌 표시용 근사(정직). 같은 입력에 같은 결과.
     */
    private static int simulateOccupancy(String code, int capacity, int hour, long reserved) {
        if (capacity <= 0) {
            return 0;
        }
        // 시각 곡선: 10~16시 피크(~0.85), 심야 저점(~0.05).
        double peakDist = Math.abs(hour - 13);       // 13시 피크 기준 거리
        double base = Math.max(0.05, 0.85 - peakDist * 0.07);
        // 주차장별 편차(-0.08 ~ +0.07) — 코드 해시 결정론.
        int seed = Math.floorMod(code.hashCode(), 15);
        double ratio = base + (seed - 7) / 100.0;
        ratio = Math.max(0.02, Math.min(0.98, ratio));
        int simulated = (int) Math.round(capacity * ratio);
        return Math.min(capacity, simulated + (int) Math.min(reserved, capacity));
    }

    // ── 사전 주차권 구매(인증) ───────────────────────────────────────────────────
    @Override
    @Transactional
    public ParkingPassDto purchasePass(String userId, PurchasePassRequest req) {
        if (userId == null || userId.isBlank()) {
            throw new ApiException(ErrorCode.UNAUTHORIZED, "로그인이 필요합니다.");
        }
        LocalDate useDate = parseUseDate(req.useDate());
        if (useDate.isBefore(LocalDate.now())) {
            throw new ApiException(ErrorCode.VALIDATION, "이용일은 오늘 이후로 선택해 주세요.");
        }

        Map<String, Object> lot = mapper.findLot(req.lotId());
        if (lot == null || !"active".equals(str(lot.get("status")))) {
            throw new ApiException(ErrorCode.NOT_FOUND, "선택한 주차장을 찾을 수 없습니다.");
        }

        long amount = toLong(lot.get("passPrice"), 0);
        String lotName = str(lot.get("name"));
        String passNo = generatePassNo();
        String payMethod = normalizeMethod(req.payMethod(), amount);

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

        String plateMasked = maskPlate(req.vehiclePlate());
        String id = "pp-" + UUID.randomUUID();
        String useDateStr = useDate.format(DATE);

        Map<String, Object> pass = new HashMap<>();
        pass.put("id", id);
        pass.put("passNo", passNo);
        pass.put("eventId", nullIfBlank(req.eventId()));
        pass.put("lotId", req.lotId());
        pass.put("lotName", lotName);
        pass.put("userId", userId);
        pass.put("useDate", useDateStr);
        pass.put("vehiclePlateMasked", plateMasked);
        pass.put("amount", amount);
        pass.put("status", "PAID");
        pass.put("payMethod", payMethod);
        pass.put("payApprovalNo", pay.approvalNo());
        mapper.insertPass(pass);

        return new ParkingPassDto(passNo, "PAID", req.lotId(), lotName, nullIfBlank(req.eventId()),
                useDateStr, plateMasked, amount, payMethod, pay.approvalNo(),
                LocalDateTime.now().format(TS));
    }

    // ── 내 주차권 조회(인증) ─────────────────────────────────────────────────────
    @Override
    @Transactional(readOnly = true)
    public List<ParkingPassDto> myPasses(String userId) {
        if (userId == null || userId.isBlank()) {
            throw new ApiException(ErrorCode.UNAUTHORIZED, "로그인이 필요합니다.");
        }
        return mapper.findPassesByUser(userId).stream().map(SimulatedParkingService::toPass).toList();
    }

    // ── helpers ──────────────────────────────────────────────────────────────
    private static String levelCode(int pct) {
        if (pct >= PCT_BUSY) {
            return "BUSY";
        }
        if (pct >= PCT_NORMAL) {
            return "NORMAL";
        }
        return "FREE";
    }

    private static String levelLabel(int pct) {
        if (pct >= PCT_BUSY) {
            return "혼잡";
        }
        if (pct >= PCT_NORMAL) {
            return "보통";
        }
        return "여유";
    }

    private String generatePassNo() {
        int year = java.time.Year.now().getValue();
        for (int attempt = 0; attempt < 6; attempt++) {
            String no = "PK-" + year + "-" + String.format("%06d", random.nextInt(1_000_000));
            if (mapper.countPassNo(no) == 0) {
                return no;
            }
        }
        return "PK-" + year + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
    }

    private static LocalDate parseUseDate(String s) {
        if (s == null || s.isBlank()) {
            throw new ApiException(ErrorCode.VALIDATION, "이용일을 선택해 주세요.");
        }
        try {
            return LocalDate.parse(s.trim());
        } catch (RuntimeException e) {
            throw new ApiException(ErrorCode.VALIDATION, "이용일 형식이 올바르지 않습니다(YYYY-MM-DD).");
        }
    }

    private static String normalizeMethod(String method, long amount) {
        if (amount == 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";
        };
    }

    /** 차량번호 마스킹 — 앞2·뒤2만 노출(원문 미저장). "123가4567" → "12****67". null 안전. */
    private static String maskPlate(String plate) {
        if (plate == null || plate.isBlank()) {
            return null;
        }
        String p = plate.replaceAll("\\s", "");
        int len = p.length();
        if (len <= 4) {
            return "*".repeat(Math.max(1, len));
        }
        return p.substring(0, 2) + "*".repeat(len - 4) + p.substring(len - 2);
    }

    private static ParkingPassDto toPass(Map<String, Object> m) {
        return new ParkingPassDto(
                str(m.get("passNo")), str(m.get("status")), str(m.get("lotId")), str(m.get("lotName")),
                str(m.get("eventId")), str(m.get("useDate")), str(m.get("vehiclePlateMasked")),
                toLong(m.get("amount"), 0), str(m.get("payMethod")), str(m.get("payApprovalNo")),
                str(m.get("issuedAt")));
    }

    private static String nullIfBlank(String s) {
        return (s == null || s.isBlank()) ? null : s.trim();
    }

    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 Integer toIntOrNull(Object o) {
        return (o instanceof Number n) ? n.intValue() : null;
    }

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

    private static Long toLongOrNull(Object o) {
        return (o instanceof Number n) ? n.longValue() : null;
    }
}
