package gc.mda.signal_batch.global.websocket.service;

import gc.mda.signal_batch.domain.vessel.dto.CompactVesselTrack;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;

/**
 * 인메모리 캐시 트랙 간소화 유틸리티
 * DB 경로의 ST_Simplify + 거리/시간 샘플링 + 줌 샘플링과 동등한 간소화를 캐시 경로에 적용
 */
@Slf4j
@Component
public class CacheTrackSimplifier {

    private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final double EARTH_RADIUS_NM = 3440.065;

    // ── SimplificationConfig: 향후 요청 파라미터로 노출 가능한 구조 ──
    @Data
    @Builder
    public static class SimplificationConfig {
        /** Douglas-Peucker tolerance (ST_Simplify 대체, degree 단위) */
        @Builder.Default private double dpTolerance = 0.005;
        /** 거리 기반 샘플링 최소 거리 (해리) */
        @Builder.Default private double minDistanceNm = 0.05;
        /** 시간 기반 샘플링 최소 간격 (분) */
        @Builder.Default private long minIntervalMinutes = 60;
        /** 저속 선박 속도 임계값 (knots) */
        @Builder.Default private double lowSpeedThreshold = 5.0;
        /** 저속 선박 최소 거리 (해리) */
        @Builder.Default private double lowSpeedMinDistanceNm = 1.5;
        /** 저속 선박 최소 시간 간격 (분) */
        @Builder.Default private long lowSpeedMinIntervalMinutes = 45;
        /** 줌 기반 N번째 포인트 샘플링 (null = 미적용) */
        @Builder.Default private Integer zoomSampleRate = null;
    }

    // ── zoom → config 팩토리 ──
    public static SimplificationConfig forZoomLevel(Integer zoomLevel) {
        return SimplificationConfig.builder()
                .dpTolerance(getDpTolerance(zoomLevel))
                .zoomSampleRate(getZoomSampleRate(zoomLevel))
                .build();
    }

    private static double getDpTolerance(Integer zoom) {
        if (zoom == null) zoom = 10;
        return switch (zoom) {
            case 0, 1, 2, 3, 4, 5 -> 0.05;
            case 6, 7 -> 0.02;
            case 8, 9 -> 0.01;
            case 10, 11 -> 0.005;
            default -> 0.002;
        };
    }

    private static Integer getZoomSampleRate(Integer zoom) {
        if (zoom == null || zoom >= 10) return null;
        if (zoom < 6) return 10;
        if (zoom < 8) return 5;
        return 2;
    }

    // ── 메인 API ──

    public List<CompactVesselTrack> simplify(List<CompactVesselTrack> tracks, Integer zoomLevel) {
        return simplify(tracks, forZoomLevel(zoomLevel));
    }

    public List<CompactVesselTrack> simplify(List<CompactVesselTrack> tracks, SimplificationConfig config) {
        if (tracks == null || tracks.isEmpty()) {
            return tracks;
        }

        long startTime = System.currentTimeMillis();
        int totalOriginalPoints = 0;
        int totalAfterDp = 0;
        int totalAfterDistTime = 0;
        int totalAfterZoom = 0;
        int simplifiedCount = 0;
        int skippedCount = 0;

        for (int t = 0; t < tracks.size(); t++) {
            CompactVesselTrack track = tracks.get(t);
            if (track.getGeometry() == null || track.getGeometry().size() <= 2) {
                skippedCount++;
                continue;
            }

            int originalSize = track.getGeometry().size();
            totalOriginalPoints += originalSize;

            // 1단계: Douglas-Peucker
            applyDouglasPeucker(track, config.getDpTolerance());
            int afterDp = track.getGeometry().size();
            totalAfterDp += afterDp;

            // 2단계: 거리/시간 샘플링
            applyDistanceTimeSampling(track, config);
            int afterDistTime = track.getGeometry().size();
            totalAfterDistTime += afterDistTime;

            // 3단계: 줌 기반 추가 샘플링
            if (config.getZoomSampleRate() != null) {
                applyZoomSampling(track, config.getZoomSampleRate());
            }
            int afterZoom = track.getGeometry().size();
            totalAfterZoom += afterZoom;

            track.setPointCount(afterZoom);

            // 처음 5개 선박 상세 로그 (debug 레벨)
            if (simplifiedCount < 5) {
                log.debug("[CacheSimplify] vessel={} original={} -> DP={} -> distTime={} -> zoom={} (avg={} kn)",
                        track.getVesselId(), originalSize, afterDp, afterDistTime, afterZoom,
                        track.getAvgSpeed() != null ? String.format("%.1f", track.getAvgSpeed()) : "N/A");
            }
            simplifiedCount++;
        }

        long elapsed = System.currentTimeMillis() - startTime;

        if (simplifiedCount > 0) {
            double totalReduction = (1 - (double) totalAfterZoom / totalOriginalPoints) * 100;

            log.info("[CacheSimplify] tracks={} (skip={}), {} -> {} pts ({}% 감소), {}ms",
                    simplifiedCount, skippedCount,
                    totalOriginalPoints, totalAfterZoom, Math.round(totalReduction), elapsed);
        }

        return tracks;
    }

