package com.zioinfo.kintex.logistics;

import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.logistics.dto.DockBoardDto;
import com.zioinfo.kintex.logistics.dto.DockDto;
import com.zioinfo.kintex.logistics.dto.DockForecastDto;
import com.zioinfo.kintex.logistics.dto.DockGridReservationDto;
import com.zioinfo.kintex.logistics.dto.DockReservationRequest;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * M8 반입/반출 도크 서비스 — 도크 현황판·슬롯 예약(중복 409)·대기열 예측(단순 시간대 집계).
 * 그리드는 08:00~19:00(12개 시간대). AI 예측은 실 예약 집계, 데이터 부재 시 표준 패턴 폴백.
 */
@Service
public class LogisticsService {

    private static final int GRID_START_HOUR = 8;
    private static final int HOURS = 12;                 // 08:00 ~ 19:00
    private static final int GRID_END_HOUR = GRID_START_HOUR + HOURS; // 20 (exclusive)
    private static final int DEFAULT_SPAN = 2;

    /** 예약 데이터 부재 시 표준 대기열 패턴(오후 4시경 피크) — 폴백 명시. */
    private static final List<Integer> FALLBACK_FORECAST =
            List.of(20, 30, 40, 55, 70, 95, 100, 80, 50, 30, 22, 18);

    private final LogisticsMapper mapper;

    public LogisticsService(LogisticsMapper mapper) {
        this.mapper = mapper;
    }

    /** 도크 현황판 — direction(in|out)/date. date 미지정 시 최근 예약 일자(없으면 오늘)로 해소. */
    public DockBoardDto getBoard(String eventId, String direction, String date) {
        String dir = normalizeDirection(direction);
        String resolvedDate = resolveDate(eventId, dir, date);

        List<Map<String, Object>> dockRows = mapper.findDocks(eventId);
        List<DockDto> docks = new ArrayList<>(dockRows.size());
        Map<String, Integer> indexByDockId = new LinkedHashMap<>();
        int i = 0;
        for (Map<String, Object> r : dockRows) {
            String id = str(r.get("id"));
            indexByDockId.put(id, i++);
            docks.add(new DockDto(id, intVal(r.get("dockNo")), str(r.get("label")), boolVal(r.get("heavyPriority"))));
        }

        List<DockGridReservationDto> grid = new ArrayList<>();
        for (Map<String, Object> r : mapper.findReservations(eventId, dir, resolvedDate)) {
            Integer idx = indexByDockId.get(str(r.get("dockId")));
            if (idx == null) {
                continue; // 다른 홀 도크(방어)
            }
            int startHour = intVal(r.get("startHour"));
            int start = startHour - GRID_START_HOUR;
            if (start < 0 || start >= HOURS) {
                continue;
            }
            int span = Math.max(1, Math.min(intVal(r.get("span")), HOURS - start));
            grid.add(new DockGridReservationDto(
                    str(r.get("id")), idx, start, span,
                    blockLabel(r), str(r.get("tone"))));
        }
        return new DockBoardDto(dir, resolvedDate, GRID_START_HOUR, HOURS, docks, grid);
    }

    /** 슬롯 예약 생성 — 중복 슬롯이면 409 CONFLICT. */
    public DockGridReservationDto createReservation(String eventId, DockReservationRequest req) {
        String dir = normalizeDirection(req.direction());
        String date = requireDate(req.date());
        List<Map<String, Object>> dockRows = mapper.findDocks(eventId);
        if (dockRows.isEmpty()) {
            throw new ApiException(ErrorCode.NOT_FOUND, "예약 가능한 도크가 없습니다.");
        }
        int dockIndex = req.dockIndex() == null ? -1 : req.dockIndex();
        if (dockIndex < 0 || dockIndex >= dockRows.size()) {
            throw new ApiException(ErrorCode.VALIDATION, "도크 선택이 올바르지 않습니다.");
        }
        String dockId = str(dockRows.get(dockIndex).get("id"));

        int startHour = req.startHour() == null ? -1 : req.startHour();
        int span = req.span() == null || req.span() < 1 ? DEFAULT_SPAN : req.span();
        if (startHour < GRID_START_HOUR || startHour >= GRID_END_HOUR) {
            throw new ApiException(ErrorCode.VALIDATION, "예약 시간대가 유효 범위를 벗어났습니다.");
        }
        int endHour = startHour + span;
        if (endHour > GRID_END_HOUR) {
            throw new ApiException(ErrorCode.VALIDATION, "예약 시간대가 운영 종료 시각을 초과합니다.");
        }
        if (mapper.countOverlaps(eventId, dockId, dir, date, startHour, endHour) > 0) {
            throw new ApiException(ErrorCode.CONFLICT, "이미 예약된 슬롯과 겹칩니다. 다른 시간대를 선택하세요.");
        }

        String id = "dr-" + UUID.randomUUID();
        Map<String, Object> p = new LinkedHashMap<>();
        p.put("id", id);
        p.put("eventId", eventId);
        p.put("dockId", dockId);
        p.put("direction", dir);
        p.put("date", date);
        p.put("startHour", startHour);
        p.put("span", span);
        p.put("companyName", req.companyName());
        p.put("vehicleNo", req.vehicleNo());
        p.put("vehicleWeight", req.weight());
        p.put("item", req.item());
        p.put("forklift", req.forklift() != null && req.forklift());
        p.put("tone", "booked");
        mapper.insertReservation(p);

        Map<String, Object> saved = mapper.findReservationById(id);
        int start = intVal(saved.get("startHour")) - GRID_START_HOUR;
        return new DockGridReservationDto(id, dockIndex, start,
                intVal(saved.get("span")), blockLabel(saved), str(saved.get("tone")));
    }

