package gc.mda.signal_batch.domain.gis.service;

import gc.mda.signal_batch.domain.gis.dto.AreaSearchRequest;
import gc.mda.signal_batch.domain.gis.dto.AreaSearchRequest.SearchMode;
import gc.mda.signal_batch.domain.gis.dto.AreaSearchRequest.SearchPolygon;
import gc.mda.signal_batch.domain.gis.dto.AreaSearchResponse;
import gc.mda.signal_batch.domain.gis.dto.AreaSearchResponse.AreaSearchSummary;
import gc.mda.signal_batch.domain.gis.dto.AreaSearchResponse.PolygonHitDetail;
import gc.mda.signal_batch.domain.vessel.dto.CompactVesselTrack;
import gc.mda.signal_batch.global.websocket.service.DailyTrackCacheManager;
import gc.mda.signal_batch.global.websocket.service.DailyTrackCacheManager.DailyTrackData;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.geom.prep.PreparedGeometry;
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
import org.locationtech.jts.index.strtree.STRtree;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class AreaSearchService {

    private final DailyTrackCacheManager cacheManager;
    private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();

    /**
     * 다중 폴리곤 영역 탐색 메인 엔트리
     */
    public AreaSearchResponse search(AreaSearchRequest request) {
        long startMs = System.currentTimeMillis();

        // 1. 입력 검증
        validateRequest(request);

        // 2. 캐시 데이터 수집 (날짜 범위)
        List<LocalDate> targetDates = collectTargetDates(request.getStartTime(), request.getEndTime());
        if (targetDates.isEmpty()) {
            return buildEmptyResponse(request, startMs);
        }

        // 3. 다일 데이터 → 선박별 단일 트랙 병합
        Map<String, CompactVesselTrack> mergedTracks = mergeMultipleDays(targetDates);
        if (mergedTracks.isEmpty()) {
            return buildEmptyResponse(request, startMs);
        }

        // 4. 좌표 → JTS Polygon 변환
        List<Polygon> jtsPolygons = convertToJtsPolygons(request.getPolygons());

        // 5. 병합된 트랙으로 STRtree 빌드
        STRtree spatialIndex = buildSpatialIndex(mergedTracks);

        // 6. 각 폴리곤별 히트 선박 + 개별 방문(trip) 수집
        List<Map<String, List<PolygonHitDetail>>> perPolygonHits = new ArrayList<>();
        for (int i = 0; i < jtsPolygons.size(); i++) {
            Polygon polygon = jtsPolygons.get(i);
            SearchPolygon searchPolygon = request.getPolygons().get(i);
            Map<String, List<PolygonHitDetail>> hits = findHitsForPolygon(
                    polygon, searchPolygon, mergedTracks, spatialIndex);
            perPolygonHits.add(hits);
        }

        // 7. 모드별 결과 합산
        SearchMode mode = request.getPolygons().size() == 1 ? SearchMode.ANY : request.getMode();
        Map<String, List<PolygonHitDetail>> resultHits;
        switch (mode) {
            case ALL:
                resultHits = processAllMode(perPolygonHits);
                break;
            case SEQUENTIAL:
                resultHits = processSequentialMode(perPolygonHits);
                break;
            default:
                resultHits = processAnyMode(perPolygonHits);
                break;
        }

        // 8. 결과 선박의 전체 기간 트랙 + 히트 메타 반환
        List<CompactVesselTrack> resultTracks = resultHits.keySet().stream()
                .map(mergedTracks::get)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        long totalPoints = resultHits.values().stream()
                .flatMap(Collection::stream)
                .mapToLong(h -> h.getHitPointCount() != null ? h.getHitPointCount() : 0)
                .sum();

        int totalCachedVessels = targetDates.stream()
                .mapToInt(d -> {
                    DailyTrackData data = cacheManager.getDailyTrackData(d);
                    return data != null ? data.getVesselCount() : 0;
                })
                .sum();

        long elapsedMs = System.currentTimeMillis() - startMs;
        log.info("Area search completed: mode={}, polygons={}, hitVessels={}, totalPoints={}, elapsed={}ms",
                mode, request.getPolygons().size(), resultHits.size(), totalPoints, elapsedMs);

        return AreaSearchResponse.builder()
                .tracks(resultTracks)
                .hitDetails(resultHits)
                .summary(AreaSearchSummary.builder()
                        .totalVessels(resultHits.size())
                        .totalPoints(totalPoints)
                        .mode(mode)
                        .polygonIds(request.getPolygons().stream()
                                .map(SearchPolygon::getId)
                                .collect(Collectors.toList()))
                        .processingTimeMs(elapsedMs)
                        .cachedDates(targetDates.stream()
                                .map(LocalDate::toString)
                                .collect(Collectors.toList()))
                        .totalCachedVessels(totalCachedVessels)
                        .build())
                .build();
    }

    // ── 입력 검증 ──

    private void validateRequest(AreaSearchRequest request) {
        if (request.getStartTime().isAfter(request.getEndTime())) {
            throw new IllegalArgumentException("시작 시간이 종료 시간보다 나중입니다");
        }

        DailyTrackCacheManager.CacheStatus cacheStatus = cacheManager.getStatus();
        if (cacheStatus == DailyTrackCacheManager.CacheStatus.LOADING
                || cacheStatus == DailyTrackCacheManager.CacheStatus.NOT_STARTED) {
            throw new CacheNotReadyException("캐시가 아직 준비되지 않았습니다 (상태: " + cacheStatus + ")");
        }

        for (SearchPolygon polygon : request.getPolygons()) {
            validatePolygon(polygon);
        }
    }

    private void validatePolygon(SearchPolygon polygon) {
        validatePolygon(polygon.getId(), polygon.getCoordinates());
    }

    /**
     * 폴리곤 유효성 검증 (package-private, VesselContactService에서 재사용).
     */
    void validatePolygon(String polygonId, List<double[]> coords) {
        if (coords == null || coords.size() < 4) {
            throw new IllegalArgumentException(
                    "폴리곤 '" + polygonId + "'은 최소 4개 좌표가 필요합니다 (첫점==끝점)");
        }

        double[] first = coords.get(0);
        double[] last = coords.get(coords.size() - 1);
        if (first[0] != last[0] || first[1] != last[1]) {
            throw new IllegalArgumentException(
                    "폴리곤 '" + polygonId + "'의 첫점과 끝점이 동일해야 합니다");
        }

        // JTS로 유효성 검사
        try {
            Polygon jtsPolygon = toJtsPolygon(coords);
            if (!jtsPolygon.isValid()) {
                throw new IllegalArgumentException(
                        "폴리곤 '" + polygonId + "'이 유효하지 않습니다 (자기 교차 등)");
            }
        } catch (IllegalArgumentException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    "폴리곤 '" + polygonId + "' 변환 실패: " + e.getMessage());
        }
    }

    // ── 날짜 수집 ──

    List<LocalDate> collectTargetDates(LocalDateTime startTime, LocalDateTime endTime) {
        LocalDate today = LocalDate.now();
        LocalDate startDate = startTime.toLocalDate();
        LocalDate endDate = endTime.toLocalDate();
        List<LocalDate> dates = new ArrayList<>();

        for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
            if (d.isAfter(today) || d.equals(today)) {
                // 오늘/미래 데이터는 캐시에 없음 (경고 로그)
                if (d.equals(today)) {
                    log.warn("Area search: 오늘({}) 데이터는 캐시에 없어 제외됩니다", today);
                }
                continue;
            }
            if (cacheManager.isCached(d)) {
                dates.add(d);
            } else {
                log.debug("Area search: 날짜 {}가 캐시에 없어 제외됩니다", d);
            }
        }

        return dates;
    }

    // ── JTS 변환 ──

    List<Polygon> convertToJtsPolygons(List<SearchPolygon> searchPolygons) {
        return searchPolygons.stream()
                .map(sp -> toJtsPolygon(sp.getCoordinates()))
                .collect(Collectors.toList());
    }

    Polygon toJtsPolygon(List<double[]> coordinates) {
        Coordinate[] coords = new Coordinate[coordinates.size()];
        for (int i = 0; i < coordinates.size(); i++) {
            double[] c = coordinates.get(i);
            coords[i] = new Coordinate(c[0], c[1]);
        }
        return GEOMETRY_FACTORY.createPolygon(coords);
    }

    // ── 다일 데이터 병합 ──

    Map<String, CompactVesselTrack> mergeMultipleDays(List<LocalDate> dates) {
        Map<String, List<CompactVesselTrack>> byVessel = new HashMap<>();

        for (LocalDate date : dates) {
            DailyTrackData data = cacheManager.getDailyTrackData(date);
            if (data == null) continue;

            for (Map.Entry<String, CompactVesselTrack> entry : data.getTracks().entrySet()) {
                byVessel.computeIfAbsent(entry.getKey(), k -> new ArrayList<>())
                        .add(entry.getValue());
            }
        }

        Map<String, CompactVesselTrack> merged = new HashMap<>(byVessel.size());
        for (Map.Entry<String, List<CompactVesselTrack>> entry : byVessel.entrySet()) {
            List<CompactVesselTrack> trackList = entry.getValue();
            if (trackList.size() == 1) {
                merged.put(entry.getKey(), trackList.get(0));
                continue;
            }

            // 여러 날짜 병합
            CompactVesselTrack first = trackList.get(0);
            List<double[]> geo = new ArrayList<>();
            List<String> ts = new ArrayList<>();
            List<Double> sp = new ArrayList<>();
            double totalDist = 0;
            double maxSpeed = 0;
            int pointCount = 0;

            for (CompactVesselTrack t : trackList) {
                if (t.getGeometry() != null) geo.addAll(t.getGeometry());
                if (t.getTimestamps() != null) ts.addAll(t.getTimestamps());
                if (t.getSpeeds() != null) sp.addAll(t.getSpeeds());
                if (t.getTotalDistance() != null) totalDist += t.getTotalDistance();
                if (t.getMaxSpeed() != null) maxSpeed = Math.max(maxSpeed, t.getMaxSpeed());
                if (t.getPointCount() != null) pointCount += t.getPointCount();
            }

            merged.put(entry.getKey(), CompactVesselTrack.builder()
                    .vesselId(first.getVesselId())
                    .nationalCode(first.getNationalCode())
                    .shipName(first.getShipName())
                    .shipType(first.getShipType())
                    .shipKindCode(first.getShipKindCode())
                    .geometry(geo)
                    .timestamps(ts)
                    .speeds(sp)
                    .totalDistance(totalDist)
                    .avgSpeed(pointCount > 0 ? totalDist / Math.max(1, pointCount) * 60 : 0)
                    .maxSpeed(maxSpeed)
                    .pointCount(pointCount)
                    .build());
        }

        return merged;
    }

    // ── STRtree 빌드 ──

    STRtree buildSpatialIndex(Map<String, CompactVesselTrack> tracks) {
        STRtree tree = new STRtree();
        for (Map.Entry<String, CompactVesselTrack> entry : tracks.entrySet()) {
            CompactVesselTrack track = entry.getValue();
            if (track.getGeometry() == null || track.getGeometry().isEmpty()) continue;

            double minLon = Double.MAX_VALUE, maxLon = -Double.MAX_VALUE;
            double minLat = Double.MAX_VALUE, maxLat = -Double.MAX_VALUE;
            for (double[] coord : track.getGeometry()) {
                if (coord[0] < minLon) minLon = coord[0];
                if (coord[0] > maxLon) maxLon = coord[0];
                if (coord[1] < minLat) minLat = coord[1];
                if (coord[1] > maxLat) maxLat = coord[1];
            }
            tree.insert(new Envelope(minLon, maxLon, minLat, maxLat), entry.getKey());
        }
        tree.build();
        return tree;
    }

    // ── 폴리곤별 히트 검색 (다중 방문 분리) ──

    @SuppressWarnings("unchecked")
    Map<String, List<PolygonHitDetail>> findHitsForPolygon(
            Polygon polygon, SearchPolygon searchPolygon,
            Map<String, CompactVesselTrack> tracks, STRtree spatialIndex) {

        PreparedGeometry prepared = PreparedGeometryFactory.prepare(polygon);
        Envelope mbr = polygon.getEnvelopeInternal();

        // STRtree 후보 추출
        List<String> candidates = spatialIndex.query(mbr);

        Map<String, List<PolygonHitDetail>> hits = new HashMap<>();
        for (String vesselId : candidates) {
            CompactVesselTrack track = tracks.get(vesselId);
            if (track == null) continue;

            List<PolygonHitDetail> visits = checkTrackAgainstPolygonMultiVisit(
                    track, prepared, polygon, searchPolygon);
            if (!visits.isEmpty()) {
                hits.put(vesselId, visits);
            }
        }

        return hits;
    }

    /**
     * 정밀 point-in-polygon 검사: 트랙을 순회하며 연속 체류 구간(방문)별로 분리.
     * 진입/진출 시각은 경계 교차점 거리 비율 보간 적용.
     */
    private List<PolygonHitDetail> checkTrackAgainstPolygonMultiVisit(
            CompactVesselTrack track, PreparedGeometry prepared,
            Polygon polygon, SearchPolygon searchPolygon) {

        List<double[]> geometry = track.getGeometry();
        List<String> timestamps = track.getTimestamps();
        if (geometry == null || geometry.isEmpty()) return Collections.emptyList();

        List<PolygonHitDetail> visits = new ArrayList<>();
        boolean wasInside = false;
        long currentEntry = 0;
        long currentExit = 0;
        int currentHitCount = 0;
        int visitIndex = 0;

        for (int i = 0; i < geometry.size(); i++) {
            double[] coord = geometry.get(i);
            Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(coord[0], coord[1]));
            boolean isInside = prepared.contains(point);

            if (isInside) {
                long ts = parseTimestamp(timestamps, i);
                if (!wasInside) {
                    // 새 방문 시작 — 보간된 진입 시각
                    if (i > 0) {
                        currentEntry = interpolateBoundaryCrossing(
                                geometry.get(i - 1), parseTimestamp(timestamps, i - 1),
                                coord, ts, polygon);
                    } else {
                        currentEntry = ts;
                    }
                    currentHitCount = 0;
                }
                currentExit = ts;
                currentHitCount++;
                wasInside = true;
            } else {
                if (wasInside) {
                    // 방문 종료 — 보간된 진출 시각
                    long interpolatedExit = interpolateBoundaryCrossing(
                            geometry.get(i - 1), parseTimestamp(timestamps, i - 1),
                            coord, parseTimestamp(timestamps, i), polygon);
                    visitIndex++;
                    visits.add(PolygonHitDetail.builder()
                            .polygonId(searchPolygon.getId())
                            .polygonName(searchPolygon.getName())
                            .entryTimestamp(currentEntry)
                            .exitTimestamp(interpolatedExit)
                            .hitPointCount(currentHitCount)
                            .visitIndex(visitIndex)
                            .build());
                }
                wasInside = false;
            }
        }
        // 마지막 포인트가 inside인 경우 마무리 (이후 outside 없어 보간 불가)
        if (wasInside && currentHitCount > 0) {
            visitIndex++;
            visits.add(PolygonHitDetail.builder()
                    .polygonId(searchPolygon.getId())
                    .polygonName(searchPolygon.getName())
                    .entryTimestamp(currentEntry)
                    .exitTimestamp(currentExit)
                    .hitPointCount(currentHitCount)
                    .visitIndex(visitIndex)
                    .build());
        }

        return visits;
    }

    // ── 경계 통과 시각 보간 ──

    /**
     * 폴리곤 경계 통과 시각을 거리 비율로 보간 계산.
     * outside↔inside 전환 시 두 인접 포인트 사이의 경계 교차점을 구하고,
     * 거리 비율에 따라 타임스탬프를 보간한다.
     * 교차점 미발견 시 fallback으로 p2의 타임스탬프 반환.
     */
    private long interpolateBoundaryCrossing(
            double[] p1, long ts1, double[] p2, long ts2, Polygon polygon) {
        Coordinate c1 = new Coordinate(p1[0], p1[1]);
        Coordinate c2 = new Coordinate(p2[0], p2[1]);
        LineSegment segment = new LineSegment(c1, c2);

        LinearRing ring = polygon.getExteriorRing();
        for (int j = 0; j < ring.getNumPoints() - 1; j++) {
            LineSegment edge = new LineSegment(
                    ring.getCoordinateN(j), ring.getCoordinateN(j + 1));
            Coordinate intersection = segment.intersection(edge);
            if (intersection != null) {
                double dTotal = c1.distance(c2);
                if (dTotal < 1e-12) return ts1;
                double ratio = c1.distance(intersection) / dTotal;
                return ts1 + Math.round((ts2 - ts1) * ratio);
            }
        }
        return ts2;  // fallback
    }

    private long parseTimestamp(List<String> timestamps, int index) {
        if (timestamps == null || index >= timestamps.size()) return 0L;
        try {
            return Long.parseLong(timestamps.get(index));
        } catch (NumberFormatException e) {
            return 0L;
        }
    }

    // ── 모드별 결과 처리 (다중 방문 지원) ──

    /**
     * ANY 모드: 합집합 (어느 영역이든 통과한 선박).
     * 모든 방문을 entryTimestamp 기준 오름차순 정렬하여 반환.
     */
    Map<String, List<PolygonHitDetail>> processAnyMode(
            List<Map<String, List<PolygonHitDetail>>> perPolygonHits) {

        Map<String, List<PolygonHitDetail>> result = new HashMap<>();
        for (Map<String, List<PolygonHitDetail>> polygonHits : perPolygonHits) {
            for (Map.Entry<String, List<PolygonHitDetail>> entry : polygonHits.entrySet()) {
                result.computeIfAbsent(entry.getKey(), k -> new ArrayList<>())
                        .addAll(entry.getValue());
            }
        }
        result.values().forEach(list ->
                list.sort(Comparator.comparingLong(PolygonHitDetail::getEntryTimestamp)));
        return result;
    }

    /**
     * ALL 모드: 교집합 (모든 영역을 통과한 선박).
     * 모든 방문을 entryTimestamp 기준 오름차순 정렬하여 반환.
     */
    Map<String, List<PolygonHitDetail>> processAllMode(
            List<Map<String, List<PolygonHitDetail>>> perPolygonHits) {

        if (perPolygonHits.isEmpty()) return Collections.emptyMap();

        // 모든 폴리곤에 공통으로 존재하는 vesselId 찾기
        Set<String> commonVessels = new HashSet<>(perPolygonHits.get(0).keySet());
        for (int i = 1; i < perPolygonHits.size(); i++) {
            commonVessels.retainAll(perPolygonHits.get(i).keySet());
        }

        Map<String, List<PolygonHitDetail>> result = new HashMap<>();
        for (String vesselId : commonVessels) {
            List<PolygonHitDetail> allVisits = new ArrayList<>();
            for (Map<String, List<PolygonHitDetail>> polygonHits : perPolygonHits) {
                allVisits.addAll(polygonHits.get(vesselId));
            }
            allVisits.sort(Comparator.comparingLong(PolygonHitDetail::getEntryTimestamp));
            result.put(vesselId, allVisits);
        }
        return result;
    }

    /**
     * SEQUENTIAL 모드: 교집합 + 방문 순서 체인 검증.
     * 각 폴리곤의 방문 중 순서가 성립하는 조합(chain)을 찾고,
     * 체인에 포함된 방문만 반환 (visitIndex=1로 재설정).
     */
    Map<String, List<PolygonHitDetail>> processSequentialMode(
            List<Map<String, List<PolygonHitDetail>>> perPolygonHits) {

        if (perPolygonHits.isEmpty()) return Collections.emptyMap();

        // 모든 폴리곤에 공통으로 존재하는 vesselId 찾기
        Set<String> commonVessels = new HashSet<>(perPolygonHits.get(0).keySet());
        for (int i = 1; i < perPolygonHits.size(); i++) {
            commonVessels.retainAll(perPolygonHits.get(i).keySet());
        }

        Map<String, List<PolygonHitDetail>> result = new HashMap<>();
        for (String vesselId : commonVessels) {
            List<List<PolygonHitDetail>> visitsByPolygon = new ArrayList<>();
            boolean valid = true;
            for (Map<String, List<PolygonHitDetail>> polygonHits : perPolygonHits) {
                List<PolygonHitDetail> visits = polygonHits.get(vesselId);
                if (visits == null || visits.isEmpty()) {
                    valid = false;
                    break;
                }
                visitsByPolygon.add(visits);
            }
            if (!valid) continue;

            List<PolygonHitDetail> chain = findSequentialChain(visitsByPolygon);
            if (chain != null) {
                result.put(vesselId, chain);
            }
        }
        return result;
    }

    /**
     * 폴리곤 순서대로 유효한 순차 체인을 greedy 탐색.
     * 각 단계에서 이전 방문의 exitTimestamp보다 늦은 entryTimestamp를 가진 첫 방문 선택.
     * 체인 성립 시 visitIndex=1로 재설정하여 반환.
     */
    private List<PolygonHitDetail> findSequentialChain(
            List<List<PolygonHitDetail>> visitsByPolygon) {
        List<PolygonHitDetail> chain = new ArrayList<>();
        Long prevExit = null;
        for (List<PolygonHitDetail> visits : visitsByPolygon) {
            PolygonHitDetail matched = null;
            for (PolygonHitDetail visit : visits) {
                if (prevExit == null || visit.getEntryTimestamp() > prevExit) {
                    matched = visit;
                    break;
                }
            }
            if (matched == null) return null;
            chain.add(matched.toBuilder().visitIndex(1).build());
            prevExit = matched.getExitTimestamp();
        }
        return chain;
    }

    // ── 빈 응답 ──

    private AreaSearchResponse buildEmptyResponse(AreaSearchRequest request, long startMs) {
        long elapsedMs = System.currentTimeMillis() - startMs;
        SearchMode mode = request.getPolygons().size() == 1 ? SearchMode.ANY : request.getMode();

        return AreaSearchResponse.builder()
                .tracks(Collections.emptyList())
                .hitDetails(Collections.emptyMap())
                .summary(AreaSearchSummary.builder()
                        .totalVessels(0)
                        .totalPoints(0L)
                        .mode(mode)
                        .polygonIds(request.getPolygons().stream()
                                .map(SearchPolygon::getId)
                                .collect(Collectors.toList()))
                        .processingTimeMs(elapsedMs)
                        .cachedDates(Collections.emptyList())
                        .totalCachedVessels(0)
                        .build())
                .build();
    }

    // ── 예외 클래스 ──

    public static class CacheNotReadyException extends RuntimeException {
        public CacheNotReadyException(String message) {
            super(message);
        }
    }
}