    // ── 1단계: Douglas-Peucker (ST_Simplify 대체) ──

    private void applyDouglasPeucker(CompactVesselTrack track, double tolerance) {
        List<double[]> geometry = track.getGeometry();
        int n = geometry.size();
        if (n <= 2) return;

        boolean[] keep = new boolean[n];
        keep[0] = true;
        keep[n - 1] = true;

        douglasPeuckerRecursive(geometry, 0, n - 1, tolerance, keep);

        List<double[]> newGeometry = new ArrayList<>();
        List<String> newTimestamps = new ArrayList<>();
        List<Double> newSpeeds = new ArrayList<>();

        List<String> timestamps = track.getTimestamps();
        List<Double> speeds = track.getSpeeds();

        for (int i = 0; i < n; i++) {
            if (keep[i]) {
                newGeometry.add(geometry.get(i));
                if (timestamps != null && i < timestamps.size()) {
                    newTimestamps.add(timestamps.get(i));
                }
                if (speeds != null && i < speeds.size()) {
                    newSpeeds.add(speeds.get(i));
                }
            }
        }

        track.setGeometry(newGeometry);
        track.setTimestamps(newTimestamps);
        track.setSpeeds(newSpeeds);
    }

    private void douglasPeuckerRecursive(List<double[]> points, int start, int end, double tolerance, boolean[] keep) {
        if (end - start < 2) return;

        double maxDist = 0;
        int maxIndex = start;

        double[] p1 = points.get(start);
        double[] p2 = points.get(end);

        for (int i = start + 1; i < end; i++) {
            double dist = perpendicularDistance(points.get(i), p1, p2);
            if (dist > maxDist) {
                maxDist = dist;
                maxIndex = i;
            }
        }

        if (maxDist > tolerance) {
            keep[maxIndex] = true;
            douglasPeuckerRecursive(points, start, maxIndex, tolerance, keep);
            douglasPeuckerRecursive(points, maxIndex, end, tolerance, keep);
        }
    }

    /**
     * 점-선분 직교 거리 (degree 단위, ST_Simplify와 동일)
     */
    private double perpendicularDistance(double[] point, double[] lineStart, double[] lineEnd) {
        double dx = lineEnd[0] - lineStart[0];
        double dy = lineEnd[1] - lineStart[1];

        if (dx == 0 && dy == 0) {
            // 선분이 점인 경우
            double pdx = point[0] - lineStart[0];
            double pdy = point[1] - lineStart[1];
            return Math.sqrt(pdx * pdx + pdy * pdy);
        }

        double t = ((point[0] - lineStart[0]) * dx + (point[1] - lineStart[1]) * dy) / (dx * dx + dy * dy);
        t = Math.max(0, Math.min(1, t));

        double nearestX = lineStart[0] + t * dx;
        double nearestY = lineStart[1] + t * dy;

        double distX = point[0] - nearestX;
        double distY = point[1] - nearestY;

        return Math.sqrt(distX * distX + distY * distY);
    }

