package com.zioinfo.kintex.hallassign;

import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.hallassign.dto.AssignmentRequest;
import com.zioinfo.kintex.hallassign.dto.ConflictDto;
import com.zioinfo.kintex.hallassign.dto.HallAssignmentDto;
import com.zioinfo.kintex.hallassign.dto.HallDto;
import com.zioinfo.kintex.hallassign.dto.QuoteLine;
import com.zioinfo.kintex.hallassign.dto.QuoteRequest;
import com.zioinfo.kintex.hallassign.dto.QuoteResult;
import com.zioinfo.kintex.rules.RateRuleSet;
import com.zioinfo.kintex.rules.RuleSetLoader;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * M1 홀 배정·자동 견적 서비스.
 * <ul>
 *   <li>배정 CRUD + 홀·기간 겹침 충돌 검사(같은 홀·기간 겹침 → 충돌 행사 반환).</li>
 *   <li>자동 견적: 요율 룰셋(rates-v1.0) 기반 라인 계산 — 서버 계산 권위.</li>
 * </ul>
 */
@Service
public class HallAssignService {

    private final HallAssignMapper mapper;
    private final RuleSetLoader ruleSets;

    public HallAssignService(HallAssignMapper mapper, RuleSetLoader ruleSets) {
        this.mapper = mapper;
        this.ruleSets = ruleSets;
    }

    // ── 홀 마스터 ────────────────────────────────────────────────
    public List<HallDto> halls() {
        List<HallDto> out = new ArrayList<>();
        for (Map<String, Object> r : mapper.findHalls()) {
            out.add(toHall(r));
        }
        return out;
    }

    // ── 배정 조회 ────────────────────────────────────────────────
    public List<HallAssignmentDto> assignments(String from, String to, String eventId) {
        DateRange range = defaultRange(from, to);
        List<HallAssignmentDto> out = new ArrayList<>();
        for (Map<String, Object> r : mapper.findAssignments(range.from, range.to, blankToNull(eventId))) {
            out.add(toAssignment(r));
        }
        return out;
    }

    /** 홀·기간 충돌 사전검사(200) — 보드 표시·배정 전 검증. */
    public List<ConflictDto> conflicts(String hallId, String from, String to, String excludeEventId) {
        requireDate(from, "from");
        requireDate(to, "to");
        List<ConflictDto> out = new ArrayList<>();
        for (Map<String, Object> r : mapper.findConflicts(hallId, from, to, blankToNull(excludeEventId), null)) {
            out.add(toConflict(r));
        }
        return out;
    }

