import { wingPool } from '../db/wingDb.js';
import { runBacktrackAnalysis } from './backtrackAnalysisService.js';

interface PredictionAnalysis {
  acdntSn: number;
  acdntNm: string;
  occurredAt: string;
  analysisDate: string;
  requestor: string;
  duration: string;
  oilType: string;
  volume: number | null;
  location: string;
  lat: number | null;
  lon: number | null;
  kospsStatus: string;
  poseidonStatus: string;
  opendriftStatus: string;
  backtrackStatus: string;
  analyst: string;
  officeName: string;
  acdntSttsCd: string;
  predRunSn: number | null;
  runDtm: string | null;
}

interface PredictionDetail {
  acdnt: {
    acdntSn: number;
    acdntNm: string;
    occurredAt: string;
    lat: number | null;
    lon: number | null;
    location: string;
    analyst: string;
    officeName: string;
  };
  spill: {
    oilType: string;
    volume: number | null;
    unit: string;
    fcstHr: number | null;
  } | null;
  vessels: Array<{
    vesselInfoSn: number;
    imoNo: string;
    vesselNm: string;
    vesselTp: string;
    loaM: number | null;
    breadthM: number | null;
    draftM: number | null;
    gt: number | null;
    dwt: number | null;
    builtYr: number | null;
    flagCd: string;
    callsign: string;
    engineDc: string;
    insuranceData: unknown;
  }>;
  weather: Array<{
    obsDtm: string;
    locNm: string;
    temp: string;
    weatherDc: string;
    wind: string;
    wave: string;
    humid: string;
    vis: string;
    sst: string;
  }>;
}

interface BacktrackResult {
  backtrackSn: number;
  acdntSn: number;
  estSpilDtm: string | null;
  anlysRange: string | null;
  lon: number | null;
  lat: number | null;
  srchRadiusNm: number | null;
  totalVessels: number | null;
  execSttsCd: string;
  rsltData: unknown;
  regDtm: string;
}

interface CreateBacktrackInput {
  acdntSn: number;
  lat: number;
  lon: number;
  estSpilDtm?: string;
  anlysRange?: string;
  srchRadiusNm?: number;
}

interface SaveBoomLineInput {
  acdntSn: number;
  boomNm: string;
  priorityOrd?: number;
  geojson: unknown;
  lengthM?: number;
  efficiencyPct?: number;
}

interface BoomLineItem {
  boomLineSn: number;
  acdntSn: number;
  boomNm: string;
  priorityOrd: number;
  geom: unknown;
  lengthM: number | null;
  efficiencyPct: number | null;
  sttsCd: string;
  regDtm: string;
}

interface ListAnalysesInput {
  search?: string;
  acdntSn?: number;
}