    // ── 2단계: 거리/시간 기반 샘플링 ──

    private void applyDistanceTimeSampling(CompactVesselTrack track, SimplificationConfig config) {
        List<double[]> geometry = track.getGeometry();
        List<String> timestamps = track.getTimestamps();
        List<Double> speeds = track.getSpeeds();

        if (geometry.size() <= 2) return;

        List<double[]> simplified = new ArrayList<>();
        List<String> simplifiedTs = new ArrayList<>();
        List<Double> simplifiedSpd = new ArrayList<>();

        double[] prevPoint = null;
        LocalDateTime prevTime = null;

        for (int i = 0; i < geometry.size(); i++) {
            double[] point = geometry.get(i);
            boolean include = false;

            if (i == 0 || i == geometry.size() - 1) {
                include = true;
            } else if (prevPoint != null && prevTime != null && timestamps != null && i < timestamps.size()) {
                LocalDateTime currentTime = parseTimestamp(timestamps.get(i));
                if (currentTime != null) {
                    double distance = calculateDistance(prevPoint[1], prevPoint[0], point[1], point[0]);
                    long minutesSincePrev = ChronoUnit.MINUTES.between(prevTime, currentTime);

                    // DAILY 전략 기준 (DB 경로와 동일)
                    include = distance > config.getMinDistanceNm() || minutesSincePrev >= config.getMinIntervalMinutes();

                    // 저속 선박 강화 간소화
                    if (track.getAvgSpeed() != null && track.getAvgSpeed() > 0 && track.getAvgSpeed() < config.getLowSpeedThreshold()) {
                        include = distance > config.getLowSpeedMinDistanceNm() || minutesSincePrev >= config.getLowSpeedMinIntervalMinutes();
                    }
                }
            }

            if (include) {
                simplified.add(point);
                if (timestamps != null && i < timestamps.size()) {
                    simplifiedTs.add(timestamps.get(i));
                }
                if (speeds != null && i < speeds.size()) {
                    simplifiedSpd.add(speeds.get(i));
                }
                prevPoint = point;
                if (timestamps != null && i < timestamps.size()) {
                    prevTime = parseTimestamp(timestamps.get(i));
                }
            }
        }

        track.setGeometry(simplified);
        track.setTimestamps(simplifiedTs);
        track.setSpeeds(simplifiedSpd);
    }

    private LocalDateTime parseTimestamp(String tsStr) {
        if (tsStr == null) return null;
        try {
            if (tsStr.matches("\\d{10,}")) {
                return LocalDateTime.ofInstant(
                        java.time.Instant.ofEpochSecond(Long.parseLong(tsStr)),
                        java.time.ZoneId.systemDefault());
            } else {
                return LocalDateTime.parse(tsStr, TIMESTAMP_FORMATTER);
            }
        } catch (Exception e) {
            return null;
        }
    }

    // ── 3단계: 줌 기반 추가 샘플링 ──

    private void applyZoomSampling(CompactVesselTrack track, int sampleRate) {
        List<double[]> geometry = track.getGeometry();
        List<String> timestamps = track.getTimestamps();
        List<Double> speeds = track.getSpeeds();

        if (geometry.size() <= 2 || sampleRate <= 1) return;

        List<double[]> sampled = new ArrayList<>();
        List<String> sampledTs = new ArrayList<>();
        List<Double> sampledSpd = new ArrayList<>();

        for (int i = 0; i < geometry.size(); i++) {
            if (i % sampleRate == 0 || i == geometry.size() - 1) {
                sampled.add(geometry.get(i));
                if (timestamps != null && i < timestamps.size()) {
                    sampledTs.add(timestamps.get(i));
                }
                if (speeds != null && i < speeds.size()) {
                    sampledSpd.add(speeds.get(i));
                }
            }
        }

        track.setGeometry(sampled);
        track.setTimestamps(sampledTs);
        track.setSpeeds(sampledSpd);
    }

    // ── 포인트 버짓 강제 (REST V2 응답 크기 제한) ──