    // ── 배정 생성/해제 ──────────────────────────────────────────
    /** 겹침 없으면 배정 생성 후 dto 반환. */
    @Transactional
    public HallAssignmentDto assign(AssignmentRequest req) {
        Map<String, Object> hall = mapper.findHallById(req.hallId());
        if (hall == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "홀을 찾을 수 없습니다.");
        }
        Map<String, Object> event = mapper.findEvent(req.eventId());
        if (event == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "행사를 찾을 수 없습니다.");
        }

        // 점유기간 결정: 요청 값 우선, 없으면 행사 개최기간으로 보완.
        String start = firstNonBlank(req.occupyStart(), str(event.get("startDate")));
        String end = firstNonBlank(req.occupyEnd(), str(event.get("endDate")));
        if (start == null || end == null) {
            throw new ApiException(ErrorCode.VALIDATION, "점유기간(occupyStart~occupyEnd)이 필요합니다.");
        }
        requireOrder(start, end);

        List<ConflictDto> conflicts = conflicts(req.hallId(), start, end, req.eventId());
        if (!conflicts.isEmpty()) {
            throw new HallAssignConflictException(conflicts);
        }

        Map<String, Object> p = new HashMap<>();
        String id = "ha-" + UUID.randomUUID().toString().substring(0, 12);
        p.put("id", id);
        p.put("eventId", req.eventId());
        p.put("hallId", req.hallId());
        p.put("primary", req.primary());
        p.put("occupyStart", start);
        p.put("occupyEnd", end);
        p.put("note", req.note());
        mapper.insertAssignment(p);
        return toAssignment(mapper.findAssignmentById(id));
    }

    /** 배정 해제. 없으면 404. */
    @Transactional
    public void release(String assignmentId) {
        Map<String, Object> existing = mapper.findAssignmentById(assignmentId);
        if (existing == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "배정을 찾을 수 없습니다.");
        }
        mapper.deleteAssignment(assignmentId);
    }

    // ── 자동 견적 ────────────────────────────────────────────────
    /** 요율 룰셋(rates-v1.0) 기반 견적. 라인 내역·임대료 합계·관리비 예치금 범위 포함. */
    public QuoteResult quote(QuoteRequest req) {
        RateRuleSet rates = ruleSets.rates();
        if (rates == null || rates.rental() == null) {
            throw new ApiException(ErrorCode.INTERNAL, "요율 룰셋이 로드되지 않았습니다.");
        }
        Map<String, Object> rental = rates.rental();

        LocalDate start = parseDate(req.startDate(), "startDate");
        LocalDate end = parseDate(req.endDate(), "endDate");
        if (end.isBefore(start)) {
            throw new ApiException(ErrorCode.VALIDATION, "종료일이 시작일보다 빠릅니다.");
        }
        int days = (int) ChronoUnit.DAYS.between(start, end) + 1; // 개최일 포함(폐구간).

        double exhibitionPerM2 = dbl(rental, "exhibitionHallPerM2", 2250);
        double lobbyPerM2 = dbl(rental, "lobbyPerM2", 10000);
        double outdoorPerM2 = dbl(rental, "outdoorPerM2", 2000);
        double eventHallPerM2 = dbl(rental, "eventHallPerM2", 2420);
        double peakRatio = dbl(rental, "peakSurchargeRatio", 0.10);
        double offPeakRatio = dbl(rental, "offPeakDiscountRatio", -0.10);
        double hall1Ratio = dbl(rental, "hall1SurchargeRatio", 0.10);
        double depositMinRatio = dbl(rental, "depositRatioMin", 0.15);
        double depositMaxRatio = dbl(rental, "depositRatioMax", 0.20);
        List<Integer> peakMonths = intList(rental.get("peakMonths"));
        List<Integer> offPeakMonths = intList(rental.get("offPeakMonths"));

        List<QuoteLine> lines = new ArrayList<>();
        BigDecimal rentalSubtotal = BigDecimal.ZERO;
        BigDecimal hall1Surcharge = BigDecimal.ZERO;
        int totalCapacity = 0;

        for (String hallId : req.hallIds()) {
            Map<String, Object> hall = mapper.findHallById(hallId);
            if (hall == null) {
                throw new ApiException(ErrorCode.NOT_FOUND, "홀을 찾을 수 없습니다: " + hallId);
            }
            BigDecimal area = num(hall.get("areaM2"));
            String floorType = str(hall.get("floorType"));
            String label = str(hall.get("label"));
            Integer capacity = hall.get("boothCapacity") == null ? null
                    : ((Number) hall.get("boothCapacity")).intValue();
            if (capacity != null) {
                totalCapacity += capacity;
            }

            double perM2 = ratePerM2(floorType, exhibitionPerM2, lobbyPerM2, outdoorPerM2, eventHallPerM2);
            BigDecimal base = area.multiply(BigDecimal.valueOf(perM2)).multiply(BigDecimal.valueOf(days));
            rentalSubtotal = rentalSubtotal.add(base);
            lines.add(new QuoteLine(
                    "RENTAL",
                    label + " 임대료",
                    String.format("면적 %s㎡ × %,.0f원/㎡ × %d일", strip(area), perM2, days),
                    round(base)));

            // 제1전시장 홀1(H1) 프리미엄 할증(룰셋 hall1SurchargeRatio) — 해당 홀에만.
            if ("H1".equalsIgnoreCase(hallId) && hall1Ratio != 0) {
                hall1Surcharge = hall1Surcharge.add(base.multiply(BigDecimal.valueOf(hall1Ratio)));
            }
        }

        // 성수기 할증 / 비수기 할인(개최 시작월 기준, 단일 적용).
        BigDecimal seasonAdj = BigDecimal.ZERO;
        int month = start.getMonthValue();
        if (peakMonths.contains(month) && peakRatio != 0) {
            seasonAdj = rentalSubtotal.multiply(BigDecimal.valueOf(peakRatio));
            lines.add(new QuoteLine("PEAK_SURCHARGE", "성수기 할증",
                    String.format("임대료 소계 × %+.0f%%", peakRatio * 100), round(seasonAdj)));
        } else if (offPeakMonths.contains(month) && offPeakRatio != 0) {
            seasonAdj = rentalSubtotal.multiply(BigDecimal.valueOf(offPeakRatio));
            lines.add(new QuoteLine("OFFPEAK_DISCOUNT", "비수기 할인",
                    String.format("임대료 소계 × %+.0f%%", offPeakRatio * 100), round(seasonAdj)));
        }

        if (hall1Surcharge.signum() != 0) {
            lines.add(new QuoteLine("HALL1_SURCHARGE", "제1전시장 홀1 할증",
                    String.format("홀1 임대료 × %+.0f%%", hall1Ratio * 100), round(hall1Surcharge)));
        }

        BigDecimal total = rentalSubtotal.add(seasonAdj).add(hall1Surcharge);
        long depositMin = round(total.multiply(BigDecimal.valueOf(depositMinRatio)));
        long depositMax = round(total.multiply(BigDecimal.valueOf(depositMaxRatio)));
        lines.add(new QuoteLine("MGMT_DEPOSIT", "관리비 예치금(임대료의 15~20%)",
                String.format("임대료 합계의 %.0f~%.0f%% (별도 예치)", depositMinRatio * 100, depositMaxRatio * 100),
                depositMin));

        String capacityNote = null;
        if (req.boothCount() != null && totalCapacity > 0 && req.boothCount() > totalCapacity) {
            capacityNote = String.format("요청 부스 %d개가 홀 표준 수용 %d개를 초과합니다(배치 재검토 필요).",
                    req.boothCount(), totalCapacity);
        }

        return new QuoteResult(
                rates.rulesetVersion(),
                rates.currency() == null ? "KRW" : rates.currency(),
                days,
                lines,
                round(rentalSubtotal),
                round(total),
                depositMin,
                depositMax,
                capacityNote,
                rates.disclaimer());
    }

    // ── 매핑 헬퍼 ────────────────────────────────────────────────
    private HallDto toHall(Map<String, Object> r) {
        return new HallDto(
                str(r.get("id")),
                ((Number) r.get("exhibitionCenter")).intValue(),
                str(r.get("label")),
                num(r.get("areaM2")),
                str(r.get("floorType")),
                r.get("boothCapacity") == null ? null : ((Number) r.get("boothCapacity")).intValue());
    }

    private HallAssignmentDto toAssignment(Map<String, Object> r) {
        return new HallAssignmentDto(
                str(r.get("id")),
                str(r.get("eventId")),
                str(r.get("eventName")),
                str(r.get("hallId")),
                str(r.get("hallLabel")),
                Boolean.TRUE.equals(r.get("primary")),
                str(r.get("occupyStart")),
                str(r.get("occupyEnd")),
                str(r.get("status")),
                str(r.get("note")));
    }

    private ConflictDto toConflict(Map<String, Object> r) {
        return new ConflictDto(
                str(r.get("assignmentId")),
                str(r.get("eventId")),
                str(r.get("eventName")),
                str(r.get("hallId")),
                str(r.get("hallLabel")),
                str(r.get("occupyStart")),
                str(r.get("occupyEnd")));
    }

    private double ratePerM2(String floorType, double exhibition, double lobby, double outdoor, double eventHall) {
        if (floorType == null) {
            return exhibition;
        }
        return switch (floorType) {
            case "outdoor" -> outdoor;
            case "carpet" -> eventHall; // 홀6(이벤트홀) 카펫.
            default -> exhibition;
        };
    }

    // ── 값 변환/검증 유틸 ───────────────────────────────────────
    private static String str(Object o) {
        return o == null ? null : o.toString();
    }

    private static BigDecimal num(Object o) {
        if (o == null) {
            return BigDecimal.ZERO;
        }
        if (o instanceof BigDecimal b) {
            return b;
        }
        return new BigDecimal(o.toString());
    }

    private static long round(BigDecimal v) {
        return v.setScale(0, RoundingMode.HALF_UP).longValueExact();
    }

    private static String strip(BigDecimal v) {
        return v.stripTrailingZeros().toPlainString();
    }

    private static double dbl(Map<String, Object> m, String key, double dflt) {
        Object o = m.get(key);
        return o instanceof Number n ? n.doubleValue() : dflt;
    }

    @SuppressWarnings("unchecked")
    private static List<Integer> intList(Object o) {
        List<Integer> out = new ArrayList<>();
        if (o instanceof List<?> l) {
            for (Object e : l) {
                if (e instanceof Number n) {
                    out.add(n.intValue());
                }
            }
        }
        return out;
    }

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

    private static String firstNonBlank(String a, String b) {
        if (a != null && !a.isBlank()) {
            return a;
        }
        return (b == null || b.isBlank()) ? null : b;
    }

    private static LocalDate parseDate(String s, String field) {
        try {
            return LocalDate.parse(s);
        } catch (DateTimeParseException e) {
            throw new ApiException(ErrorCode.VALIDATION, field + " 날짜 형식이 올바르지 않습니다(YYYY-MM-DD).");
        }
    }

    private static void requireDate(String s, String field) {
        if (s == null || s.isBlank()) {
            throw new ApiException(ErrorCode.VALIDATION, field + "(YYYY-MM-DD)이 필요합니다.");
        }
        parseDate(s, field);
    }

    private static void requireOrder(String start, String end) {
        LocalDate s = parseDate(start, "occupyStart");
        LocalDate e = parseDate(end, "occupyEnd");
        if (e.isBefore(s)) {
            throw new ApiException(ErrorCode.VALIDATION, "점유 종료일이 시작일보다 빠릅니다.");
        }
    }

    private record DateRange(String from, String to) {
    }

    private DateRange defaultRange(String from, String to) {
        LocalDate f = (from == null || from.isBlank()) ? LocalDate.now().withDayOfYear(1) : parseDate(from, "from");
        LocalDate t = (to == null || to.isBlank()) ? f.plusYears(1) : parseDate(to, "to");
        return new DateRange(f.toString(), t.toString());
    }
}
