/**
 * 어구 그룹 궤적 리플레이 전처리 모듈
 *
 * API 데이터(history frames) → deck.gl TripsLayer 포맷으로 변환.
 * iran 프로젝트의 검증된 패턴을 KCG에 맞게 재구현.
 */

// ── 타입 ──────────────────────────────────────────────────────────────

export interface HistoryFrame {
  snapshotTime: string;
  centerLat: number;
  centerLon: number;
  memberCount: number;
  polygon: unknown;
  members: FrameMember[];
  subClusterId?: number;
  areaSqNm?: number;
}

export interface FrameMember {
  mmsi: string;
  name?: string;
  lat: number;
  lon: number;
  cog?: number;
  sog?: number;
  role?: string;
  isParent?: boolean;
}

/** TripsLayer 데이터 항목 — MMSI별 궤적 */
export interface TripsLayerDatum {
  id: string;
  path: [number, number][];
  timestamps: number[];
  color: [number, number, number, number];
}

/** 중심 궤적 세그먼트 (PathLayer용) */
export interface CenterTrailSegment {
  path: [number, number][];
  isInterpolated: boolean;
}

/** 멤버 보간 위치 (프레임 간 보간) */
export interface MemberPosition {
  mmsi: string;
  name: string;
  lon: number;
  lat: number;
  cog: number;
  role: string;
  isParent: boolean;
  isGear: boolean;
  stale: boolean;
}

// ── 색상 상수 ──────────────────────────────────────────────────────────

const PARENT_COLOR: [number, number, number, number] = [6, 182, 212, 220];   // cyan
const GEAR_COLOR: [number, number, number, number] = [245, 158, 11, 200];    // amber
const OTHER_COLOR: [number, number, number, number] = [148, 163, 184, 180];  // slate

function memberColor(role: string, isParent: boolean): [number, number, number, number] {
  if (isParent || role === 'PARENT') return PARENT_COLOR;
  if (role === 'GEAR') return GEAR_COLOR;
  return OTHER_COLOR;
}

// ── 1. buildMemberTripsData ────────────────────────────────────────────

/**
 * 프레임별 멤버를 순회하여 MMSI별 TripsLayer 데이터를 생성.
 * timestamps는 startTime 기준 상대값 (TripsLayer 요구사항).
 */
export function buildMemberTripsData(
  frames: HistoryFrame[],
  startTime: number,
): TripsLayerDatum[] {
  const byMmsi = new Map<string, {
    path: [number, number][];
    timestamps: number[];
    role: string;
    isParent: boolean;
  }>();

  for (const frame of frames) {
    const frameTime = new Date(frame.snapshotTime).getTime();
    const relativeTime = frameTime - startTime;

    for (const m of frame.members) {
      if (m.lat == null || m.lon == null) continue;

      let entry = byMmsi.get(m.mmsi);
      if (!entry) {
        entry = {
          path: [],
          timestamps: [],
          role: m.role ?? 'UNKNOWN',
          isParent: m.isParent ?? false,
        };
        byMmsi.set(m.mmsi, entry);
      }
      entry.path.push([m.lon, m.lat]);
      entry.timestamps.push(relativeTime);
    }
  }

  const result: TripsLayerDatum[] = [];
  for (const [mmsi, entry] of byMmsi) {
    if (entry.path.length < 2) continue; // 궤적이 되려면 최소 2점
    result.push({
      id: mmsi,
      path: entry.path,
      timestamps: entry.timestamps,
      color: memberColor(entry.role, entry.isParent),
    });
  }
  return result;
}

// ── 2. buildCenterTrailData ────────────────────────────────────────────

/**
 * 어구 그룹 중심 이동 궤적 (PathLayer용).
 * 연속 프레임의 중심을 이어서 세그먼트 생성.
 */
export function buildCenterTrailData(frames: HistoryFrame[]): {
  segments: CenterTrailSegment[];
  dots: [number, number][];
} {
  const dots: [number, number][] = [];
  const path: [number, number][] = [];

  for (const f of frames) {
    if (f.centerLon == null || f.centerLat == null) continue;
    const pos: [number, number] = [f.centerLon, f.centerLat];
    dots.push(pos);
    path.push(pos);
  }

  // 단일 연속 세그먼트 (끊김 없음)
  const segments: CenterTrailSegment[] = path.length >= 2
    ? [{ path, isInterpolated: false }]
    : [];

  return { segments, dots };
}

// ── 3. buildSnapshotRanges ─────────────────────────────────────────────

/**
 * 프로그레스바 틱마크용 — 각 스냅샷 시점을 [0, 1] 범위로 정규화.
 */
export function buildSnapshotRanges(
  frames: HistoryFrame[],
  startTime: number,
  endTime: number,
): number[] {
  const duration = endTime - startTime;
  if (duration <= 0) return [];

  return frames.map(f => {
    const t = new Date(f.snapshotTime).getTime();
    return (t - startTime) / duration;
  });
}