export async function listAnalyses(input: ListAnalysesInput): Promise<PredictionAnalysis[]> {
  const params: unknown[] = [];
  const conditions: string[] = ["A.USE_YN = 'Y'"];

  if (input.acdntSn) {
    params.push(input.acdntSn);
    conditions.push(`A.ACDNT_SN = $${params.length}`);
  }

  if (input.search) {
    params.push(`%${input.search}%`);
    conditions.push(`(A.ACDNT_NM ILIKE $${params.length} OR A.LOC_DC ILIKE $${params.length})`);
  }

  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';

  const sql = `
    SELECT
      A.ACDNT_SN,
      A.ACDNT_NM,
      A.ACDNT_STTS_CD,
      A.OCCRN_DTM,
      A.LAT,
      A.LNG,
      A.LOC_DC,
      A.ANALYST_NM,
      A.OFFICE_NM,
      A.REGION_NM,
      S.OIL_TP_CD,
      S.SPIL_QTY,
      S.SPIL_UNIT_CD,
      S.FCST_HR,
      P.PRED_RUN_SN,
      P.RUN_DTM,
      P.KOSPS_STATUS,
      P.POSEIDON_STATUS,
      P.OPENDRIFT_STATUS,
      B.BACKTRACK_STATUS,
      COALESCE(U.USER_NM, A.ANALYST_NM) AS RESOLVED_ANALYST,
      COALESCE(O.ORG_NM, A.OFFICE_NM) AS RESOLVED_OFFICE
    FROM ACDNT A
    INNER JOIN (
      SELECT
        ACDNT_SN,
        PRED_RUN_SN,
        MIN(BGNG_DTM) AS RUN_DTM,
        MIN(SPIL_DATA_SN) AS SPIL_DATA_SN,
        MIN(EXEC_USER_ID::TEXT)::UUID AS EXEC_USER_ID,
        MAX(CASE WHEN ALGO_CD = 'KOSPS' THEN EXEC_STTS_CD END) AS KOSPS_STATUS,
        MAX(CASE WHEN ALGO_CD = 'POSEIDON' THEN EXEC_STTS_CD END) AS POSEIDON_STATUS,
        MAX(CASE WHEN ALGO_CD = 'OPENDRIFT' THEN EXEC_STTS_CD END) AS OPENDRIFT_STATUS
      FROM PRED_EXEC
      GROUP BY ACDNT_SN, PRED_RUN_SN
    ) P ON P.ACDNT_SN = A.ACDNT_SN
    LEFT JOIN SPIL_DATA S ON S.SPIL_DATA_SN = P.SPIL_DATA_SN
    LEFT JOIN AUTH_USER U ON U.USER_ID = P.EXEC_USER_ID
    LEFT JOIN AUTH_ORG O ON O.ORG_SN = U.ORG_SN
    LEFT JOIN (
      SELECT
        ACDNT_SN,
        MAX(CASE WHEN B.EXEC_STTS_CD IS NOT NULL THEN B.EXEC_STTS_CD ELSE 'pending' END) AS BACKTRACK_STATUS
      FROM BACKTRACK B
      GROUP BY ACDNT_SN
    ) B ON B.ACDNT_SN = A.ACDNT_SN
    ${whereClause}
    ORDER BY P.RUN_DTM DESC NULLS LAST, A.OCCRN_DTM DESC
  `;

  const { rows } = await wingPool.query(sql, params);

  return rows.map((row: Record<string, unknown>) => ({
    acdntSn: Number(row['acdnt_sn']),
    acdntNm: String(row['acdnt_nm'] ?? ''),
    occurredAt: row['occrn_dtm'] ? String(row['occrn_dtm']) : '',
    analysisDate: row['occrn_dtm'] ? String(row['occrn_dtm']) : '',
    requestor: String(row['analyst_nm'] ?? ''),
    duration: row['fcst_hr'] != null ? `${row['fcst_hr']}hr` : '',
    oilType: String(row['oil_tp_cd'] ?? ''),
    volume: row['spil_qty'] != null ? parseFloat(String(row['spil_qty'])) : null,
    location: String(row['loc_dc'] ?? ''),
    lat: row['lat'] != null ? parseFloat(String(row['lat'])) : null,
    lon: row['lng'] != null ? parseFloat(String(row['lng'])) : null,
    kospsStatus: String(row['kosps_status'] ?? 'pending').toLowerCase(),
    poseidonStatus: String(row['poseidon_status'] ?? 'pending').toLowerCase(),
    opendriftStatus: String(row['opendrift_status'] ?? 'pending').toLowerCase(),
    backtrackStatus: String(row['backtrack_status'] ?? 'pending').toLowerCase(),
    analyst: String(row['resolved_analyst'] ?? ''),
    officeName: String(row['resolved_office'] ?? ''),
    acdntSttsCd: String(row['acdnt_stts_cd'] ?? 'ACTIVE'),
    predRunSn: row['pred_run_sn'] != null ? Number(row['pred_run_sn']) : null,
    runDtm: row['run_dtm'] ? String(row['run_dtm']) : null,
  }));
}

