import { wingPool } from '../db/wingDb.js';
import { spawn, type ChildProcess } from 'child_process';
import { existsSync, mkdirSync, rmSync } from 'fs';
import { execSync } from 'child_process';
import path from 'path';

// ============================================================
// AERIAL_MEDIA
// ============================================================

interface AerialMediaItem {
  aerialMediaSn: number;
  acdntSn: number | null;
  fileNm: string;
  orgnlNm: string | null;
  filePath: string | null;
  lon: number | null;
  lat: number | null;
  locDc: string | null;
  equipTpCd: string | null;
  equipNm: string | null;
  mediaTpCd: string | null;
  takngDtm: string | null;
  fileSz: string | null;
  resolution: string | null;
  regDtm: string;
}

interface ListMediaInput {
  equipType?: string;
  mediaType?: string;
  acdntSn?: number;
  search?: string;
}

function rowToMedia(r: Record<string, unknown>): AerialMediaItem {
  return {
    aerialMediaSn: r.aerial_media_sn as number,
    acdntSn: r.acdnt_sn as number | null,
    fileNm: r.file_nm as string,
    orgnlNm: r.orgnl_nm as string | null,
    filePath: r.file_path as string | null,
    lon: r.lon ? parseFloat(r.lon as string) : null,
    lat: r.lat ? parseFloat(r.lat as string) : null,
    locDc: r.loc_dc as string | null,
    equipTpCd: r.equip_tp_cd as string | null,
    equipNm: r.equip_nm as string | null,
    mediaTpCd: r.media_tp_cd as string | null,
    takngDtm: r.takng_dtm as string | null,
    fileSz: r.file_sz as string | null,
    resolution: r.resolution as string | null,
    regDtm: r.reg_dtm as string,
  };
}

export async function getMediaBySn(sn: number): Promise<AerialMediaItem | null> {
  const { rows } = await wingPool.query(
    `SELECT AERIAL_MEDIA_SN, ACDNT_SN, FILE_NM, ORGNL_NM, FILE_PATH,
            LON, LAT, LOC_DC, EQUIP_TP_CD, EQUIP_NM, MEDIA_TP_CD,
            TAKNG_DTM, FILE_SZ, RESOLUTION, REG_DTM
     FROM wing.AERIAL_MEDIA WHERE AERIAL_MEDIA_SN = $1 AND USE_YN = 'Y'`,
    [sn]
  );
  return rows.length > 0 ? rowToMedia(rows[0]) : null;
}

export async function fetchOriginalImage(camTy: string, fileId: string): Promise<Buffer> {
  const res = await fetch(`${IMAGE_API_URL}/get-original-image/${camTy}/${fileId}`, {
    signal: AbortSignal.timeout(30_000),
  });
  if (!res.ok) throw new Error(`이미지 서버 응답: ${res.status}`);
  const base64 = await res.json() as string;
  return Buffer.from(base64, 'base64');
}

export async function listMedia(input: ListMediaInput): Promise<AerialMediaItem[]> {
  const conditions: string[] = ["USE_YN = 'Y'"];
  const params: (string | number)[] = [];
  let idx = 1;

  if (input.equipType) {
    conditions.push(`EQUIP_TP_CD = $${idx++}`);
    params.push(input.equipType);
  }
  if (input.mediaType) {
    conditions.push(`MEDIA_TP_CD = $${idx++}`);
    params.push(input.mediaType);
  }
  if (input.acdntSn) {
    conditions.push(`ACDNT_SN = $${idx++}`);
    params.push(input.acdntSn);
  }
  if (input.search) {
    conditions.push(`(FILE_NM ILIKE '%' || $${idx} || '%' OR EQUIP_NM ILIKE '%' || $${idx} || '%')`);
    params.push(input.search);
    idx++;
  }

  const { rows } = await wingPool.query(
    `SELECT AERIAL_MEDIA_SN, ACDNT_SN, FILE_NM, ORGNL_NM, FILE_PATH,
            LON, LAT, LOC_DC, EQUIP_TP_CD, EQUIP_NM, MEDIA_TP_CD,
            TAKNG_DTM, FILE_SZ, RESOLUTION, REG_DTM
     FROM AERIAL_MEDIA
     WHERE ${conditions.join(' AND ')}
     ORDER BY TAKNG_DTM DESC NULLS LAST`,
    params
  );

  return rows.map((r: Record<string, unknown>) => rowToMedia(r));
}

