/**
 * 클라이언트 지리정보 검색 알고리즘
 *
 * 서버 server/src/services/geoMatch.ts 의 검색/투영 로직을 그대로 포팅한다.
 * (검색 UI 결과를 서버 응답과 동일하게 유지하기 위해, 오버레이용 proj4 기반
 *  geoProjection.ts 와는 별개의 단순 ENU(cos-lat 근사) 투영을 사용한다.)
 *
 * 싱글턴 캐시(loadFrames/loadPois) 대신 데이터를 인자로 받는 순수 함수로 변경했다.
 * 입력 데이터는 geoStore 에서 읽어 넘긴다.
 */

import type {
  DroneFrame,
  GeoPoint,
  FrameMatch,
  PoiInFrame,
  GeoOrigin,
} from '../types/geo';

// ── 카메라/지구 상수 (geoMatch.ts 동일) ──────────────────────────────
const SENSOR_W_MM = 36;
const SENSOR_H_MM = 24;
const R_EARTH = 6371000;
const DEFAULT_FPS = 30;
const GAP = 30;

export function toRad(deg: number): number {
  return (deg * Math.PI) / 180;
}

/** 위경도+표고 → 월드 ENU (m). refLat/refLon/refAlt = 원점. geoMatch.ts:78-87 */
export function geoToEnu(
  lat: number, lon: number, alt: number,
  refLat: number, refLon: number, refAlt: number,
): [number, number, number] {
  const cosRef = Math.cos(toRad(refLat));
  const e = toRad(lon - refLon) * cosRef * R_EARTH; // East  (m)
  const n = toRad(lat - refLat) * R_EARTH;          // North (m)
  const u = alt - refAlt;                            // Up    (m)
  return [e, n, u];
}

/** 월드 ENU 벡터 + 카메라 자세 → 정규화 픽셀. geoMatch.ts:90-124 */
export function projectEnu(
  relEnu: [number, number, number],
  yawDeg: number,
  pitchDeg: number,
  focalMm: number,
  yawOffset = 0,
): { px: number; py: number; cx: number; cy: number; cz: number } | null {
  const yaw = toRad(yawDeg + yawOffset);
  const pitch = toRad(pitchDeg);
  const cosY = Math.cos(yaw), sinY = Math.sin(yaw);
  const cosP = Math.cos(pitch), sinP = Math.sin(pitch);

  const fwd: readonly [number, number, number] = [sinY * cosP, cosY * cosP, sinP];
  const right: readonly [number, number, number] = [cosY, -sinY, 0];
  const up: readonly [number, number, number] = [
    right[1] * fwd[2] - right[2] * fwd[1],
    right[2] * fwd[0] - right[0] * fwd[2],
    right[0] * fwd[1] - right[1] * fwd[0],
  ];

  const dot = (a: readonly [number, number, number], b: readonly [number, number, number]) =>
    a[0] * b[0] + a[1] * b[1] + a[2] * b[2];

  const cx = dot(relEnu, right);
  const cy = dot(relEnu, up);
  const cz = dot(relEnu, fwd);

  if (cz <= 0) return null;

  const f = focalMm || 24;
  const px = 0.5 + (cx / cz) * (f / SENSOR_W_MM);
  const py = 0.5 - (cy / cz) * (f / SENSOR_H_MM);
  return { px, py, cx, cy, cz };
}

/** DroneFrame + POI + 공통 기준점 → 픽셀. geoMatch.ts:127-156 */
export function project3D(
  drone: DroneFrame,
  poi: { lat: number; lon: number; z: number },
  yawOffset = 0,
  origin?: GeoOrigin,
): { px: number; py: number; dist: number; h: number; v: number; inFov: boolean } | null {
  const ref = origin ?? { lat: drone.lat, lon: drone.lon, alt: drone.altitude };

  const stEnu = geoToEnu(poi.lat, poi.lon, poi.z, ref.lat, ref.lon, ref.alt);
  const drEnu = geoToEnu(drone.lat, drone.lon, drone.altitude, ref.lat, ref.lon, ref.alt);
  const relEnu: [number, number, number] = [
    stEnu[0] - drEnu[0],
    stEnu[1] - drEnu[1],
    stEnu[2] - drEnu[2],
  ];
  const dist = Math.sqrt(relEnu[0] ** 2 + relEnu[1] ** 2);

  const res = projectEnu(relEnu, drone.yaw, drone.pitch, drone.focalLen || 24, yawOffset);
  if (!res) return null;

  const h = Math.atan2(res.cx, res.cz) * (180 / Math.PI);
  const v = Math.atan2(res.cy, res.cz) * (180 / Math.PI);
  const inFov = res.px >= 0 && res.px <= 1 && res.py >= 0 && res.py <= 1;
  return {
    px: Math.max(0, Math.min(1, res.px)),
    py: Math.max(0, Math.min(1, res.py)),
    dist, h, v, inFov,
  };
}