    /**
     * 전체 트랙의 총 포인트가 상한을 초과하면 비율 기반 균등 분배 샘플링 적용.
     * 모든 트랙에 동일한 비율(ratio)을 적용하여 일관적 간소화 보장.
     *
     * 예시: 총 51만, 상한 50만 → ratio=0.98, 각 트랙 포인트 2%만 제거 → 결과 ≈50만
     * 예시: 총 100만, 상한 50만 → ratio=0.5, 각 트랙 포인트 50% 제거 → 결과 ≈50만
     */
    public List<CompactVesselTrack> enforcePointBudget(List<CompactVesselTrack> tracks, int maxTotalPoints) {
        if (tracks == null || tracks.isEmpty() || maxTotalPoints <= 0) {
            return tracks;
        }

        int totalPoints = tracks.stream()
                .mapToInt(t -> t.getGeometry() != null ? t.getGeometry().size() : 0)
                .sum();

        if (totalPoints <= maxTotalPoints) {
            log.debug("[PointBudget] totalPoints={} <= max={}, no additional simplification needed",
                    totalPoints, maxTotalPoints);
            return tracks;
        }

        double ratio = (double) maxTotalPoints / totalPoints;
        log.info("[PointBudget] totalPoints={} > max={}, applying uniform ratio={} to {} tracks",
                totalPoints, maxTotalPoints, String.format("%.4f", ratio), tracks.size());

        int resultPoints = 0;
        for (CompactVesselTrack track : tracks) {
            if (track.getGeometry() != null && track.getGeometry().size() > 2) {
                int trackPoints = track.getGeometry().size();
                int targetPoints = Math.max(2, (int) Math.round(trackPoints * ratio));

                if (targetPoints < trackPoints) {
                    applyProportionalSampling(track, targetPoints);
                    track.setPointCount(track.getGeometry().size());
                }
            }
            resultPoints += track.getGeometry() != null ? track.getGeometry().size() : 0;
        }

        log.info("[PointBudget] reduced {} -> {} points ({} tracks)",
                totalPoints, resultPoints, tracks.size());

        return tracks;
    }

    /**
     * 비율 기반 균등 분배 샘플링: targetPoints 개수만큼 균등 간격으로 포인트 선택.
     * 첫 포인트와 마지막 포인트는 항상 보존.
     */
    private void applyProportionalSampling(CompactVesselTrack track, int targetPoints) {
        List<double[]> geometry = track.getGeometry();
        List<String> timestamps = track.getTimestamps();
        List<Double> speeds = track.getSpeeds();
        int n = geometry.size();

        if (targetPoints >= n || targetPoints < 2) return;

        List<double[]> sampled = new ArrayList<>(targetPoints);
        List<String> sampledTs = (timestamps != null) ? new ArrayList<>(targetPoints) : null;
        List<Double> sampledSpd = (speeds != null) ? new ArrayList<>(targetPoints) : null;

        double stride = (double) (n - 1) / (targetPoints - 1);

        for (int i = 0; i < targetPoints; i++) {
            int index = (i == targetPoints - 1) ? n - 1 : (int) Math.round(i * stride);

            sampled.add(geometry.get(index));
            if (sampledTs != null && timestamps != null && index < timestamps.size()) {
                sampledTs.add(timestamps.get(index));
            }
            if (sampledSpd != null && speeds != null && index < speeds.size()) {
                sampledSpd.add(speeds.get(index));
            }
        }

        track.setGeometry(sampled);
        if (sampledTs != null) track.setTimestamps(sampledTs);
        if (sampledSpd != null) track.setSpeeds(sampledSpd);
    }

    // ── 거리 계산 (Haversine, 해리 단위) ──

    private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
        double lat1Rad = Math.toRadians(lat1);
        double lat2Rad = Math.toRadians(lat2);
        double deltaLat = lat2Rad - lat1Rad;
        double deltaLon = Math.toRadians(lon2 - lon1);

        double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
                Math.cos(lat1Rad) * Math.cos(lat2Rad) *
                        Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

        return EARTH_RADIUS_NM * c;
    }
}