// ── 4. findFrameAtTime ─────────────────────────────────────────────────

/**
 * 현재 시점에 해당하는 프레임 인덱스를 찾는다.
 * 커서 기반 전진 스캔(O(1) amortized) + 이진탐색 fallback.
 *
 * @returns { index, cursor } — index=-1이면 stale (30분 이상 이전)
 */
export function findFrameAtTime(
  frameTimes: number[],
  timeMs: number,
  cursor: number,
): { index: number; cursor: number } {
  const len = frameTimes.length;
  if (len === 0) return { index: -1, cursor: 0 };

  // 범위 밖
  if (timeMs <= frameTimes[0]) return { index: 0, cursor: 0 };
  if (timeMs >= frameTimes[len - 1]) return { index: len - 1, cursor: len - 1 };

  // 커서 전진 스캔 (재생 중 대부분 여기서 해결)
  let c = Math.min(Math.max(0, cursor), len - 1);
  if (frameTimes[c] <= timeMs) {
    while (c < len - 1 && frameTimes[c + 1] <= timeMs) c++;
    return { index: c, cursor: c };
  }

  // fallback: 이진탐색 (seek 시)
  let lo = 0;
  let hi = len - 1;
  while (lo < hi) {
    const mid = (lo + hi + 1) >> 1;
    if (frameTimes[mid] <= timeMs) lo = mid;
    else hi = mid - 1;
  }
  return { index: lo, cursor: lo };
}

// ── 5. interpolateMemberPositions ──────────────────────────────────────

// ── 7. computeConvexHull ───────────────────────────────────────────────

/**
 * 멤버 위치 기반 Convex Hull 계산 (Graham scan) + 패딩.
 *
 * 리플레이 시 매 프레임 보간된 멤버 위치로 폴리곤을 직접 생성.
 * API 폴리곤(프레임별 스냅샷)이 아닌 실시간 멤버 위치 반영.
 *
 * @param paddingDeg — 외곽 패딩 (도 단위, 기본 0.005° ≈ 약 500m)
 */
export function computeConvexHull(
  positions: MemberPosition[],
  paddingDeg = 0.005,
): [number, number][] | null {
  const points: [number, number][] = positions.map(p => [p.lon, p.lat]);
  if (points.length < 3) return null;

  // 가장 아래(lat 최소), 같으면 왼쪽(lon 최소) 점 찾기
  let pivot = 0;
  for (let i = 1; i < points.length; i++) {
    if (
      points[i][1] < points[pivot][1] ||
      (points[i][1] === points[pivot][1] && points[i][0] < points[pivot][0])
    ) {
      pivot = i;
    }
  }
  [points[0], points[pivot]] = [points[pivot], points[0]];
  const origin = points[0];

  // 극각 기준 정렬
  const rest = points.slice(1).sort((a, b) => {
    const angleA = Math.atan2(a[1] - origin[1], a[0] - origin[0]);
    const angleB = Math.atan2(b[1] - origin[1], b[0] - origin[0]);
    if (angleA !== angleB) return angleA - angleB;
    // 같은 각도면 거리순
    const distA = (a[0] - origin[0]) ** 2 + (a[1] - origin[1]) ** 2;
    const distB = (b[0] - origin[0]) ** 2 + (b[1] - origin[1]) ** 2;
    return distA - distB;
  });

  const stack: [number, number][] = [origin];
  for (const p of rest) {
    while (stack.length >= 2) {
      const a = stack[stack.length - 2];
      const b = stack[stack.length - 1];
      // 외적: 반시계 방향이 아니면 제거
      const cross = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
      if (cross > 0) break;
      stack.pop();
    }
    stack.push(p);
  }

  if (stack.length < 3) return null;

  // 중심 계산
  let cx = 0, cy = 0;
  for (const p of stack) { cx += p[0]; cy += p[1]; }
  cx /= stack.length;
  cy /= stack.length;

  // 패딩 적용: 각 꼭짓점을 중심에서 바깥으로 paddingDeg만큼 밀어냄
  const padded: [number, number][] = stack.map(p => {
    const dx = p[0] - cx;
    const dy = p[1] - cy;
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (dist === 0) return p;
    const scale = (dist + paddingDeg) / dist;
    return [cx + dx * scale, cy + dy * scale];
  });

  // GeoJSON Polygon은 첫 점 = 끝 점 (닫힌 링)
  padded.push(padded[0]);
  return padded;
}

// ── 5. 멤버 메타데이터 ─────────────────────────────────────────────────

/** 멤버 메타 정보 (name, role, isParent) — 전체 프레임에서 추출 */
export interface MemberMeta {
  name: string;
  role: string;
  isParent: boolean;
}

/**
 * 전체 프레임을 순회하여 멤버별 메타 정보 수집.
 * 나중 프레임이 이전 프레임을 덮어씀 (최신 정보 우선).
 */