export async function getAnalysisDetail(acdntSn: number): Promise<PredictionDetail | null> {
  const acdntSql = `
    SELECT
      A.ACDNT_SN,
      A.ACDNT_NM,
      A.OCCRN_DTM,
      A.LAT,
      A.LNG,
      A.LOC_DC,
      A.ANALYST_NM,
      A.OFFICE_NM
    FROM ACDNT A
    WHERE A.ACDNT_SN = $1
      AND A.USE_YN = 'Y'
  `;
  const { rows: acdntRows } = await wingPool.query(acdntSql, [acdntSn]);
  if (acdntRows.length === 0) return null;

  const a = acdntRows[0] as Record<string, unknown>;

  const spillSql = `
    SELECT
      OIL_TP_CD,
      SPIL_QTY,
      SPIL_UNIT_CD,
      FCST_HR
    FROM SPIL_DATA
    WHERE ACDNT_SN = $1
    ORDER BY SPIL_DATA_SN ASC
    LIMIT 1
  `;
  const { rows: spillRows } = await wingPool.query(spillSql, [acdntSn]);

  const vesselSql = `
    SELECT
      VESSEL_INFO_SN,
      IMO_NO,
      VESSEL_NM,
      VESSEL_TP,
      LOA_M,
      BREADTH_M,
      DRAFT_M,
      GT,
      DWT,
      BUILT_YR,
      FLAG_CD,
      CALLSIGN,
      ENGINE_DC,
      INSURANCE_DATA
    FROM VESSEL_INFO
    WHERE ACDNT_SN = $1
    ORDER BY VESSEL_INFO_SN ASC
  `;
  const { rows: vesselRows } = await wingPool.query(vesselSql, [acdntSn]);

  const weatherSql = `
    SELECT
      OBS_DTM,
      LOC_NM,
      TEMP,
      WEATHER_DC,
      WIND,
      WAVE,
      HUMID,
      VIS,
      SST
    FROM ACDNT_WEATHER
    WHERE ACDNT_SN = $1
    ORDER BY OBS_DTM ASC
  `;
  const { rows: weatherRows } = await wingPool.query(weatherSql, [acdntSn]);

  const spill =
    spillRows.length > 0
      ? (() => {
          const s = spillRows[0] as Record<string, unknown>;
          return {
            oilType: String(s['oil_tp_cd'] ?? ''),
            volume: s['spil_qty'] != null ? parseFloat(String(s['spil_qty'])) : null,
            unit: String(s['spil_unit_cd'] ?? ''),
            fcstHr: s['fcst_hr'] != null ? parseFloat(String(s['fcst_hr'])) : null,
          };
        })()
      : null;

  const vessels = vesselRows.map((v: Record<string, unknown>) => ({
    vesselInfoSn: Number(v['vessel_info_sn']),
    imoNo: String(v['imo_no'] ?? ''),
    vesselNm: String(v['vessel_nm'] ?? ''),
    vesselTp: String(v['vessel_tp'] ?? ''),
    loaM: v['loa_m'] != null ? parseFloat(String(v['loa_m'])) : null,
    breadthM: v['breadth_m'] != null ? parseFloat(String(v['breadth_m'])) : null,
    draftM: v['draft_m'] != null ? parseFloat(String(v['draft_m'])) : null,
    gt: v['gt'] != null ? parseFloat(String(v['gt'])) : null,
    dwt: v['dwt'] != null ? parseFloat(String(v['dwt'])) : null,
    builtYr: v['built_yr'] != null ? Number(v['built_yr']) : null,
    flagCd: String(v['flag_cd'] ?? ''),
    callsign: String(v['callsign'] ?? ''),
    engineDc: String(v['engine_dc'] ?? ''),
    insuranceData: v['insurance_data'] ?? null,
  }));

  const weather = weatherRows.map((w: Record<string, unknown>) => ({
    obsDtm: w['obs_dtm'] ? String(w['obs_dtm']) : '',
    locNm: String(w['loc_nm'] ?? ''),
    temp: String(w['temp'] ?? ''),
    weatherDc: String(w['weather_dc'] ?? ''),
    wind: String(w['wind'] ?? ''),
    wave: String(w['wave'] ?? ''),
    humid: String(w['humid'] ?? ''),
    vis: String(w['vis'] ?? ''),
    sst: String(w['sst'] ?? ''),
  }));

  return {
    acdnt: {
      acdntSn: Number(a['acdnt_sn']),
      acdntNm: String(a['acdnt_nm'] ?? ''),
      occurredAt: a['occrn_dtm'] ? String(a['occrn_dtm']) : '',
      lat: a['lat'] != null ? parseFloat(String(a['lat'])) : null,
      lon: a['lng'] != null ? parseFloat(String(a['lng'])) : null,
      location: String(a['loc_dc'] ?? ''),
      analyst: String(a['analyst_nm'] ?? ''),
      officeName: String(a['office_nm'] ?? ''),
    },
    spill,
    vessels,
    weather,
  };
}