export async function createMedia(input: {
  acdntSn?: number;
  fileNm: string;
  orgnlNm?: string;
  filePath?: string;
  lon?: number;
  lat?: number;
  locDc?: string;
  equipTpCd?: string;
  equipNm?: string;
  mediaTpCd?: string;
  takngDtm?: string;
  fileSz?: string;
  resolution?: string;
}): Promise<{ aerialMediaSn: number }> {
  const { rows } = await wingPool.query(
    `INSERT INTO AERIAL_MEDIA (
      ACDNT_SN, FILE_NM, ORGNL_NM, FILE_PATH,
      LON, LAT,
      GEOM,
      LOC_DC, EQUIP_TP_CD, EQUIP_NM, MEDIA_TP_CD,
      TAKNG_DTM, FILE_SZ, RESOLUTION
    ) VALUES (
      $1, $2, $3, $4,
      $5::float8, $6::float8,
      CASE WHEN $5 IS NOT NULL AND $6 IS NOT NULL THEN ST_SetSRID(ST_MakePoint($5, $6), 4326) END,
      $7, $8, $9, $10,
      $11, $12, $13
    ) RETURNING AERIAL_MEDIA_SN`,
    [
      input.acdntSn || null,
      input.fileNm,
      input.orgnlNm || null,
      input.filePath || null,
      input.lon || null,
      input.lat || null,
      input.locDc || null,
      input.equipTpCd || null,
      input.equipNm || null,
      input.mediaTpCd || null,
      input.takngDtm || null,
      input.fileSz || null,
      input.resolution || null,
    ]
  );

  return { aerialMediaSn: rows[0].aerial_media_sn };
}

// ============================================================
// CCTV_CAMERA
// ============================================================

interface CctvCameraItem {
  cctvSn: number;
  cameraNm: string;
  regionNm: string | null;
  lon: number | null;
  lat: number | null;
  locDc: string | null;
  coordDc: string | null;
  sttsCd: string;
  ptzYn: string;
  sourceNm: string | null;
  streamUrl: string | null;
  regDtm: string;
}

interface ListCctvInput {
  region?: string;
  status?: string;
}

function rowToCctv(r: Record<string, unknown>): CctvCameraItem {
  return {
    cctvSn: r.cctv_sn as number,
    cameraNm: r.camera_nm as string,
    regionNm: r.region_nm as string | null,
    lon: r.lon ? parseFloat(r.lon as string) : null,
    lat: r.lat ? parseFloat(r.lat as string) : null,
    locDc: r.loc_dc as string | null,
    coordDc: r.coord_dc as string | null,
    sttsCd: r.stts_cd as string,
    ptzYn: r.ptz_yn as string,
    sourceNm: r.source_nm as string | null,
    streamUrl: r.stream_url as string | null,
    regDtm: r.reg_dtm as string,
  };
}

export async function listCctv(input: ListCctvInput): Promise<CctvCameraItem[]> {
  const conditions: string[] = ["USE_YN = 'Y'"];
  const params: string[] = [];
  let idx = 1;

  if (input.region) {
    conditions.push(`REGION_NM = $${idx++}`);
    params.push(input.region);
  }
  if (input.status) {
    conditions.push(`STTS_CD = $${idx++}`);
    params.push(input.status);
  }

  const { rows } = await wingPool.query(
    `SELECT CCTV_SN, CAMERA_NM, REGION_NM, LON, LAT,
            LOC_DC, COORD_DC, STTS_CD, PTZ_YN, SOURCE_NM, STREAM_URL, REG_DTM
     FROM CCTV_CAMERA
     WHERE ${conditions.join(' AND ')}
     ORDER BY REGION_NM, CAMERA_NM`,
    params
  );

  return rows.map((r: Record<string, unknown>) => rowToCctv(r));
}

// ============================================================
// SAT_REQUEST
// ============================================================

interface SatRequestItem {
  satReqSn: number;
  reqCd: string;
  acdntSn: number | null;
  lon: number | null;
  lat: number | null;
  zoneDc: string | null;
  coordDc: string | null;
  zoneAreaKm2: number | null;
  satNm: string | null;
  providerNm: string | null;
  resolution: string | null;
  purposeDc: string | null;
  reqstrNm: string | null;
  reqDtm: string | null;
  expectedRcvDtm: string | null;
  sttsCd: string;
  regDtm: string;
}

interface ListSatRequestsInput {
  status?: string;
}