export function buildMemberMetadata(
  frames: HistoryFrame[],
): Map<string, MemberMeta> {
  const meta = new Map<string, MemberMeta>();
  for (const frame of frames) {
    for (const m of frame.members) {
      meta.set(m.mmsi, {
        name: m.name ?? '',
        role: m.role ?? 'UNKNOWN',
        isParent: m.isParent ?? false,
      });
    }
  }
  return meta;
}

// ── 6. interpolateFromTripsData ────────────────────────────────────────

/**
 * 두 좌표 사이의 heading(침로) 계산.
 * iran animationStore.ts의 calculateHeading과 동일.
 */
function calcHeading(p1: [number, number], p2: [number, number]): number {
  const dx = p2[0] - p1[0];
  const dy = p2[1] - p1[1];
  let angle = (Math.atan2(dx, dy) * 180) / Math.PI;
  if (angle < 0) angle += 360;
  return angle;
}

/**
 * iran의 getCurrentVesselPositions 패턴 — 멤버별 개별 타임라인에서 보간.
 *
 * 프레임 기반(frameA/frameB) 대신 멤버별 경로(memberTripsData)를 사용하여
 * 각 멤버가 24시간 내내 연속 경로로 유지.
 * 커서 기반 전진 스캔(O(1)) + 이진탐색 fallback (iran과 동일).
 *
 * @param memberTripsData — 전처리된 멤버별 경로 (timestamps는 startTime 기준 상대값)
 * @param memberMeta — 멤버 메타 정보 (name, role, isParent)
 * @param relativeTimeMs — startTime 기준 상대 시간 (ms)
 * @param cursors — 멤버별 커서 Map (호출 간 유지, 재생 시 O(1))
 */
export function interpolateFromTripsData(
  memberTripsData: TripsLayerDatum[],
  memberMeta: Map<string, MemberMeta>,
  relativeTimeMs: number,
  cursors: Map<string, number>,
): MemberPosition[] {
  const positions: MemberPosition[] = [];

  for (const trip of memberTripsData) {
    const { id: mmsi, path, timestamps } = trip;
    if (path.length === 0) continue;

    const meta = memberMeta.get(mmsi);
    const role = meta?.role ?? 'UNKNOWN';
    const base = {
      mmsi,
      name: meta?.name ?? '',
      role,
      isParent: meta?.isParent ?? false,
      isGear: role === 'GEAR',
      stale: false,
    };

    // 범위 밖: 처음/마지막 위치 고정
    if (relativeTimeMs <= timestamps[0]) {
      positions.push({ ...base, lon: path[0][0], lat: path[0][1], cog: 0 });
      continue;
    }
    if (relativeTimeMs >= timestamps[timestamps.length - 1]) {
      const last = path.length - 1;
      const cog = last > 0 ? calcHeading(path[last - 1], path[last]) : 0;
      positions.push({ ...base, lon: path[last][0], lat: path[last][1], cog });
      cursors.set(mmsi, last);
      continue;
    }

    // 커서 기반 탐색 (iran positionCursors 패턴)
    let cursor = cursors.get(mmsi) ?? 0;

    if (
      cursor >= timestamps.length ||
      (cursor > 0 && timestamps[cursor - 1] > relativeTimeMs)
    ) {
      // 커서 무효 or 시간 역행 → 이진탐색 fallback (seek 시)
      let lo = 0;
      let hi = timestamps.length - 1;
      while (lo < hi) {
        const mid = (lo + hi + 1) >> 1;
        if (timestamps[mid] <= relativeTimeMs) lo = mid;
        else hi = mid - 1;
      }
      cursor = lo;
    } else {
      // 선형 전진 (재생 중 1~2칸, O(1))
      while (cursor < timestamps.length - 1 && timestamps[cursor + 1] <= relativeTimeMs) {
        cursor++;
      }
    }
    cursors.set(mmsi, cursor);

    const idx1 = cursor;
    const idx2 = Math.min(cursor + 1, timestamps.length - 1);

    if (idx1 === idx2 || timestamps[idx1] === timestamps[idx2]) {
      const cog = idx1 > 0 ? calcHeading(path[idx1 - 1], path[idx1]) : 0;
      positions.push({ ...base, lon: path[idx1][0], lat: path[idx1][1], cog });
    } else {
      // 선형 보간 (iran의 interpolatePosition과 동일)
      const ratio = (relativeTimeMs - timestamps[idx1]) / (timestamps[idx2] - timestamps[idx1]);
      const lon = path[idx1][0] + (path[idx2][0] - path[idx1][0]) * ratio;
      const lat = path[idx1][1] + (path[idx2][1] - path[idx1][1]) * ratio;
      const cog = calcHeading(path[idx1], path[idx2]);
      positions.push({ ...base, lon, lat, cog });
    }
  }

  return positions;
}