    /** 철거일 대기열 예측 — 일자별 도크 점유를 시간대 밀집도로 집계. 예약 없으면 폴백. */
    public DockForecastDto getForecast(String eventId, String date) {
        String resolvedDate = requireDate(date);
        List<Map<String, Object>> occ = mapper.findOccupancyByDate(eventId, resolvedDate);
        int dockCount = Math.max(1, mapper.findDocks(eventId).size());

        if (occ.isEmpty()) {
            return new DockForecastDto(FALLBACK_FORECAST, true, "16:00");
        }
        int[] count = new int[HOURS];
        for (Map<String, Object> r : occ) {
            int start = intVal(r.get("startHour")) - GRID_START_HOUR;
            int span = Math.max(1, intVal(r.get("span")));
            for (int h = start; h < start + span && h < HOURS; h++) {
                if (h >= 0) {
                    count[h]++;
                }
            }
        }
        List<Integer> hourly = new ArrayList<>(HOURS);
        int peakIdx = 0;
        for (int h = 0; h < HOURS; h++) {
            hourly.add((int) Math.round(count[h] * 100.0 / dockCount));
            if (count[h] > count[peakIdx]) {
                peakIdx = h;
            }
        }
        String peak = String.format("%02d:00", GRID_START_HOUR + peakIdx);
        return new DockForecastDto(hourly, false, peak);
    }

    // ── 헬퍼 ─────────────────────────────────────────────────────────
    private String resolveDate(String eventId, String dir, String date) {
        String norm = nullIfBlank(date);
        if (norm != null) {
            return requireDate(norm);
        }
        String def = mapper.findDefaultDate(eventId, dir);
        return def != null ? def : LocalDate.now().toString();
    }

    private static String requireDate(String date) {
        String norm = nullIfBlank(date);
        if (norm == null) {
            return LocalDate.now().toString();
        }
        try {
            return LocalDate.parse(norm).toString();
        } catch (DateTimeParseException e) {
            throw new ApiException(ErrorCode.VALIDATION, "date 형식은 YYYY-MM-DD 이어야 합니다.");
        }
    }

    private static String normalizeDirection(String direction) {
        String d = direction == null ? "in" : direction.trim().toLowerCase();
        if (!"in".equals(d) && !"out".equals(d)) {
            throw new ApiException(ErrorCode.VALIDATION, "direction 은 in 또는 out 이어야 합니다.");
        }
        return d;
    }

    /** 그리드 블록 라벨 — 우선 슬롯은 안내문, 그 외 업체·중량 조합. */
    private static String blockLabel(Map<String, Object> r) {
        if ("priority".equals(str(r.get("tone")))) {
            return "중량물 우선 (5t 이상)";
        }
        String company = str(r.get("companyName"));
        String weight = str(r.get("vehicleWeight"));
        if (company == null && weight == null) {
            return "예약됨";
        }
        if (company == null) {
            return weight;
        }
        return weight == null ? company : company + " · " + weight;
    }

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

    private static int intVal(Object o) {
        if (o instanceof Number n) return n.intValue();
        if (o == null) return 0;
        try {
            return Integer.parseInt(String.valueOf(o));
        } catch (NumberFormatException e) {
            return 0;
        }
    }

    private static boolean boolVal(Object o) {
        return o instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(o));
    }

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