#!/usr/bin/env node
/**
 * Standalone shell harness for the surveillance-recording lookup.
 *
 * Mirrors what server/api/surveillance/shipment-recording/init.post.ts does
 * but talks to Synology directly so we can verify the timezone shift and
 * the listing window without going through the Nuxt request stack.
 *
 * Usage:
 *   node scripts/test-synology-recording.mjs --ts 2026-05-03T08:04:42Z
 *   node scripts/test-synology-recording.mjs --ts 2026-05-03T08:04:42Z --pre 120 --post 60
 *   node scripts/test-synology-recording.mjs --ts 2026-05-03T08:04:42Z --no-shift   # skip Berlin offset
 *   node scripts/test-synology-recording.mjs --ts 2026-05-03T08:04:42Z --download /tmp/clip.mp4
 *
 * Env (reads .env from repo root automatically):
 *   SYNOLOGY_URL, SYNOLOGY_USER, SYNOLOGY_PASSWORD, SYNOLOGY_CAMERA_IDS
 */

import { readFileSync, createWriteStream } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const repoRoot = resolve(__dirname, '..', '..') // repo root (this file lives in reference/test-scripts/)

// ---------- tiny .env loader (no dependency) ----------------------------
function loadEnv() {
  for (const name of ['.env', '.env-prod', '.env.local']) {
    try {
      const text = readFileSync(resolve(repoRoot, name), 'utf8')
      for (const raw of text.split('\n')) {
        const line = raw.trim()
        if (!line || line.startsWith('#')) continue
        const eq = line.indexOf('=')
        if (eq < 0) continue
        const k = line.slice(0, eq).trim()
        let v = line.slice(eq + 1).trim()
        if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
          v = v.slice(1, -1)
        }
        if (!(k in process.env)) process.env[k] = v
      }
      // Use the first one we find — .env wins over .env-prod by listing
      // order above. Stop after one to mirror the live runtime.
      return
    } catch { /* try next */ }
  }
}
loadEnv()

// ---------- arg parsing -------------------------------------------------
const args = Object.fromEntries(
  process.argv.slice(2).reduce((acc, cur, i, arr) => {
    if (cur.startsWith('--')) {
      const key = cur.slice(2)
      const next = arr[i + 1]
      if (!next || next.startsWith('--')) acc.push([key, true])
      else acc.push([key, next])
    }
    return acc
  }, [])
)

const tsArg = args.ts || args.anchor
if (!tsArg) {
  console.error('Required: --ts <iDempiere Z time, e.g. 2026-05-03T08:04:42Z>')
  process.exit(2)
}
const pre = Number(args.pre ?? process.env.SYNOLOGY_PRE_SECONDS ?? 120)
const post = Number(args.post ?? process.env.SYNOLOGY_POST_SECONDS ?? 60)
const noShift = !!args['no-shift']
const downloadPath = typeof args.download === 'string' ? args.download : null
const cameraIdsRaw = (args.cams || process.env.SYNOLOGY_CAMERA_IDS || '2').toString()
const cameraIds = cameraIdsRaw.split(/[,\s]+/).map(Number).filter(n => Number.isFinite(n) && n > 0)

// ---------- timezone helpers (must match init.post.ts) ------------------
function berlinOffsetSeconds(unixSeconds) {
  const ms = unixSeconds * 1000
  const date = new Date(ms)
  const fmt = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'Europe/Berlin',
    year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', second: '2-digit',
    hourCycle: 'h23',
  })
  const parts = {}
  for (const p of fmt.formatToParts(date)) if (p.type !== 'literal') parts[p.type] = p.value
  const berlinAsUtcMs = Date.UTC(
    Number(parts.year), Number(parts.month) - 1, Number(parts.day),
    Number(parts.hour), Number(parts.minute), Number(parts.second)
  )
  return Math.round((berlinAsUtcMs - ms) / 1000)
}

function parseTs(s) {
  const t = Math.floor(new Date(s).getTime() / 1000)
  if (!Number.isFinite(t) || t <= 0) return null
  return noShift ? t : t + berlinOffsetSeconds(t)
}

const fmtUtc = ts => new Date(ts * 1000).toISOString().replace('.000', '')
const fmtBerlin = ts => new Date(ts * 1000).toLocaleString('de-DE', { timeZone: 'Europe/Berlin', hour12: false })

// ---------- Synology calls ----------------------------------------------
const url = (process.env.SYNOLOGY_URL || '').replace(/\/+$/, '')
const user = process.env.SYNOLOGY_USER || ''
const password = process.env.SYNOLOGY_PASSWORD || ''
if (!url || !user || !password) {
  console.error('Missing SYNOLOGY_URL / SYNOLOGY_USER / SYNOLOGY_PASSWORD in env.')
  process.exit(2)
}

function entryUrl(params) {
  const qs = new URLSearchParams()
  for (const [k, v] of Object.entries(params)) qs.set(k, String(v))
  return `${url}/webapi/entry.cgi?${qs.toString()}`
}

async function login() {
  const target = entryUrl({
    api: 'SYNO.API.Auth', method: 'login', version: 7,
    account: user, passwd: password, session: 'SurveillanceStation', format: 'sid'
  })
  const res = await fetch(target)
  const json = await res.json().catch(() => null)
  if (!json?.success || !json?.data?.sid) {
    throw new Error(`Login failed: ${JSON.stringify(json?.error || { http: res.status })}`)
  }
  return json.data.sid
}

async function logout(sid) {
  const target = entryUrl({
    api: 'SYNO.API.Auth', method: 'logout', version: 7,
    _sid: sid, session: 'SurveillanceStation'
  })
  await fetch(target).catch(() => null)
}

