/**
 * Synology Surveillance Station helper.
 *
 * Mirrors the working Python tool we already use to pull commissioning
 * footage off the NAS. One call per request: log in → list/download → log out.
 *
 *   const { sid, did } = await synologyLogin()
 *   const recs        = await synologyListRecordings(sid, did, [2], from, to)
 *   await synologyDownloadRecording(sid, did, recs[0].id, '/tmp/foo.mp4')
 *   await synologyLogout(sid, did)
 */

import { createWriteStream } from 'node:fs'
import { Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'

type SynoConfig = {
  url: string
  user: string
  password: string
}

function getConfig(): SynoConfig {
  const config = useRuntimeConfig()
  const url = (config.synology?.url || '').toString().replace(/\/+$/, '')
  const user = (config.synology?.user || '').toString()
  const password = (config.synology?.password || '').toString()
  if (!url || !user || !password) {
    throw createError({
      statusCode: 500,
      statusMessage: 'Synology Surveillance Station is not configured (SYNOLOGY_URL / SYNOLOGY_USER / SYNOLOGY_PASSWORD).'
    })
  }
  return { url, user, password }
}

/** Build `${base}/webapi/entry.cgi?…` with the given params. */
function buildEntryUrl(base: string, params: Record<string, string | number>): string {
  const qs = new URLSearchParams()
  for (const [k, v] of Object.entries(params)) {
    qs.set(k, String(v))
  }
  return `${base}/webapi/entry.cgi?${qs.toString()}`
}

export type SynoSession = {
  sid: string
  did?: string
}

export async function synologyLogin(): Promise<SynoSession> {
  const { url, user, password } = getConfig()
  const target = buildEntryUrl(url, {
    api: 'SYNO.API.Auth',
    method: 'login',
    version: 7,
    account: user,
    passwd: password,
    session: 'SurveillanceStation',
    format: 'sid',
  })

  const res = await fetch(target)
  const json: any = await res.json().catch(() => null)
  if (!res.ok || !json?.success || !json?.data?.sid) {
    const code = json?.error?.code ?? res.status
    throw createError({
      statusCode: 502,
      statusMessage: `Synology login failed (code ${code}).`
    })
  }
  return { sid: json.data.sid, did: json.data.did }
}

export async function synologyLogout(session: SynoSession): Promise<void> {
  try {
    const { url } = getConfig()
    const target = buildEntryUrl(url, {
      api: 'SYNO.API.Auth',
      method: 'logout',
      version: 7,
      _sid: session.sid,
      session: 'SurveillanceStation',
    })
    await fetch(target).catch(() => null)
  } catch {
    // Best-effort logout; ignore failures.
  }
}

export type SynoRecording = {
  id: number | string
  cameraId: number | string | null
  startTime: number | null
  filePath: string | null
  raw: any
}

/**
 * On some DSM/Surveillance Station builds the List response does NOT
 * include a startTime field at all — instead the recording's start UTC
 * timestamp is embedded as a 13-digit millisecond number in `filePath`,
 * e.g. `Kommissionierung-1-20260503-002000-1777760400027-7.mp4`.
 *
 * Returns the start time in UTC seconds, or null if it can't be parsed.
 */
function parseStartFromFilePath(filePath?: string | null): number | null {
  if (!filePath) return null
  const m = /([0-9]{13})-[0-9]+\.mp4$/.exec(filePath)
  if (!m) return null
  const ms = Number(m[1])
  if (!Number.isFinite(ms) || ms <= 0) return null
  return Math.floor(ms / 1000)
}

/**
 * List recordings overlapping the [from, to] UNIX-timestamp window for the
 * given camera ids. Tries the API parameter formats most SS builds accept,
 * falling back to an unfiltered query + client-side filter.
 */
export async function synologyListRecordings(
  session: SynoSession,
  cameraIds: number[],
  fromTs: number,
  toTs: number,
  apiVersion: number = 6
): Promise<SynoRecording[]> {
  const { url } = getConfig()
  const camCsv = cameraIds.join(',')
  const camBracket = `[${camCsv}]`

  const attempts: Array<Record<string, string | number>> = [
    { cameraIds: camCsv },
    { cameraIds: camBracket },
    {}, // last resort: unfiltered
  ]

  let lastError: any = null
  for (const extra of attempts) {
    const params: Record<string, string | number> = {
      api: 'SYNO.SurveillanceStation.Recording',
      method: 'List',
      version: apiVersion,
      fromTime: fromTs,
      toTime: toTs,
      _sid: session.sid,
      ...extra,
    }
    const target = buildEntryUrl(url, params)
    let res: Response
    try {
      res = await fetch(target)
    } catch (err: any) {
      lastError = err
      continue
    }
    if (!res.ok) {
      lastError = new Error(`HTTP ${res.status}`)
      continue
    }
    const json: any = await res.json().catch(() => null)
    if (!json?.success) {
      lastError = json?.error
      continue
    }
    const items: any[] = json?.data?.recordings || json?.data?.events || []
    const camSet = new Set(cameraIds.map((c) => Number(c)))
    const filtered = items
      .filter((r) => {
        // If we used the unfiltered fallback, filter client-side.
        if (Object.keys(extra).length === 0) {
          const cid = Number(r?.camera_id ?? r?.cameraId)
          return camSet.has(cid)
        }
        return true
      })
      .map((r) => {
        const filePath = r?.filePath ?? r?.file_path ?? null
        const startTime =
          r?.startTime ??
          r?.start_time ??
          r?.recordingTime ??
          r?.recordTime ??
          parseStartFromFilePath(filePath)
        return {
          id: r?.id ?? r?.recordId,
          cameraId: r?.camera_id ?? r?.cameraId ?? null,
          startTime: startTime ?? null,
          filePath,
          raw: r,
        }
      })
    return filtered
  }

  throw createError({
    statusCode: 502,
    statusMessage: `Synology Recording.List failed: ${lastError?.message ?? lastError?.code ?? 'unknown error'}`
  })
}

/**
 * Stream a recording to disk. Returns the number of bytes written.
 * Used by the init endpoint to cache the file before serving it.
 *
 * `offsetTimeMs` / `playTimeMs` slice a sub-window out of the underlying
 * (typically ~30-min) recording so we don't download the whole file when
 * we only need a few minutes around the commissioning timestamp. Both
 * are optional — passing neither gives the full recording.
 */
export async function synologyDownloadRecording(
  session: SynoSession,
  recordingId: number | string,
  destPath: string,
  apiVersion: number = 6,
  offsetTimeMs?: number,
  playTimeMs?: number
): Promise<number> {
  const { url } = getConfig()
  const params: Record<string, string | number> = {
    api: 'SYNO.SurveillanceStation.Recording',
    method: 'Download',
    version: apiVersion,
    id: recordingId,
    mountId: 0,
    _sid: session.sid,
  }
  if (Number.isFinite(offsetTimeMs) && (offsetTimeMs as number) >= 0) {
    params.offsetTimeMs = Math.floor(offsetTimeMs as number)
  }
  if (Number.isFinite(playTimeMs) && (playTimeMs as number) > 0) {
    params.playTimeMs = Math.floor(playTimeMs as number)
  }
  const target = buildEntryUrl(url, params)

  const res = await fetch(target)
  if (!res.ok || !res.body) {
    throw createError({
      statusCode: 502,
      statusMessage: `Synology Recording.Download failed (HTTP ${res.status}).`
    })
  }

  // Some SS builds return a JSON error with HTTP 200 if the recording is gone.
  const ct = res.headers.get('content-type') || ''
  if (ct.includes('application/json')) {
    const json: any = await res.json().catch(() => null)
    throw createError({
      statusCode: 502,
      statusMessage: `Synology Recording.Download returned an error (code ${json?.error?.code ?? '?'}).`
    })
  }

  const out = createWriteStream(destPath)
  // Node 18+ converts a WHATWG ReadableStream into a Readable via fromWeb.
  const nodeStream = Readable.fromWeb(res.body as any)
  let written = 0
  nodeStream.on('data', (chunk: Buffer) => { written += chunk.length })
  await pipeline(nodeStream, out)
  return written
}

/**
 * Server-side time-range export. Documented in the Surveillance Station
 * Web API as a 3-step async flow:
 *
 *   1. RangeExport(camId, fromTime, toTime, fileName) → returns dlid
 *   2. GetRangeExportProgress(dlid)                   → poll until progress=100
 *      (must be called within every 20 s to keep the task alive)
 *   3. OnRangeExportDone(dlid, fileName)              → stream the merged MP4
 *      (must be called within 1 min of progress=100, else NAS cleans up)
 *
 * On a continuously-recording camera (one ~30-min file per period) this
 * returns a single MP4 covering the exact window asked for — no local
 * stitching needed. On a motion-triggered camera the result is a merged
 * file with motion gaps elided (same content as our previous ffmpeg
 * concat path, just produced server-side); set the camera back to
 * continuous recording for full timeline alignment.
 *
 * Throws if the NAS reports `progress=-1`, returns a `zip` (which means
 * codec/resolution mismatch in the range and the API gave us a bundle
 * of pieces, not a merged file), or if any step times out.
 */
export async function synologyRangeExport(
  session: SynoSession,
  cameraId: number,
  fromTs: number,
  toTs: number,
  destPath: string,
  fileName: string = 'video',
  apiVersion: number = 6,
): Promise<{ bytes: number; fileExt: 'mp4' | 'zip' }> {
  const { url } = getConfig()

  // Step 1: kick off the export task.
  const startUrl = buildEntryUrl(url, {
    api: 'SYNO.SurveillanceStation.Recording',
    method: 'RangeExport',
    version: apiVersion,
    camId: cameraId,
    fromTime: fromTs,
    toTime: toTs,
    fileName,
    _sid: session.sid,
  })
  const startRes = await fetch(startUrl)
  const startJson: any = await startRes.json().catch(() => null)
  if (!startJson?.success || !Number.isFinite(startJson?.data?.dlid)) {
    throw createError({
      statusCode: 502,
      statusMessage: `RangeExport start failed (code ${startJson?.error?.code ?? 'unknown'}).`
    })
  }
  const dlid = Number(startJson.data.dlid)

  // Step 2: poll progress every 1.5 s until done. Hard cap at 5 min so a
  // stuck task can't pin the request indefinitely. The doc says we MUST
  // poll at least every 20 s to keep the task alive; 1.5 s is fine.
  let fileExt = ''
  const t0 = Date.now()
  const MAX_MS = 5 * 60 * 1000
  while (Date.now() - t0 < MAX_MS) {
    await new Promise((r) => setTimeout(r, 1500))
    const progressUrl = buildEntryUrl(url, {
      api: 'SYNO.SurveillanceStation.Recording',
      method: 'GetRangeExportProgress',
      version: apiVersion,
      dlid,
      _sid: session.sid,
    })
    const progressJson: any = await fetch(progressUrl).then(r => r.json()).catch(() => null)
    if (!progressJson?.success) {
      throw createError({
        statusCode: 502,
        statusMessage: `RangeExport progress failed (code ${progressJson?.error?.code ?? 'unknown'}).`
      })
    }
    const progress = Number(progressJson?.data?.progress)
    fileExt = String(progressJson?.data?.fileExt ?? '')
    if (progress === -1) {
      throw createError({ statusCode: 502, statusMessage: 'RangeExport task failed (progress=-1).' })
    }
    if (progress === 100) break
  }
  if (fileExt !== 'mp4' && fileExt !== 'zip') {
    throw createError({
      statusCode: 504,
      statusMessage: `RangeExport timed out after ${Math.round((Date.now() - t0) / 1000)}s (last fileExt='${fileExt}').`
    })
  }
  if (fileExt === 'zip') {
    throw createError({
      statusCode: 502,
      statusMessage: 'RangeExport produced a zip (codec/resolution mismatch in the time range). Set the camera to continuous recording with consistent encoding.'
    })
  }

  // Step 3: stream the merged file to disk.
  const downloadUrl = buildEntryUrl(url, {
    api: 'SYNO.SurveillanceStation.Recording',
    method: 'OnRangeExportDone',
    version: apiVersion,
    dlid,
    fileName,
    _sid: session.sid,
  })
  const dlRes = await fetch(downloadUrl)
  if (!dlRes.ok || !dlRes.body) {
    throw createError({
      statusCode: 502,
      statusMessage: `RangeExport download failed (HTTP ${dlRes.status}).`
    })
  }
  const ct = dlRes.headers.get('content-type') || ''
  if (ct.includes('application/json')) {
    const json: any = await dlRes.json().catch(() => null)
    throw createError({
      statusCode: 502,
      statusMessage: `RangeExport download returned error (code ${json?.error?.code ?? '?'}).`
    })
  }
  const out = createWriteStream(destPath)
  const nodeStream = Readable.fromWeb(dlRes.body as any)
  let written = 0
  nodeStream.on('data', (chunk: Buffer) => { written += chunk.length })
  await pipeline(nodeStream, out)
  return { bytes: written, fileExt: 'mp4' }
}

export function getDefaultCameraIds(): number[] {
  const config = useRuntimeConfig()
  const raw = (config.synology?.cameraIds || '').toString().trim()
  if (!raw) return [2]
  return raw
    .split(/[,\s]+/)
    .map((s) => Number(s))
    .filter((n) => Number.isFinite(n) && n > 0)
}

export function getWindowSeconds(): { pre: number; post: number } {
  const config = useRuntimeConfig()
  const pre = Number(config.synology?.preSeconds ?? 10)
  const post = Number(config.synology?.postSeconds ?? 180)
  return {
    pre: Number.isFinite(pre) && pre >= 0 ? pre : 10,
    post: Number.isFinite(post) && post >= 0 ? post : 180,
  }
}
