'use strict';

const express = require('express');
const fs = require('fs');
const http = require('http');
const path = require('path');
const { CameraSwitch } = require('./src/cameraSwitch');
const { createSnapshotRouter, createStreamRouter, createCamerasRouter } = require('./src/snapshotService');
const { createConfigRouter } = require('./src/configService');
const { resolveHwenc, mseCodecString } = require('./src/hwencode');

const PORT = parseInt(process.env.PORT ?? '8444', 10);
const LIVE_SIZE = process.env.LIVE_SIZE ?? '640x480';
const LIVE_FPS = parseInt(process.env.LIVE_FPS ?? '30', 10);
const HIRES_SIZE = process.env.HIRES_SIZE ?? '1280x960';
const HIRES_FPS = parseInt(process.env.HIRES_FPS ?? '15', 10);
const ENCODE_MODE = process.env.ENCODE_MODE ?? 'copybsf'; // 'copybsf' (niedrige CPU) | 'mjpeg' (Re-Encode) | 'h264' (GPU)
const ON_DEMAND = (process.env.ON_DEMAND ?? 'true') !== 'false'; // Live nur bei Verbrauchern (spart idle-CPU)
const IDLE_GRACE_MS = parseInt(process.env.IDLE_GRACE_MS ?? '15000', 10);

// ── Hardware-Encoding (nur relevant für Kameras mit encode='h264') ────────────
//   GPU=intel|amd|auto|none   – Maschinen-GPU (intel/amd → VAAPI, none → libx264)
//   HWENC=vaapi|qsv|libx264   – Encoder erzwingen (überschreibt GPU)
//   HWENC_DEVICE=/dev/dri/renderD128   – VAAPI/QSV-Renderknoten
const HWENC = resolveHwenc({
  vendor:  process.env.GPU ?? 'auto',
  encoder: process.env.HWENC,
  device:  process.env.HWENC_DEVICE,
});
const H264 = {
  bitrate: process.env.H264_BITRATE ?? '3M',
  gop:     process.env.H264_GOP ? parseInt(process.env.H264_GOP, 10) : undefined, // default in hwencode: ~2×fps
  profile: process.env.H264_PROFILE ?? 'main',
  fragMs:  parseInt(process.env.H264_FRAG_MS ?? '200', 10),
  jpegFps: parseInt(process.env.H264_JPEG_FPS ?? '2', 10), // Snapshot-Nebenausgang
};
const MSE_CODEC = process.env.H264_MSE_CODEC ?? mseCodecString(H264.profile, process.env.H264_LEVEL ?? '1F');

// ── cameras.json → CameraSwitch-Instanzen ─────────────────────────────────────
const CAMERAS_PATH = path.join(__dirname, 'cameras.json');
let camerasJson;
try {
  camerasJson = JSON.parse(fs.readFileSync(CAMERAS_PATH, 'utf8'));
} catch (e) {
  console.error('cameras.json fehlt oder ungültig:', e.message); process.exit(1);
}

// Atomar schreiben: tmp + rename → nie ein halb-geschriebenes cameras.json.
function persistCameras(obj) {
  const tmp = CAMERAS_PATH + '.tmp';
  fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
  fs.renameSync(tmp, CAMERAS_PATH);
}
const camsConfig = camerasJson.cameras;
if (!Array.isArray(camsConfig) || camsConfig.length === 0) {
  console.error('cameras.json: "cameras" muss ein nicht-leeres Array sein'); process.exit(1);
}