export async function getBacktrack(sn: number): Promise<BacktrackResult | null> {
  const sql = `
    SELECT BACKTRACK_SN, ACDNT_SN, EST_SPIL_DTM, ANLYS_RANGE,
           LON, LAT, SRCH_RADIUS_NM, TOTAL_VESSELS,
           EXEC_STTS_CD, RSLT_DATA, REG_DTM
    FROM BACKTRACK
    WHERE BACKTRACK_SN = $1 AND USE_YN = 'Y'
  `;
  const { rows } = await wingPool.query(sql, [sn]);
  if (rows.length === 0) return null;
  return rowToBacktrack(rows[0] as Record<string, unknown>);
}

export async function listBacktracksByAcdnt(acdntSn: number): Promise<BacktrackResult[]> {
  const sql = `
    SELECT BACKTRACK_SN, ACDNT_SN, EST_SPIL_DTM, ANLYS_RANGE,
           LON, LAT, SRCH_RADIUS_NM, TOTAL_VESSELS,
           EXEC_STTS_CD, RSLT_DATA, REG_DTM
    FROM BACKTRACK
    WHERE ACDNT_SN = $1 AND USE_YN = 'Y'
    ORDER BY REG_DTM DESC
  `;
  const { rows } = await wingPool.query(sql, [acdntSn]);
  return rows.map((r: Record<string, unknown>) => rowToBacktrack(r));
}

function rowToBacktrack(r: Record<string, unknown>): BacktrackResult {
  return {
    backtrackSn: Number(r['backtrack_sn']),
    acdntSn: Number(r['acdnt_sn']),
    estSpilDtm: r['est_spil_dtm'] ? new Date(r['est_spil_dtm'] as string | Date).toISOString() : null,
    anlysRange: r['anlys_range'] ? String(r['anlys_range']) : null,
    lon: r['lon'] != null ? parseFloat(String(r['lon'])) : null,
    lat: r['lat'] != null ? parseFloat(String(r['lat'])) : null,
    srchRadiusNm: r['srch_radius_nm'] != null ? parseFloat(String(r['srch_radius_nm'])) : null,
    totalVessels: r['total_vessels'] != null ? Number(r['total_vessels']) : null,
    execSttsCd: String(r['exec_stts_cd'] ?? ''),
    rsltData: r['rslt_data'] ?? null,
    regDtm: String(r['reg_dtm'] ?? ''),
  };
}

export async function createBacktrack(
  input: CreateBacktrackInput,
): Promise<BacktrackResult> {
  const { acdntSn, lat, lon, estSpilDtm, anlysRange, srchRadiusNm } = input;

  const sql = `
    INSERT INTO BACKTRACK (ACDNT_SN, LAT, LON, GEOM, LOC_DC, EST_SPIL_DTM, ANLYS_RANGE, SRCH_RADIUS_NM, EXEC_STTS_CD)
    VALUES (
      $1, $2::double precision, $3::double precision,
      ST_SetSRID(ST_MakePoint($3::double precision, $2::double precision), 4326),
      $3::text || ' + ' || $2::text,
      $4, $5, $6, 'PENDING'
    )
    RETURNING BACKTRACK_SN
  `;

  const { rows } = await wingPool.query(sql, [
    acdntSn, lat, lon,
    estSpilDtm || null, anlysRange || null, srchRadiusNm || null,
  ]);
  const backtrackSn = Number((rows[0] as Record<string, unknown>)['backtrack_sn']);

  // 동기 분석 (완료까지 대기 후 결과 반환)
  await runBacktrackAnalysis(backtrackSn);

  const result = await getBacktrack(backtrackSn);
  if (!result) throw new Error('역추적 결과를 찾을 수 없습니다');
  return result;
}

export async function saveBoomLine(input: SaveBoomLineInput): Promise<{ boomLineSn: number }> {
  const { acdntSn, boomNm, priorityOrd = 0, geojson, lengthM, efficiencyPct } = input;

  const sql = `
    INSERT INTO BOOM_LINE (ACDNT_SN, BOOM_NM, PRIORITY_ORD, GEOM, LENGTH_M, EFFICIENCY_PCT)
    VALUES ($1, $2, $3, ST_GeomFromGeoJSON($4), $5, $6)
    RETURNING BOOM_LINE_SN
  `;

  const { rows } = await wingPool.query(sql, [
    acdntSn, boomNm, priorityOrd,
    JSON.stringify(geojson),
    lengthM || null, efficiencyPct || null,
  ]);

  return { boomLineSn: Number((rows[0] as Record<string, unknown>)['boom_line_sn']) };
}