function rowToSatRequest(r: Record<string, unknown>): SatRequestItem {
  return {
    satReqSn: r.sat_req_sn as number,
    reqCd: r.req_cd as string,
    acdntSn: r.acdnt_sn as number | null,
    lon: r.lon ? parseFloat(r.lon as string) : null,
    lat: r.lat ? parseFloat(r.lat as string) : null,
    zoneDc: r.zone_dc as string | null,
    coordDc: r.coord_dc as string | null,
    zoneAreaKm2: r.zone_area_km2 ? parseFloat(r.zone_area_km2 as string) : null,
    satNm: r.sat_nm as string | null,
    providerNm: r.provider_nm as string | null,
    resolution: r.resolution as string | null,
    purposeDc: r.purpose_dc as string | null,
    reqstrNm: r.reqstr_nm as string | null,
    reqDtm: r.req_dtm as string | null,
    expectedRcvDtm: r.expected_rcv_dtm as string | null,
    sttsCd: r.stts_cd as string,
    regDtm: r.reg_dtm as string,
  };
}

export async function listSatRequests(input: ListSatRequestsInput): Promise<SatRequestItem[]> {
  const conditions: string[] = ["USE_YN = 'Y'"];
  const params: string[] = [];
  let idx = 1;

  if (input.status) {
    conditions.push(`STTS_CD = $${idx++}`);
    params.push(input.status);
  }

  const { rows } = await wingPool.query(
    `SELECT SAT_REQ_SN, REQ_CD, ACDNT_SN, LON, LAT,
            ZONE_DC, COORD_DC, ZONE_AREA_KM2, SAT_NM, PROVIDER_NM,
            RESOLUTION, PURPOSE_DC, REQSTR_NM,
            REQ_DTM, EXPECTED_RCV_DTM, STTS_CD, REG_DTM
     FROM SAT_REQUEST
     WHERE ${conditions.join(' AND ')}
     ORDER BY REQ_DTM DESC NULLS LAST`,
    params
  );

  return rows.map((r: Record<string, unknown>) => rowToSatRequest(r));
}

export async function createSatRequest(input: {
  reqCd: string;
  acdntSn?: number;
  lon?: number;
  lat?: number;
  zoneDc?: string;
  coordDc?: string;
  zoneAreaKm2?: number;
  satNm?: string;
  providerNm?: string;
  resolution?: string;
  purposeDc?: string;
  reqstrNm?: string;
  reqDtm?: string;
  expectedRcvDtm?: string;
}): Promise<{ satReqSn: number }> {
  const { rows } = await wingPool.query(
    `INSERT INTO SAT_REQUEST (
      REQ_CD, ACDNT_SN, LON, LAT,
      GEOM,
      ZONE_DC, COORD_DC, ZONE_AREA_KM2,
      SAT_NM, PROVIDER_NM, RESOLUTION,
      PURPOSE_DC, REQSTR_NM, REQ_DTM, EXPECTED_RCV_DTM
    ) VALUES (
      $1, $2, $3, $4,
      CASE WHEN $3 IS NOT NULL AND $4 IS NOT NULL THEN ST_SetSRID(ST_MakePoint($3::float, $4::float), 4326) END,
      $5, $6, $7,
      $8, $9, $10,
      $11, $12, $13, $14
    ) RETURNING SAT_REQ_SN`,
    [
      input.reqCd,
      input.acdntSn || null,
      input.lon || null,
      input.lat || null,
      input.zoneDc || null,
      input.coordDc || null,
      input.zoneAreaKm2 || null,
      input.satNm || null,
      input.providerNm || null,
      input.resolution || null,
      input.purposeDc || null,
      input.reqstrNm || null,
      input.reqDtm || null,
      input.expectedRcvDtm || null,
    ]
  );

  return { satReqSn: rows[0].sat_req_sn };
}

const VALID_SAT_STATUSES = ['PENDING', 'SHOOTING', 'COMPLETED', 'CANCELLED'] as const;
type SatStatus = typeof VALID_SAT_STATUSES[number];

export function isValidSatStatus(value: string): value is SatStatus {
  return (VALID_SAT_STATUSES as readonly string[]).includes(value);
}

export async function updateSatRequestStatus(sn: number, sttsCd: string): Promise<void> {
  await wingPool.query(
    `UPDATE SAT_REQUEST SET STTS_CD = $1 WHERE SAT_REQ_SN = $2 AND USE_YN = 'Y'`,
    [sttsCd, sn]
  );
}