function parseStartFromFilePath(filePath) {
  if (!filePath) return null
  const m = /([0-9]{13})-[0-9]+\.mp4$/.exec(filePath)
  if (!m) return null
  const ms = Number(m[1])
  return Number.isFinite(ms) && ms > 0 ? Math.floor(ms / 1000) : null
}

async function listRecordings(sid, fromTs, toTs, label) {
  const camCsv = cameraIds.join(',')
  const attempts = [{ cameraIds: camCsv }, { cameraIds: `[${camCsv}]` }, {}]
  for (const extra of attempts) {
    const target = entryUrl({
      api: 'SYNO.SurveillanceStation.Recording',
      method: 'List', version: 6,
      fromTime: fromTs, toTime: toTs, _sid: sid, ...extra
    })
    const res = await fetch(target)
    if (!res.ok) continue
    const json = await res.json().catch(() => null)
    if (!json?.success) continue
    const items = json?.data?.recordings || json?.data?.events || []
    const camSet = new Set(cameraIds)
    return items
      .filter(r => Object.keys(extra).length || camSet.has(Number(r?.camera_id ?? r?.cameraId)))
      .map(r => ({
        id: r?.id ?? r?.recordId,
        cameraId: r?.camera_id ?? r?.cameraId ?? null,
        startTime: r?.startTime ?? r?.start_time ?? r?.recordingTime ?? r?.recordTime ?? parseStartFromFilePath(r?.filePath ?? r?.file_path),
        filePath: r?.filePath ?? r?.file_path ?? null,
      }))
  }
  return []
}

async function downloadClip(sid, recordingId, outPath, offsetMs, playMs) {
  const params = {
    api: 'SYNO.SurveillanceStation.Recording', method: 'Download',
    version: 6, id: recordingId, mountId: 0, _sid: sid,
  }
  if (Number.isFinite(offsetMs)) params.offsetTimeMs = offsetMs
  if (Number.isFinite(playMs)) params.playTimeMs = playMs
  const target = entryUrl(params)
  const res = await fetch(target)
  if (!res.ok || !res.body) throw new Error(`Download failed (HTTP ${res.status})`)
  await pipeline(Readable.fromWeb(res.body), createWriteStream(outPath))
}

// ---------- main --------------------------------------------------------
const rawAnchor = Math.floor(new Date(tsArg).getTime() / 1000)
const offset = berlinOffsetSeconds(rawAnchor)
const anchor = parseTs(tsArg)
const fromTs = anchor - pre
const toTs = anchor + post

console.log('=== Input ===')
console.log(`  --ts             ${tsArg}`)
console.log(`  pre/post         ${pre}s / ${post}s`)
console.log(`  cameras          ${cameraIds.join(',')}`)
console.log(`  shift            ${noShift ? 'OFF (raw UTC)' : `ON (+${offset}s = ${(offset / 3600).toFixed(0)}h Berlin)`}`)
console.log()
console.log('=== Anchors ===')
console.log(`  iDempiere "Z"    ${tsArg}              (parsed as UTC: ${rawAnchor})`)
console.log(`  Berlin clock     ${fmtBerlin(rawAnchor)}    (real Berlin local for that UTC moment)`)
console.log(`  Query anchor     ${anchor}              (${fmtUtc(anchor)} interpreted as UTC)`)
console.log(`  Query window     [${fromTs}, ${toTs}]   →  ${fmtUtc(fromTs)} → ${fmtUtc(toTs)}`)
console.log()

const sid = await login()
console.log(`✓ Logged in (sid ${sid.slice(0, 8)}…)`)
try {
  console.log()
  console.log('=== Narrow listing (fromTs/toTs as-is) ===')
  const narrow = await listRecordings(sid, fromTs, toTs)
  console.log(`  ${narrow.length} hit(s)`)
  for (const r of narrow) {
    console.log(`    id=${r.id}  cam=${r.cameraId}  start=${r.startTime} (${r.startTime ? fmtUtc(r.startTime) : '?'})  file=${r.filePath}`)
  }

  // Cameras are motion-triggered → chunks are short and start at irregular
  // times (not on 30-min boundaries). The chunk that actually contains
  // the anchor is the one with the LATEST start ≤ anchor — closest by
  // absolute distance can prefer a chunk that started a few seconds AFTER
  // the anchor and miss the moment.
  const before = narrow.filter(r => Number.isFinite(r.startTime) && r.startTime <= anchor)
  let chosen = null
  if (before.length) {
    chosen = before.reduce((best, r) => (r.startTime > best.startTime ? r : best), before[0])
    console.log()
    console.log('=== Chosen chunk (latest start ≤ anchor) ===')
  } else if (narrow.length) {
    chosen = narrow.reduce((best, r) => ((r.startTime ?? Infinity) < (best.startTime ?? Infinity) ? r : best), narrow[0])
    console.log()
    console.log('=== Chosen chunk (no chunk starts before anchor — taking earliest forward) ===')
  }
  let sliceStart, offsetMs, playMs
  if (chosen) {
    const st = Number(chosen.startTime)
    console.log(`    id=${chosen.id}  start=${fmtUtc(st)}  anchor is ${anchor - st}s into chunk`)
    sliceStart = Math.max(fromTs, st)
    offsetMs = Math.max(0, (sliceStart - st) * 1000)
    playMs = Math.max(1000, (toTs - sliceStart) * 1000)
    console.log(`    sliceStart=${fmtUtc(sliceStart)}  offsetMs=${offsetMs}  playMs=${playMs}`)
  }

  if (downloadPath && chosen) {
    console.log()
    console.log(`=== Downloading id=${chosen.id} → ${downloadPath} ===`)
    await downloadClip(sid, chosen.id, downloadPath, offsetMs, playMs)
    console.log(`  ✓ saved`)
  }
} finally {
  await logout(sid)
  console.log()
  console.log('✓ Logged out')
}