const switches = {};
const camsMeta = []; // { id, device, name, position, stream, hires, note }
for (const cam of camsConfig) {
  if (!cam.id || !cam.device) {
    console.error(`cameras.json: Eintrag ohne id/device: ${JSON.stringify(cam)}`); process.exit(1);
  }
  const camEncode = cam.encode ?? ENCODE_MODE;
  // Per-Kamera-Felder in cameras.json überschreiben die globalen Env-Werte
  switches[cam.id] = new CameraSwitch({
    id: cam.id, device: cam.device,
    liveSize:    cam.liveSize   ?? LIVE_SIZE,
    liveFps:     cam.liveFps    ?? LIVE_FPS,
    hiresSize:   cam.hiresSize  ?? HIRES_SIZE,
    hiresFps:    cam.hiresFps   ?? HIRES_FPS,
    encode:      camEncode,
    hiresEncode: cam.hiresEncode,                // undefined → fällt im Konstruktor auf encode zurück
    stream:      cam.stream !== false,           // UI "Aus" → Kamera startet nicht live
    onDemand: ON_DEMAND, idleGraceMs: IDLE_GRACE_MS,
    hwenc: HWENC, h264: H264, mseCodec: MSE_CODEC, // nur für encode='h264' relevant
  });
  camsMeta.push({
    id: cam.id,
    device: cam.device,
    name: cam.name ?? cam.id,
    position: cam.position ?? '',
    stream: cam.stream !== false,
    hires: cam.hires !== false,
    encode: camEncode,
    mseCodec: camEncode === 'h264' ? MSE_CODEC : null,
    note: cam.note ?? '',
  });
}

const app = express();
app.use(express.json()); // POST /api/config liest JSON-Body

// ── 1. Eigene Endpunkte ───────────────────────────────────────────────────────
app.use('/api/snapshot', createSnapshotRouter(switches, camsMeta));
app.use('/api/stream', createStreamRouter(switches));
app.use('/api/cameras', createCamerasRouter(camsMeta));
app.use('/api/config', createConfigRouter({
  switches, camsMeta,
  getCamerasJson: () => camerasJson,
  setCamerasJson: (v) => { camerasJson = v; },
  persist: persistCameras,
  mseCodec: MSE_CODEC,
}));

app.get('/health', (_req, res) => {
  res.json({
    status: 'ok',
    cameras: camsMeta.map((c) => {
      const sw = switches[c.id];
      return { id: c.id, name: c.name, device: c.device, state: sw?.state, hasFrame: !!sw?.latest };
    }),
  });
});

app.get('/config.json', (_req, res) => {
  res.json({ cameras: camsMeta.map((c) => ({ id: c.id, name: c.name, stream: c.stream })) });
});

// ── 2. Statische Dateien ──────────────────────────────────────────────────────
//  no-cache: Browser MUSS index.html/viewer.js vor Nutzung revalidieren.
app.use(express.static(path.join(__dirname, 'public'), {
  setHeaders: (res) => res.setHeader('Cache-Control', 'no-cache'),
}));

// ── 3. Start ──────────────────────────────────────────────────────────────────
const server = http.createServer(app);

server.listen(PORT, '0.0.0.0', () => {
  console.log(`AppRobotWebcam  http://0.0.0.0:${PORT}`);
  console.log(`  Kameras:      ${camsMeta.map((c) => `${c.id}=${c.device} "${c.name}" stream=${c.stream}`).join(', ')}`);
  console.log(`  Live:         ${LIVE_SIZE}@${LIVE_FPS}  ·  HD-Grab: ${HIRES_SIZE}@${HIRES_FPS}  ·  Encode: ${ENCODE_MODE}  ·  On-Demand: ${ON_DEMAND}`);
  console.log(`  H.264-GPU:    ${HWENC.encoder} @ ${HWENC.device}  ·  ${H264.bitrate}, profile=${H264.profile}, MSE=${MSE_CODEC}  (nur für encode=h264)`);
  console.log(`  Viewer:       http://0.0.0.0:${PORT}/`);

  // Live-Producer starten (Dauerbetrieb)
  Object.values(switches).forEach((sw) => sw.start());
});

const shutdown = (sig) => {
  console.log(`\n${sig} – shutting down`);
  Object.values(switches).forEach((sw) => { sw.stopping = true; if (sw.proc) { try { sw.proc.kill('SIGKILL'); } catch (_e) {} } });
  server.close(() => process.exit(0));
  setTimeout(() => process.exit(0), 3000); // Sicherheitsnetz
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