interface TrajectoryParticle {
  lat: number;
  lon: number;
  stranded?: 0 | 1;
}

interface TrajectoryWindPoint {
  lat: number;
  lon: number;
  wind_speed: number;
  wind_direction: number;
}

interface TrajectoryHydrGrid {
  lonInterval: number[];
  boundLonLat: { top: number; bottom: number; left: number; right: number };
  rows: number;
  cols: number;
  latInterval: number[];
}

interface TrajectoryTimeStep {
  particles: TrajectoryParticle[];
  remaining_volume_m3: number;
  weathered_volume_m3: number;
  evaporation_volume_m3?: number;
  dispersion_volume_m3?: number;
  pollution_area_km2: number;
  beached_volume_m3: number;
  pollution_coast_length_m: number;
  center_lat?: number;
  center_lon?: number;
  wind_data?: TrajectoryWindPoint[];
  hydr_data?: [number[][], number[][]];
  hydr_grid?: TrajectoryHydrGrid;
}

// ALGO_CD → 프론트엔드 모델명 매핑
const ALGO_CD_TO_MODEL: Record<string, string> = {
  'OPENDRIFT': 'OpenDrift',
  'POSEIDON':  'POSEIDON',
};

interface SingleModelTrajectoryResult {
  trajectory: Array<{ lat: number; lon: number; time: number; particle: number; stranded?: 0 | 1; model: string }>;
  summary: {
    remainingVolume: number;
    weatheredVolume: number;
    evaporationVolume: number;
    dispersionVolume: number;
    pollutionArea: number;
    beachedVolume: number;
    pollutionCoastLength: number;
  };
  stepSummaries: Array<{
    remainingVolume: number;
    weatheredVolume: number;
    evaporationVolume: number;
    dispersionVolume: number;
    pollutionArea: number;
    beachedVolume: number;
    pollutionCoastLength: number;
  }>;
  centerPoints: Array<{ lat: number; lon: number; time: number; model: string }>;
  windData: TrajectoryWindPoint[][];
  hydrData: ({ value: [number[][], number[][]]; grid: TrajectoryHydrGrid } | null)[];
}

interface TrajectoryResult {
  trajectory: Array<{ lat: number; lon: number; time: number; particle: number; stranded?: 0 | 1; model: string }>;
  summary: {
    remainingVolume: number;
    weatheredVolume: number;
    evaporationVolume: number;
    dispersionVolume: number;
    pollutionArea: number;
    beachedVolume: number;
    pollutionCoastLength: number;
  };
  centerPoints: Array<{ lat: number; lon: number; time: number; model: string }>;
  windDataByModel: Record<string, TrajectoryWindPoint[][]>;
  hydrDataByModel: Record<string, ({ value: [number[][], number[][]]; grid: TrajectoryHydrGrid } | null)[]>;
  summaryByModel: Record<string, SingleModelTrajectoryResult['summary']>;
  stepSummariesByModel: Record<string, SingleModelTrajectoryResult['stepSummaries']>;
}

function transformTrajectoryResult(rawResult: TrajectoryTimeStep[], model: string): SingleModelTrajectoryResult {
  const trajectory = rawResult.flatMap((step, stepIdx) =>
    step.particles.map((p, i) => ({
      lat: p.lat,
      lon: p.lon,
      time: stepIdx,
      particle: i,
      stranded: p.stranded,
      model,
    }))
  );
  const lastStep = rawResult[rawResult.length - 1];
  const summary = {
    remainingVolume: lastStep.remaining_volume_m3,
    weatheredVolume: lastStep.weathered_volume_m3,
    evaporationVolume: lastStep.evaporation_volume_m3 ?? lastStep.weathered_volume_m3 * 0.65,
    dispersionVolume: lastStep.dispersion_volume_m3 ?? lastStep.weathered_volume_m3 * 0.35,
    pollutionArea: lastStep.pollution_area_km2,
    beachedVolume: lastStep.beached_volume_m3,
    pollutionCoastLength: lastStep.pollution_coast_length_m,
  };
  const centerPoints = rawResult
    .map((step, stepIdx) =>
      step.center_lat != null && step.center_lon != null
        ? { lat: step.center_lat, lon: step.center_lon, time: stepIdx, model }
        : null
    )
    .filter((p): p is { lat: number; lon: number; time: number; model: string } => p !== null);
  const stepSummaries = rawResult.map((step) => ({
    remainingVolume: step.remaining_volume_m3,
    weatheredVolume: step.weathered_volume_m3,
    evaporationVolume: step.evaporation_volume_m3 ?? step.weathered_volume_m3 * 0.65,
    dispersionVolume: step.dispersion_volume_m3 ?? step.weathered_volume_m3 * 0.35,
    pollutionArea: step.pollution_area_km2,
    beachedVolume: step.beached_volume_m3,
    pollutionCoastLength: step.pollution_coast_length_m,
  }));
  const windData = rawResult.map((step) => step.wind_data ?? []);
  const hydrData = rawResult.map((step) =>
    step.hydr_data && step.hydr_grid
      ? { value: step.hydr_data, grid: step.hydr_grid }
      : null
  );
  return { trajectory, summary, stepSummaries, centerPoints, windData, hydrData };
}