// ============================================================
// OIL INFERENCE (GPU 서버 프록시)
// ============================================================

const IMAGE_API_URL = process.env.IMAGE_API_URL ?? 'http://localhost:5001';
const OIL_INFERENCE_URL = process.env.OIL_INFERENCE_URL || 'http://localhost:8090';
const INFERENCE_TIMEOUT_MS = 10_000;

export interface OilInferenceRegion {
  classId: number;
  className: string;
  pixelCount: number;
  percentage: number;
  thicknessMm: number;
}

export interface OilInferenceResult {
  mask: string;       // base64 uint8 array (values 0-4)
  width: number;
  height: number;
  regions: OilInferenceRegion[];
}

/** 여러 이미지를 이미지 분석 서버의 /stitch 엔드포인트로 전송해 합성 JPEG를 반환한다. */
export async function stitchImages(
  files: Express.Multer.File[],
  fileId: string
): Promise<Buffer> {
  const form = new FormData();
  form.append('fileId', fileId);
  for (const f of files) {
    form.append('files', new Blob([f.buffer], { type: f.mimetype }), f.originalname);
  }
  const response = await fetch(`${IMAGE_API_URL}/stitch`, {
    method: 'POST',
    body: form,
    signal: AbortSignal.timeout(300_000),
  });
  if (!response.ok) {
    const detail = await response.text().catch(() => '');
    throw new Error(`stitch server responded ${response.status}: ${detail}`);
  }
  return Buffer.from(await response.arrayBuffer());
}

/** GPU 추론 서버에 이미지를 전송하고 세그멘테이션 결과를 반환한다. */
export async function requestOilInference(imageBase64: string): Promise<OilInferenceResult> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), INFERENCE_TIMEOUT_MS);

  try {
    const response = await fetch(`${OIL_INFERENCE_URL}/inference`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ image: imageBase64 }),
      signal: controller.signal,
    });

    if (!response.ok) {
      const detail = await response.text().catch(() => '');
      throw new Error(`Inference server responded ${response.status}: ${detail}`);
    }

    return await response.json() as OilInferenceResult;
  } finally {
    clearTimeout(timeout);
  }
}

/** GPU 추론 서버 헬스체크 */
export async function checkInferenceHealth(): Promise<{ status: string; device?: string }> {
  try {
    const response = await fetch(`${OIL_INFERENCE_URL}/health`, {
      signal: AbortSignal.timeout(3000),
    });
    if (!response.ok) throw new Error(`status ${response.status}`);
    return await response.json() as { status: string; device?: string };
  } catch {
    return { status: 'unavailable' };
  }
}

// ============================================================
// DRONE STREAM (RTSP → HLS via FFmpeg)
// ============================================================

export interface DroneStreamConfig {
  id: string;
  name: string;
  shipName: string;
  droneModel: string;
  ip: string;
  rtspUrl: string;
  region: string;
}

export interface DroneStreamStatus extends DroneStreamConfig {
  status: 'idle' | 'starting' | 'streaming' | 'error';
  hlsUrl: string | null;
  error: string | null;
}

const DRONE_STREAMS: DroneStreamConfig[] = [
  { id: 'busan-1501', name: '1501함 드론', shipName: '부산서 1501함', droneModel: 'DJI M300 RTK', ip: '10.26.7.213', rtspUrl: 'rtsp://10.26.7.213:554/stream0', region: '부산' },
  { id: 'incheon-3008', name: '3008함 드론', shipName: '인천서 3008함', droneModel: 'DJI M30T', ip: '10.26.5.21', rtspUrl: 'rtsp://10.26.5.21:554/stream0', region: '인천' },
  { id: 'mokpo-3015', name: '3015함 드론', shipName: '목포서 3015함', droneModel: 'DJI Mavic 3E', ip: '10.26.7.85', rtspUrl: 'rtsp://10.26.7.85:554/stream0', region: '목포' },
];

const HLS_OUTPUT_DIR = '/tmp/wing-drone-hls';
const activeProcesses = new Map<string, { process: ChildProcess; status: 'starting' | 'streaming' | 'error'; error: string | null }>();

function getHlsDir(id: string): string {
  return path.join(HLS_OUTPUT_DIR, id);
}

function checkFfmpeg(): boolean {
  try {
    execSync('which ffmpeg', { stdio: 'ignore' });
    return true;
  } catch {
    return false;
  }
}

