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

import gc.mda.signal_batch.domain.gis.dto.VesselContactRequest;
import gc.mda.signal_batch.domain.gis.dto.VesselContactResponse;
import gc.mda.signal_batch.domain.gis.dto.VesselContactResponse.*;
import gc.mda.signal_batch.domain.vessel.dto.CompactVesselTrack;
import gc.mda.signal_batch.global.websocket.service.DailyTrackCacheManager;
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.*;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class VesselContactService {

    private final AreaSearchService areaSearchService;
    private final DailyTrackCacheManager cacheManager;

    private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
    private static final double EARTH_RADIUS_M = 6_371_000.0;
    private static final double EARTH_RADIUS_NM = 3_440.065;

    /** 시간 동기화 허용 오차 (10분, 데이터 해상도 ~5분에 맞춤) */
    private static final long SYNC_TOLERANCE_SEC = 600;
    /** 연속 세그먼트 분리 기준 갭 (20분) */
    private static final long MAX_GAP_SEC = 1200;

    private static final ZoneId KST = ZoneId.of("Asia/Seoul");

    public VesselContactResponse search(VesselContactRequest request) {
        long startMs = System.currentTimeMillis();

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

        // 2. 캐시 데이터 수집 + 다일 병합
        List<LocalDate> targetDates = areaSearchService.collectTargetDates(
                request.getStartTime(), request.getEndTime());
        if (targetDates.isEmpty()) {
            return buildEmptyResponse(request, targetDates, startMs);
        }

        Map<String, CompactVesselTrack> mergedTracks = areaSearchService.mergeMultipleDays(targetDates);
        if (mergedTracks.isEmpty()) {
            return buildEmptyResponse(request, targetDates, startMs);
        }

        // 3. 병합된 트랙을 직접 사용 (단일 수집원이므로 필터 불필요)
        Map<String, CompactVesselTrack> filtered = mergedTracks;

        // 4. JTS Polygon + PreparedGeometry
        VesselContactRequest.SearchPolygon poly = request.getPolygon();
        Polygon jtsPolygon = areaSearchService.toJtsPolygon(poly.getCoordinates());
        PreparedGeometry prepared = PreparedGeometryFactory.prepare(jtsPolygon);

        // 5. STRtree 후보 필터링 + 폴리곤 내부 포인트 수집
        STRtree spatialIndex = areaSearchService.buildSpatialIndex(filtered);
        Envelope mbr = jtsPolygon.getEnvelopeInternal();

        @SuppressWarnings("unchecked")
        List<String> candidates = spatialIndex.query(mbr);

        long minDurationSec = request.getMinContactDurationMinutes() * 60L;
        double maxDistanceMeters = request.getMaxContactDistanceMeters();

        Map<String, List<InsidePosition>> insidePositions = new HashMap<>();
        for (String vesselId : candidates) {
            CompactVesselTrack track = filtered.get(vesselId);
            if (track == null || track.getGeometry() == null) continue;

            List<InsidePosition> inside = collectInsidePositions(track, prepared);
            if (!inside.isEmpty()) {
                insidePositions.put(vesselId, inside);
            }
        }

        int totalVesselsInPolygon = insidePositions.size();
        log.info("Vessel contact: filtered={}, insidePolygon={}, dates={}",
                filtered.size(), totalVesselsInPolygon, targetDates.size());

        // 6. 시간 범위 겹침 사전 필터 + 선박 쌍별 접촉 판정
        List<String> vesselIds = new ArrayList<>(insidePositions.keySet());
        List<VesselContactPair> contactPairs = new ArrayList<>();
        Set<String> involvedVessels = new HashSet<>();

        for (int i = 0; i < vesselIds.size(); i++) {
            String idA = vesselIds.get(i);
            List<InsidePosition> posA = insidePositions.get(idA);
            long minTsA = posA.get(0).timestamp;
            long maxTsA = posA.get(posA.size() - 1).timestamp;

            for (int j = i + 1; j < vesselIds.size(); j++) {
                String idB = vesselIds.get(j);
                List<InsidePosition> posB = insidePositions.get(idB);
                long minTsB = posB.get(0).timestamp;
                long maxTsB = posB.get(posB.size() - 1).timestamp;

                // 시간 겹침 사전 필터 (minContactDuration 반영)
                long overlap = Math.min(maxTsA, maxTsB) - Math.max(minTsA, minTsB);
                if (overlap < minDurationSec) continue;

                // Two-pointer 접촉 판정
                List<VesselContactPair> pairs = detectContacts(
                        idA, posA, idB, posB,
                        filtered.get(idA), filtered.get(idB),
                        minDurationSec, maxDistanceMeters);

                if (!pairs.isEmpty()) {
                    contactPairs.addAll(pairs);
                    involvedVessels.add(idA);
                    involvedVessels.add(idB);
                }
            }
        }

        // 7. 관련 선박 트랙 수집
        List<CompactVesselTrack> resultTracks = involvedVessels.stream()
                .map(mergedTracks::get)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        long elapsedMs = System.currentTimeMillis() - startMs;
        log.info("Vessel contact completed: pairs={}, vessels={}, elapsed={}ms",
                contactPairs.size(), involvedVessels.size(), elapsedMs);

        return VesselContactResponse.builder()
                .contacts(contactPairs)
                .tracks(resultTracks)
                .summary(VesselContactSummary.builder()
                        .totalContactPairs(contactPairs.size())
                        .totalVesselsInvolved(involvedVessels.size())
                        .totalVesselsInPolygon(totalVesselsInPolygon)
                        .processingTimeMs(elapsedMs)
                        .polygonId(poly.getId())
                        .cachedDates(targetDates.stream()
                                .map(LocalDate::toString)
                                .collect(Collectors.toList()))
                        .build())
                .build();
    }

    // ── 입력 검증 ──

    private void validateRequest(VesselContactRequest 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 AreaSearchService.CacheNotReadyException(
                    "캐시가 아직 준비되지 않았습니다 (상태: " + cacheStatus + ")");
        }

        VesselContactRequest.SearchPolygon polygon = request.getPolygon();
        areaSearchService.validatePolygon(polygon.getId(), polygon.getCoordinates());
    }

    // ── 폴리곤 내부 포인트 수집 ──

    private List<InsidePosition> collectInsidePositions(
            CompactVesselTrack track, PreparedGeometry prepared) {
        List<double[]> geometry = track.getGeometry();
        List<String> timestamps = track.getTimestamps();
        List<InsidePosition> inside = new ArrayList<>();

        for (int i = 0; i < geometry.size(); i++) {
            double[] coord = geometry.get(i);
            Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(coord[0], coord[1]));
            if (prepared.contains(point)) {
                long ts = parseTimestamp(timestamps, i);
                inside.add(new InsidePosition(ts, coord[0], coord[1]));
            }
        }

        // 시간순 정렬 보장
        inside.sort(Comparator.comparingLong(p -> p.timestamp));
        return inside;
    }

    // ── Two-pointer 접촉 판정 ──

    private List<VesselContactPair> detectContacts(
            String idA, List<InsidePosition> posA,
            String idB, List<InsidePosition> posB,
            CompactVesselTrack trackA, CompactVesselTrack trackB,
            long minDurationSec, double maxDistanceMeters) {

        // Step 1: Two-pointer 매칭 (거리 임계값 없이 모두 수집)
        List<MatchedPoint> matched = twoPointerMatch(posA, posB);
        if (matched.isEmpty()) return Collections.emptyList();

        // Step 2: 연속 세그먼트 분리 (갭 > MAX_GAP)
        List<List<MatchedPoint>> segments = splitByGap(matched);

        // Step 3: 세그먼트별 평균 거리 판정
        List<VesselContactPair> results = new ArrayList<>();
        for (List<MatchedPoint> segment : segments) {
            if (segment.size() < 2) continue;

            long duration = segment.get(segment.size() - 1).timestamp - segment.get(0).timestamp;
            if (duration < minDurationSec) continue;

            double avgDist = segment.stream().mapToDouble(p -> p.distanceMeters).average().orElse(0);
            if (avgDist > maxDistanceMeters) continue;

            // 접촉 확정 — VesselContactPair 생성
            results.add(buildContactPair(segment, idA, posA, idB, posB, trackA, trackB));
        }

        return results;
    }

    private List<MatchedPoint> twoPointerMatch(
            List<InsidePosition> posA, List<InsidePosition> posB) {
        List<MatchedPoint> matched = new ArrayList<>();
        int pA = 0, pB = 0;

        while (pA < posA.size() && pB < posB.size()) {
            InsidePosition a = posA.get(pA);
            InsidePosition b = posB.get(pB);
            long diff = Math.abs(a.timestamp - b.timestamp);

            if (diff <= SYNC_TOLERANCE_SEC) {
                double dist = haversineMeters(a.lat, a.lon, b.lat, b.lon);
                long ts = Math.min(a.timestamp, b.timestamp) + diff / 2; // 중간 시각
                matched.add(new MatchedPoint(ts, dist, a, b));
                pA++;
                pB++;
            } else if (a.timestamp < b.timestamp) {
                pA++;
            } else {
                pB++;
            }
        }

        return matched;
    }

    private List<List<MatchedPoint>> splitByGap(List<MatchedPoint> matched) {
        List<List<MatchedPoint>> segments = new ArrayList<>();
        List<MatchedPoint> current = new ArrayList<>();

        for (MatchedPoint point : matched) {
            if (!current.isEmpty()
                    && (point.timestamp - current.get(current.size() - 1).timestamp) > MAX_GAP_SEC) {
                segments.add(current);
                current = new ArrayList<>();
            }
            current.add(point);
        }
        if (!current.isEmpty()) {
            segments.add(current);
        }

        return segments;
    }

    // ── 결과 빌드 ──

    private VesselContactPair buildContactPair(
            List<MatchedPoint> segment,
            String idA, List<InsidePosition> insidePosA,
            String idB, List<InsidePosition> insidePosB,
            CompactVesselTrack trackA, CompactVesselTrack trackB) {

        long contactStart = segment.get(0).timestamp;
        long contactEnd = segment.get(segment.size() - 1).timestamp;
        long durationMin = (contactEnd - contactStart) / 60;

        DoubleSummaryStatistics distStats = segment.stream()
                .mapToDouble(p -> p.distanceMeters)
                .summaryStatistics();

        // 접촉 중심점 계산
        double centerLon = segment.stream().mapToDouble(p -> (p.posA.lon + p.posB.lon) / 2).average().orElse(0);
        double centerLat = segment.stream().mapToDouble(p -> (p.posA.lat + p.posB.lat) / 2).average().orElse(0);

        // 각 선박의 접촉 구간 내 inside 포인트로 추정 속도 계산
        double speedA = estimateAvgSpeed(insidePosA, contactStart, contactEnd);
        double speedB = estimateAvgSpeed(insidePosB, contactStart, contactEnd);

        VesselContactInfo infoA = buildVesselInfo(idA, trackA, insidePosA, speedA);
        VesselContactInfo infoB = buildVesselInfo(idB, trackB, insidePosB, speedB);

        TransshipmentIndicators indicators = buildIndicators(infoA, infoB, speedA, speedB, contactStart, contactEnd);

        return VesselContactPair.builder()
                .contactStartTimestamp(contactStart)
                .contactEndTimestamp(contactEnd)
                .contactDurationMinutes(durationMin)
                .minDistanceMeters(Math.round(distStats.getMin() * 10.0) / 10.0)
                .avgDistanceMeters(Math.round(distStats.getAverage() * 10.0) / 10.0)
                .maxDistanceMeters(Math.round(distStats.getMax() * 10.0) / 10.0)
                .contactCenterPoint(new double[]{
                        Math.round(centerLon * 1_000_000.0) / 1_000_000.0,
                        Math.round(centerLat * 1_000_000.0) / 1_000_000.0})
                .contactPointCount(segment.size())
                .vessel1(infoA)
                .vessel2(infoB)
                .indicators(indicators)
                .build();
    }

    private VesselContactInfo buildVesselInfo(
            String vesselId, CompactVesselTrack track,
            List<InsidePosition> insidePositions, double estimatedSpeed) {

        long startTs = insidePositions.get(0).timestamp;
        long endTs = insidePositions.get(insidePositions.size() - 1).timestamp;
        long durationMin = (endTs - startTs) / 60;

        return VesselContactInfo.builder()
                .vesselId(vesselId)
                .vesselName(track.getShipName())
                .shipType(track.getShipType())
                .shipKindCode(track.getShipKindCode())
                .nationalCode(track.getNationalCode())
                .insidePolygonStartTs(startTs)
                .insidePolygonEndTs(endTs)
                .insidePolygonDurationMinutes(durationMin)
                .estimatedAvgSpeedKnots(Math.round(estimatedSpeed * 100.0) / 100.0)
                .build();
    }

    private TransshipmentIndicators buildIndicators(
            VesselContactInfo infoA, VesselContactInfo infoB,
            double speedA, double speedB,
            long contactStart, long contactEnd) {

        boolean lowSpeed = speedA < 3.0 && speedB < 3.0;

        boolean diffTypes = !Objects.equals(infoA.getShipKindCode(), infoB.getShipKindCode())
                && infoA.getShipKindCode() != null && infoB.getShipKindCode() != null;

        boolean diffNationality = !Objects.equals(infoA.getNationalCode(), infoB.getNationalCode())
                && infoA.getNationalCode() != null && infoB.getNationalCode() != null;

        boolean nightTime = isNightTimeContact(contactStart, contactEnd);

        return TransshipmentIndicators.builder()
                .lowSpeedContact(lowSpeed)
                .differentVesselTypes(diffTypes)
                .differentNationalities(diffNationality)
                .nightTimeContact(nightTime)
                .build();
    }

    /**
     * 접촉 구간이 22:00~06:00 KST에 포함되는지 판단.
     */
    private boolean isNightTimeContact(long contactStartSec, long contactEndSec) {
        Instant startInstant = Instant.ofEpochSecond(contactStartSec);
        Instant endInstant = Instant.ofEpochSecond(contactEndSec);

        ZonedDateTime startKst = startInstant.atZone(KST);
        ZonedDateTime endKst = endInstant.atZone(KST);

        // 접촉 구간 내 모든 날짜에 대해 야간 시간대 겹침 체크
        LocalDate day = startKst.toLocalDate();
        LocalDate lastDay = endKst.toLocalDate().plusDays(1);

        while (!day.isAfter(lastDay)) {
            // 해당 날짜의 야간: 전날 22:00 ~ 당일 06:00
            ZonedDateTime nightStart = day.atTime(LocalTime.of(22, 0)).atZone(KST).minusDays(1);
            ZonedDateTime nightEnd = day.atTime(LocalTime.of(6, 0)).atZone(KST);

            // 당일 22:00 ~ 다음날 06:00
            ZonedDateTime nightStart2 = day.atTime(LocalTime.of(22, 0)).atZone(KST);
            ZonedDateTime nightEnd2 = day.plusDays(1).atTime(LocalTime.of(6, 0)).atZone(KST);

            if (isOverlapping(startKst, endKst, nightStart, nightEnd)
                    || isOverlapping(startKst, endKst, nightStart2, nightEnd2)) {
                return true;
            }
            day = day.plusDays(1);
        }
        return false;
    }

    private boolean isOverlapping(ZonedDateTime s1, ZonedDateTime e1,
                                  ZonedDateTime s2, ZonedDateTime e2) {
        return s1.isBefore(e2) && s2.isBefore(e1);
    }

    // ── 추정 속도 계산 ──

    /**
     * 접촉 구간 내 폴리곤 내부 포인트 간 거리/시간으로 평균 속도(knots) 추정.
     */
    private double estimateAvgSpeed(List<InsidePosition> insidePositions,
                                    long contactStart, long contactEnd) {
        // 접촉 구간에 해당하는 포인트만 필터
        List<InsidePosition> contactPoints = new ArrayList<>();
        for (InsidePosition pos : insidePositions) {
            if (pos.timestamp >= contactStart && pos.timestamp <= contactEnd) {
                contactPoints.add(pos);
            }
        }

        if (contactPoints.size() < 2) return 0.0;

        double totalDistNm = 0;
        for (int i = 1; i < contactPoints.size(); i++) {
            InsidePosition prev = contactPoints.get(i - 1);
            InsidePosition curr = contactPoints.get(i);
            totalDistNm += haversineNm(prev.lat, prev.lon, curr.lat, curr.lon);
        }

        long firstTs = contactPoints.get(0).timestamp;
        long lastTs = contactPoints.get(contactPoints.size() - 1).timestamp;
        double totalHours = (lastTs - firstTs) / 3600.0;

        return totalHours > 0 ? totalDistNm / totalHours : 0.0;
    }

    // ── Haversine 거리 계산 ──

    private double haversineMeters(double lat1, double lon1, double lat2, double lon2) {
        double dLat = Math.toRadians(lat2 - lat1);
        double dLon = Math.toRadians(lon2 - lon1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
                * Math.sin(dLon / 2) * Math.sin(dLon / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return EARTH_RADIUS_M * c;
    }

    private double haversineNm(double lat1, double lon1, double lat2, double lon2) {
        double dLat = Math.toRadians(lat2 - lat1);
        double dLon = Math.toRadians(lon2 - lon1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
                * Math.sin(dLon / 2) * Math.sin(dLon / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return EARTH_RADIUS_NM * c;
    }

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

    // ── 빈 응답 ──

    private VesselContactResponse buildEmptyResponse(
            VesselContactRequest request, List<LocalDate> targetDates, long startMs) {
        long elapsedMs = System.currentTimeMillis() - startMs;
        return VesselContactResponse.builder()
                .contacts(Collections.emptyList())
                .tracks(Collections.emptyList())
                .summary(VesselContactSummary.builder()
                        .totalContactPairs(0)
                        .totalVesselsInvolved(0)
                        .totalVesselsInPolygon(0)
                        .processingTimeMs(elapsedMs)
                        .polygonId(request.getPolygon().getId())
                        .cachedDates(targetDates.stream()
                                .map(LocalDate::toString)
                                .collect(Collectors.toList()))
                        .build())
                .build();
    }

    // ── 내부 데이터 클래스 ──

    private static class InsidePosition {
        final long timestamp;
        final double lon;
        final double lat;

        InsidePosition(long timestamp, double lon, double lat) {
            this.timestamp = timestamp;
            this.lon = lon;
            this.lat = lat;
        }
    }

    private static class MatchedPoint {
        final long timestamp;
        final double distanceMeters;
        final InsidePosition posA;
        final InsidePosition posB;

        MatchedPoint(long timestamp, double distanceMeters,
                     InsidePosition posA, InsidePosition posB) {
            this.timestamp = timestamp;
            this.distanceMeters = distanceMeters;
            this.posA = posA;
            this.posB = posB;
        }
    }
}