/** 측점 km 정렬 키. geoMatch.ts:371-375 */
export function stationOrder(title: string): number {
  const m = title.match(/(\d+)[Kk](\d+)/);
  if (!m) return 0;
  return parseInt(m[1]) * 1000 + parseInt(m[2]);
}

/** 첫 번째 측점 위치를 ENU 원점으로 반환. geoMatch.ts:378-387 */
export function getWorldOrigin(frames: DroneFrame[], pois: GeoPoint[]): GeoOrigin {
  const stations = pois
    .filter((p) => p.type === 'station')
    .sort((a, b) => stationOrder(a.title) - stationOrder(b.title));
  if (stations.length) {
    return { lat: stations[0].lat, lon: stations[0].lon, alt: stations[0].z };
  }
  return frames[0]
    ? { lat: frames[0].lat, lon: frames[0].lon, alt: frames[0].altitude }
    : { lat: 0, lon: 0, alt: 0 };
}

/**
 * 건물/측점명 검색 → 매칭 POI + 카메라 시야 프레임 목록. geoMatch.ts:399-468
 * pois 는 POI+측점 통합 배열을 넘긴다(검색 대상 전체).
 */
export function findFramesForPoi(
  frames: DroneFrame[],
  pois: GeoPoint[],
  query: string,
  marginFactor = 1.0,
  maxDist = 2000,
  yawOffset = 0,
  origin?: GeoOrigin | null,
): { poi: GeoPoint | null; frames: FrameMatch[] } {
  const q = query.trim().toLowerCase();
  const poi = pois.find((p) => p.title.toLowerCase().includes(q));
  if (!poi) return { poi: null, frames: [] };

  const matches: FrameMatch[] = [];
  const ref = origin ?? getWorldOrigin(frames, pois);

  for (const f of frames) {
    const res = project3D(f, poi, yawOffset, ref);
    if (!res) continue;
    if (res.dist > maxDist) continue;

    const halfW = 0.5 * marginFactor;
    const halfH = 0.5 * marginFactor;
    const rawPx = 0.5 + (res.px - 0.5);
    const rawPy = 0.5 + (res.py - 0.5);
    if (Math.abs(rawPx - 0.5) > halfW || Math.abs(rawPy - 0.5) > halfH) continue;

    matches.push({
      frame: f.frame,
      time: f.frame / DEFAULT_FPS,
      bearingDiff: res.h,
      elevationDiff: res.v,
      distance: res.dist,
      pixelX: res.px,
      pixelY: res.py,
    });
  }

  matches.sort((a, b) => a.frame - b.frame);

  const groups: FrameMatch[][] = [];
  let group: FrameMatch[] = [];
  for (const m of matches) {
    if (group.length === 0 || m.frame - group[group.length - 1].frame <= GAP) {
      group.push(m);
    } else {
      groups.push(group);
      group = [m];
    }
  }
  if (group.length > 0) groups.push(group);

  const best = groups.map((g) => {
    const groupStart = g[0].frame;
    const groupEnd = g[g.length - 1].frame;
    g.sort(
      (a, b) =>
        a.bearingDiff ** 2 + a.elevationDiff ** 2 - (b.bearingDiff ** 2 + b.elevationDiff ** 2),
    );
    return { ...g[0], groupSize: g.length, groupStart, groupEnd };
  });

  best.sort(
    (a, b) =>
      a.bearingDiff ** 2 + a.elevationDiff ** 2 - (b.bearingDiff ** 2 + b.elevationDiff ** 2),
  );

  return { poi, frames: best };
}

/**
 * 프레임 번호 → 해당 프레임에서 카메라 시야에 들어오는 POI/측점 목록. geoMatch.ts:473-512
 */
export function findPoisForFrame(
  frames: DroneFrame[],
  pois: GeoPoint[],
  frameNum: number,
  marginFactor = 1.0,
  yawOffset = 0,
  origin?: GeoOrigin | null,
): { droneFrame: DroneFrame | null; pois: PoiInFrame[] } {
  const drone =
    frames.find((f) => f.frame === frameNum) ??
    (() => {
      let best = frames[0];
      let bestD = Math.abs((best?.frame ?? 0) - frameNum);
      for (const f of frames) {
        const d = Math.abs(f.frame - frameNum);
        if (d < bestD) {
          bestD = d;
          best = f;
        }
        if (d === 0) break;
      }
      return best;
    })();
  if (!drone) return { droneFrame: null, pois: [] };

  const result: PoiInFrame[] = [];
  const ref = origin ?? getWorldOrigin(frames, pois);

  for (const poi of pois) {
    const res = project3D(drone, poi, yawOffset, ref);
    if (!res) continue;

    const halfW = 0.5 * marginFactor;
    const halfH = 0.5 * marginFactor;
    if (Math.abs(res.px - 0.5) > halfW || Math.abs(res.py - 0.5) > halfH) continue;

    result.push({
      poi,
      bearingDiff: res.h,
      elevationDiff: res.v,
      distance: res.dist,
      pixelX: res.px,
      pixelY: res.py,
    });
  }

  result.sort((a, b) => a.distance - b.distance);
  return { droneFrame: drone, pois: result };
}