export async function getAnalysisTrajectory(acdntSn: number, predRunSn?: number): Promise<TrajectoryResult | null> {
  // 완료된 모든 모델(OPENDRIFT, POSEIDON) 결과 조회
  // predRunSn이 있으면 해당 실행의 결과만, 없으면 최신 결과
  const sql = predRunSn != null
    ? `
      SELECT ALGO_CD, RSLT_DATA, CMPL_DTM FROM wing.PRED_EXEC
      WHERE ACDNT_SN = $1
        AND PRED_RUN_SN = $2
        AND ALGO_CD IN ('OPENDRIFT', 'POSEIDON')
        AND EXEC_STTS_CD = 'COMPLETED'
      ORDER BY CMPL_DTM DESC
    `
    : `
      SELECT ALGO_CD, RSLT_DATA, CMPL_DTM FROM wing.PRED_EXEC
      WHERE ACDNT_SN = $1
        AND ALGO_CD IN ('OPENDRIFT', 'POSEIDON')
        AND EXEC_STTS_CD = 'COMPLETED'
      ORDER BY CMPL_DTM DESC
    `;
  const params = predRunSn != null ? [acdntSn, predRunSn] : [acdntSn];
  const { rows } = await wingPool.query(sql, params);
  if (rows.length === 0) return null;

  // 모든 모델의 파티클을 하나의 배열로 병합
  let mergedTrajectory: TrajectoryResult['trajectory'] = [];
  let allCenterPoints: TrajectoryResult['centerPoints'] = [];

  // summary: 가장 최근 완료된 OpenDrift 기준, 없으면 POSEIDON 기준
  let baseResult: SingleModelTrajectoryResult | null = null;
  const windDataByModel: Record<string, TrajectoryWindPoint[][]> = {};
  const hydrDataByModel: Record<string, ({ value: [number[][], number[][]]; grid: TrajectoryHydrGrid } | null)[]> = {};
  const summaryByModel: Record<string, SingleModelTrajectoryResult['summary']> = {};
  const stepSummariesByModel: Record<string, SingleModelTrajectoryResult['stepSummaries']> = {};

  // OpenDrift 우선, 없으면 POSEIDON 선택 (ORDER BY CMPL_DTM DESC이므로 첫 번째 행이 가장 최근)
  const opendriftRow = (rows as Array<Record<string, unknown>>).find((r) => r['algo_cd'] === 'OPENDRIFT');
  const poseidonRow = (rows as Array<Record<string, unknown>>).find((r) => r['algo_cd'] === 'POSEIDON');
  const baseRow = opendriftRow ?? poseidonRow ?? null;

  for (const row of rows as Array<Record<string, unknown>>) {
    if (!row['rslt_data']) continue;
    const algoCd = String(row['algo_cd'] ?? '');
    const modelName = ALGO_CD_TO_MODEL[algoCd] ?? algoCd;
    const parsed = transformTrajectoryResult(row['rslt_data'] as TrajectoryTimeStep[], modelName);
    mergedTrajectory = mergedTrajectory.concat(parsed.trajectory);
    allCenterPoints = allCenterPoints.concat(parsed.centerPoints);
    windDataByModel[modelName] = parsed.windData;
    hydrDataByModel[modelName] = parsed.hydrData;
    summaryByModel[modelName] = parsed.summary;
    stepSummariesByModel[modelName] = parsed.stepSummaries;

    if (row === baseRow) {
      baseResult = parsed;
    }
  }

  if (!baseResult) return null;

  return {
    trajectory: mergedTrajectory,
    summary: baseResult.summary,
    centerPoints: allCenterPoints,
    windDataByModel,
    hydrDataByModel,
    summaryByModel,
    stepSummariesByModel,
  };
}