export function listDroneStreams(): DroneStreamStatus[] {
  return DRONE_STREAMS.map(ds => {
    const active = activeProcesses.get(ds.id);
    return {
      ...ds,
      status: active?.status ?? 'idle',
      hlsUrl: active?.status === 'streaming' ? `/api/aerial/drone/hls/${ds.id}/stream.m3u8` : null,
      error: active?.error ?? null,
    };
  });
}

export function startDroneStream(id: string): { success: boolean; error?: string; hlsUrl?: string } {
  const config = DRONE_STREAMS.find(d => d.id === id);
  if (!config) return { success: false, error: '알 수 없는 드론 스트림 ID' };

  if (activeProcesses.has(id)) {
    const existing = activeProcesses.get(id)!;
    if (existing.status === 'streaming' || existing.status === 'starting') {
      return { success: true, hlsUrl: `/api/aerial/drone/hls/${id}/stream.m3u8` };
    }
  }

  if (!checkFfmpeg()) {
    return { success: false, error: 'FFmpeg가 설치되어 있지 않습니다. 서버에 FFmpeg를 설치하세요.' };
  }

  const hlsDir = getHlsDir(id);
  if (!existsSync(hlsDir)) {
    mkdirSync(hlsDir, { recursive: true });
  }

  const outputPath = path.join(hlsDir, 'stream.m3u8');

  const ffmpeg = spawn('ffmpeg', [
    '-rtsp_transport', 'tcp',
    '-i', config.rtspUrl,
    '-c:v', 'copy',
    '-c:a', 'aac',
    '-f', 'hls',
    '-hls_time', '2',
    '-hls_list_size', '5',
    '-hls_flags', 'delete_segments',
    '-y',
    outputPath,
  ], { stdio: ['ignore', 'pipe', 'pipe'] });

  const entry = { process: ffmpeg, status: 'starting' as const, error: null as string | null };
  activeProcesses.set(id, entry);

  // Monitor for m3u8 file creation to confirm streaming
  const checkInterval = setInterval(() => {
    if (existsSync(outputPath)) {
      const e = activeProcesses.get(id);
      if (e && e.status === 'starting') {
        e.status = 'streaming';
      }
      clearInterval(checkInterval);
    }
  }, 500);

  // Timeout after 15 seconds
  setTimeout(() => {
    clearInterval(checkInterval);
    const e = activeProcesses.get(id);
    if (e && e.status === 'starting') {
      e.status = 'error';
      e.error = 'RTSP 연결 시간 초과 — 내부망에서만 접속 가능합니다.';
      ffmpeg.kill('SIGTERM');
    }
  }, 15000);

  let stderrBuf = '';
  ffmpeg.stderr?.on('data', (chunk: Buffer) => {
    stderrBuf += chunk.toString();
  });

  ffmpeg.on('close', (code) => {
    clearInterval(checkInterval);
    const e = activeProcesses.get(id);
    if (e) {
      if (e.status !== 'error') {
        e.status = 'error';
        e.error = code !== 0
          ? `FFmpeg 종료 (코드: ${code})${stderrBuf.includes('Connection refused') ? ' — RTSP 연결 거부됨' : ''}`
          : '스트림 종료';
      }
    }
    console.log(`[drone] FFmpeg 종료 (${id}): code=${code}`);
  });

  ffmpeg.on('error', (err) => {
    clearInterval(checkInterval);
    const e = activeProcesses.get(id);
    if (e) {
      e.status = 'error';
      e.error = `FFmpeg 실행 오류: ${err.message}`;
    }
  });

  return { success: true, hlsUrl: `/api/aerial/drone/hls/${id}/stream.m3u8` };
}

export function stopDroneStream(id: string): { success: boolean } {
  const entry = activeProcesses.get(id);
  if (!entry) return { success: true };

  entry.process.kill('SIGTERM');
  activeProcesses.delete(id);

  // Cleanup HLS files
  const hlsDir = getHlsDir(id);
  try {
    if (existsSync(hlsDir)) {
      rmSync(hlsDir, { recursive: true, force: true });
    }
  } catch (err) {
    console.error(`[drone] HLS 디렉토리 정리 실패 (${id}):`, err);
  }

  return { success: true };
}

export function getHlsDirectory(id: string): string | null {
  const config = DRONE_STREAMS.find(d => d.id === id);
  if (!config) return null;
  const dir = getHlsDir(id);
  return existsSync(dir) ? dir : null;
}