export async function getSensitiveResourcesByAcdntSn(
  acdntSn: number,
): Promise<{ category: string; count: number; totalArea: number | null }[]> {
  const sql = `
    WITH all_wkts AS (
      SELECT step_data ->> 'wkt' AS wkt
      FROM wing.PRED_EXEC,
           jsonb_array_elements(RSLT_DATA) AS step_data
      WHERE ACDNT_SN = $1
        AND ALGO_CD IN ('OPENDRIFT', 'POSEIDON')
        AND EXEC_STTS_CD = 'COMPLETED'
        AND RSLT_DATA IS NOT NULL
    ),
    union_geom AS (
      SELECT ST_Union(ST_GeomFromText(wkt, 4326)) AS geom
      FROM all_wkts
      WHERE wkt IS NOT NULL AND wkt <> ''
    )
    SELECT sr.CATEGORY,
      COUNT(*)::int AS count,
      CASE
        WHEN bool_and(sr.PROPERTIES ? 'area')
        THEN SUM((sr.PROPERTIES->>'area')::float)
        ELSE NULL
      END AS total_area
    FROM wing.SENSITIVE_RESOURCE sr, union_geom
    WHERE union_geom.geom IS NOT NULL
      AND ST_Intersects(sr.GEOM, union_geom.geom)
    GROUP BY sr.CATEGORY
    ORDER BY sr.CATEGORY
  `;
  const { rows } = await wingPool.query(sql, [acdntSn]);
  return rows.map((r: Record<string, unknown>) => ({
    category: String(r['category'] ?? ''),
    count: Number(r['count'] ?? 0),
    totalArea: r['total_area'] != null ? Number(r['total_area']) : null,
  }));
}

export async function getSensitiveResourcesGeoJsonByAcdntSn(
  acdntSn: number,
): Promise<{ type: 'FeatureCollection'; features: unknown[] }> {
  const sql = `
    WITH all_wkts AS (
      SELECT step_data ->> 'wkt' AS wkt
      FROM wing.PRED_EXEC,
           jsonb_array_elements(RSLT_DATA) AS step_data
      WHERE ACDNT_SN = $1
        AND ALGO_CD IN ('OPENDRIFT', 'POSEIDON')
        AND EXEC_STTS_CD = 'COMPLETED'
        AND RSLT_DATA IS NOT NULL
    ),
    union_geom AS (
      SELECT ST_Union(ST_GeomFromText(wkt, 4326)) AS geom
      FROM all_wkts
      WHERE wkt IS NOT NULL AND wkt <> ''
    )
    SELECT sr.SR_ID, sr.CATEGORY, sr.PROPERTIES,
           ST_AsGeoJSON(sr.GEOM)::jsonb AS geom_json
    FROM wing.SENSITIVE_RESOURCE sr, union_geom
    WHERE union_geom.geom IS NOT NULL
      AND ST_Intersects(sr.GEOM, union_geom.geom)
    ORDER BY sr.CATEGORY, sr.SR_ID
  `;
  const { rows } = await wingPool.query(sql, [acdntSn]);
  const features = rows.map((r: Record<string, unknown>) => ({
    type: 'Feature',
    geometry: r['geom_json'],
    properties: {
      srId: Number(r['sr_id']),
      category: String(r['category'] ?? ''),
      ...(r['properties'] as Record<string, unknown> ?? {}),
    },
  }));
  return { type: 'FeatureCollection', features };
}

export async function getSensitivityEvaluationGeojsonByAcdntSn(
  acdntSn: number,
): Promise<{ type: 'FeatureCollection'; features: unknown[] }> {
  const acdntSql = `SELECT LAT, LNG FROM wing.ACDNT WHERE ACDNT_SN = $1 AND USE_YN = 'Y'`;
  const { rows: acdntRows } = await wingPool.query(acdntSql, [acdntSn]);
  if (acdntRows.length === 0 || acdntRows[0]['lat'] == null) return { type: 'FeatureCollection', features: [] };
  const lat = Number(acdntRows[0]['lat']);
  const lng = Number(acdntRows[0]['lng']);

  const sql = `
    SELECT SR_ID, PROPERTIES,
           ST_AsGeoJSON(GEOM)::jsonb AS geom_json,
           ST_Area(GEOM::geography) / 1000000.0 AS area_km2
    FROM wing.SENSITIVE_EVALUATION
    WHERE ST_DWithin(
      GEOM::geography,
      ST_SetSRID(ST_MakePoint($2, $1), 4326)::geography,
      10000
    )
    ORDER BY SR_ID
  `;
  const { rows } = await wingPool.query(sql, [lat, lng]);
  const features = rows.map((r: Record<string, unknown>) => ({
    type: 'Feature',
    geometry: r['geom_json'],
    properties: {
      srId: Number(r['sr_id']),
      area_km2: Number(r['area_km2']),
      ...(r['properties'] as Record<string, unknown> ?? {}),
    },
  }));
  return { type: 'FeatureCollection', features };
}

export async function getPredictionParticlesGeojsonByAcdntSn(
  acdntSn: number,
): Promise<{ type: 'FeatureCollection'; features: unknown[]; maxStep: number }> {
  const sql = `
    SELECT ALGO_CD, RSLT_DATA
    FROM wing.PRED_EXEC
    WHERE ACDNT_SN = $1
      AND ALGO_CD IN ('OPENDRIFT', 'POSEIDON')
      AND EXEC_STTS_CD = 'COMPLETED'
      AND RSLT_DATA IS NOT NULL
  `;
  const { rows } = await wingPool.query(sql, [acdntSn]);
  if (rows.length === 0) return { type: 'FeatureCollection', features: [], maxStep: 0 };

  const ALGO_TO_MODEL: Record<string, string> = { OPENDRIFT: 'OpenDrift', POSEIDON: 'POSEIDON' };
  const features: unknown[] = [];
  let globalMaxStep = 0;

  for (const row of rows) {
    const model = ALGO_TO_MODEL[String(row['algo_cd'])] ?? String(row['algo_cd']);
    const steps = row['rslt_data'] as TrajectoryTimeStep[];
    const maxStep = steps.length - 1;
    if (maxStep > globalMaxStep) globalMaxStep = maxStep;

    steps.forEach((step, stepIdx) => {
      step.particles.forEach(p => {
        features.push({
          type: 'Feature',
          geometry: { type: 'Point', coordinates: [p.lon, p.lat] },
          properties: {
            model,
            time: stepIdx,
            stranded: p.stranded ?? 0,
            isLastStep: stepIdx === maxStep,
          },
        });
      });
    });
  }

  return { type: 'FeatureCollection', features, maxStep: globalMaxStep };
}

export async function listBoomLines(acdntSn: number): Promise<BoomLineItem[]> {
  const sql = `
    SELECT BOOM_LINE_SN, ACDNT_SN, BOOM_NM, PRIORITY_ORD,
           ST_AsGeoJSON(GEOM) AS GEOM, LENGTH_M, EFFICIENCY_PCT, STTS_CD, REG_DTM
    FROM BOOM_LINE
    WHERE ACDNT_SN = $1 AND USE_YN = 'Y'
    ORDER BY PRIORITY_ORD ASC
  `;

  const { rows } = await wingPool.query(sql, [acdntSn]);

  return rows.map((r: Record<string, unknown>) => ({
    boomLineSn: Number(r['boom_line_sn']),
    acdntSn: Number(r['acdnt_sn']),
    boomNm: String(r['boom_nm'] ?? ''),
    priorityOrd: Number(r['priority_ord'] ?? 0),
    geom: r['geom'] != null ? JSON.parse(String(r['geom'])) : null,
    lengthM: r['length_m'] != null ? parseFloat(String(r['length_m'])) : null,
    efficiencyPct: r['efficiency_pct'] != null ? parseFloat(String(r['efficiency_pct'])) : null,
    sttsCd: String(r['stts_cd'] ?? 'PLANNED'),
    regDtm: String(r['reg_dtm'] ?? ''),
  }));
}
