// ─── PS4 POS PRINT BRIDGE v3.0 ────────────────────────────────────────────────
// NEW in v3.0:
//   • POST /notify        — Kitchen sends "item ready" event
//   • GET  /events        — SSE stream → Flutter app + React POS subscribe here
//   • GET  /notifications — Poll last N notifications (fallback for SSE-less clients)
//
// Existing endpoints unchanged:
//   GET  /health  •  GET /printers  •  GET /build
//   POST /print   •  POST /print-wifi
// ─────────────────────────────────────────────────────────────────────────────

import http              from 'http';
import fs                from 'fs';
import path              from 'path';
import os                from 'os';
import { exec, execSync, spawnSync } from 'child_process';
import net               from 'net';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const PORT     = 9191;
const PLATFORM = os.platform();

const RAWPRINT_EXE = path.join(__dirname, 'rawprint.exe');

const SPAWN_OPTS = { stdio: 'pipe', windowsHide: true };
const EXEC_OPTS  = { stdio: 'pipe', windowsHide: true };

// =============================================================================
//  BRIDGE CONFIG (#14) ────────────────────────────────────────────────────────
//  Persisted to bridge-config.json alongside server.js. Controls whether the
//  fingerprint module, printer endpoints, and notification endpoints are
//  enabled — set from the React app's Master Setup → App Config screen via
//  POST /fingerprint/config (fingerprint) or edited directly in this file for
//  printer/notification (no dedicated endpoint needed — these mirror the
//  React-side AppConfigService flags but as a server-side failsafe / for
//  headless deployments where the bridge runs without the POS UI open).
// =============================================================================
const CONFIG_FILE = path.join(__dirname, 'bridge-config.json');

const DEFAULT_CONFIG = {
  fingerprintEnabled:  false,
  printerEnabled:      true,
  notificationEnabled: true,
  esslDeviceIp:        '',
  esslDevicePort:      4370,
};

function loadConfig() {
  try {
    const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
    return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
  } catch {
    return { ...DEFAULT_CONFIG };
  }
}

function saveConfig(cfg) {
  try {
    fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
    return true;
  } catch (e) {
    console.warn('[config] save failed:', e.message);
    return false;
  }
}

let bridgeConfig = loadConfig();

// =============================================================================
//  SSE NOTIFICATION SYSTEM  (new in v3.0)
// =============================================================================

// In-memory store: last 100 notifications (ring buffer)
const MAX_NOTIFICATIONS = 100;
const notificationStore = [];   // [{ id, ts, ...payload }]
let   notifIdCounter    = 1;

// All active SSE connections: Map<res, { captainName?, lastId }>
const sseClients = new Map();

/**
 * Push a notification to ALL connected SSE clients, and persist it.
 * payload: { orderId, tableNo, itemName, captainName, salesType, type }
 */
function pushNotification(payload) {
  const id   = notifIdCounter++;
  const ts   = Date.now();
  const notif = { id, ts, ...payload };

  // Store (ring-buffer trim)
  notificationStore.push(notif);
  if (notificationStore.length > MAX_NOTIFICATIONS) notificationStore.shift();

  // Broadcast to every SSE client
  const data = `id: ${id}\ndata: ${JSON.stringify(notif)}\n\n`;
  for (const [res] of sseClients) {
    try { res.write(data); } catch { sseClients.delete(res); }
  }

  console.log(`[NOTIFY] #${id} → Table ${payload.tableNo} | ${payload.itemName} | captain: ${payload.captainName || 'all'}`);
  return notif;
}

// =============================================================================
//  EXISTING HELPERS (unchanged)
// =============================================================================

function safeExec(cmd, extraOpts = {}) {
  try {
    return execSync(cmd, { ...EXEC_OPTS, timeout: 6000, ...extraOpts }).toString();
  } catch (e) {
    console.warn('[safeExec]', e.message.slice(0, 120));
    return '';
  }
}

let _printerCache  = { list: [], default: null, ts: 0 };
const CACHE_TTL_MS = 60_000;

function cors(res) {
  res.setHeader('Access-Control-Allow-Origin',  '*');
  res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Last-Event-ID');
}
function jsonRes(res, code, obj) {
  cors(res);
  res.writeHead(code, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(obj));
}

function getDefaultPrinterSync() {
  if (PLATFORM === 'win32') {
    const r = safeExec('wmic printer where "Default=TRUE" get Name /value');
    const m = r.match(/Name=(.+)/);
    return m ? m[1].trim() : null;
  } else {
    const r = safeExec('lpstat -d 2>/dev/null');
    const m = r.match(/destination:\s*(.+)/);
    return m ? m[1].trim() : null;
  }
}

function listPrintersSync() {
  if (PLATFORM === 'win32') {
    const r = safeExec('wmic printer get Name /value');
    return r.split('\n')
      .map(l => { const m = l.match(/Name=(.+)/); return m ? m[1].trim() : ''; })
      .filter(Boolean);
  } else {
    const cmd = PLATFORM === 'linux'
      ? "lpstat -a 2>/dev/null | awk '{print $1}'"
      : "lpstat -p 2>/dev/null | awk '{print $2}'";
    return safeExec(cmd).split('\n').map(s => s.trim()).filter(Boolean);
  }
}

async function getPrinterInfo() {
  if (Date.now() - _printerCache.ts < CACHE_TTL_MS) return _printerCache;
  const list        = listPrintersSync();
  const defaultName = getDefaultPrinterSync();
  _printerCache = { list, default: defaultName || (list.length ? list[0] : null), ts: Date.now() };
  return _printerCache;
}

async function matchPrinter(requested) {
  const info = await getPrinterInfo();
  const all  = info.list;
  if (!requested || !requested.trim()) return { matched: info.default, all, usedDefault: true };
  const req   = requested.toLowerCase().trim();
  const exact = all.find(p => p.toLowerCase().trim() === req);
  if (exact) return { matched: exact, all, usedDefault: false };
  const fwd   = all.find(p => p.toLowerCase().includes(req));
  if (fwd)   return { matched: fwd,   all, usedDefault: false };
  const bwd   = all.find(p => req.includes(p.toLowerCase().trim()));
  if (bwd)   return { matched: bwd,   all, usedDefault: false };
  return { matched: info.default, all, usedDefault: true, fallback: true };
}

function writeTmp(bytes) {
  const tmp = path.join(os.tmpdir(), `ps4_${Date.now()}_${Math.random().toString(36).slice(2)}.bin`);
  fs.writeFileSync(tmp, Buffer.from(bytes));
  return tmp;
}
function rmTmp(f) { try { fs.unlinkSync(f); } catch {} }
function rawprintExists() { try { return fs.existsSync(RAWPRINT_EXE); } catch { return false; } }

function buildRawprintExe() {
  const csFile = path.join(__dirname, 'rawprint.cs');
  if (!fs.existsSync(csFile)) { console.error('[build] rawprint.cs not found'); return false; }
  const cscPaths = [
    'C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe',
    'C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\csc.exe',
  ];
  let cscPath = cscPaths.find(p => fs.existsSync(p)) || null;
  if (!cscPath) { try { safeExec('csc.exe /help'); cscPath = 'csc.exe'; } catch {} }
  if (!cscPath) { console.error('[build] csc.exe not found'); return false; }
  const result = spawnSync(cscPath, [`/out:${RAWPRINT_EXE}`, '/target:exe', '/optimize+', csFile], { ...SPAWN_OPTS, timeout: 30000 });
  if (result.status === 0 && fs.existsSync(RAWPRINT_EXE)) { console.log('[build] ✓ rawprint.exe compiled'); return true; }
  console.error('[build] ✗', result.stderr?.toString().slice(0, 300));
  return false;
}

async function printWindows(printerName, bytes) {
  const tmp = writeTmp(bytes);
  console.log(`[WIN] → "${printerName}"  ${bytes.length}B`);
  if (!rawprintExists()) { console.log('[WIN] rawprint.exe missing — auto-building...'); buildRawprintExe(); }
  if (rawprintExists()) {
    const result = spawnSync(RAWPRINT_EXE, [printerName, tmp], { ...SPAWN_OPTS, timeout: 10000 });
    const stdout = result.stdout?.toString().trim() || '';
    if (result.status === 0 && stdout.startsWith('OK:')) { rmTmp(tmp); return { ok: true, method: 'rawprint_exe', printer: printerName }; }
  }
  const copyResult = safeExec(`cmd /c copy /b "${tmp}" "${printerName}"`);
  rmTmp(tmp);
  if (copyResult) return { ok: true, method: 'cmd_copy_b', printer: printerName };
  return { ok: false, error: `All methods failed for "${printerName}"` };
}

async function printUnix(printerName, bytes) {
  const tmp = writeTmp(bytes);
  return new Promise(resolve => {
    exec(`lp -d "${printerName}" "${tmp}"`, { timeout: 8000 }, (err) => {
      rmTmp(tmp);
      if (err) resolve({ ok: false, error: err.message });
      else     resolve({ ok: true, method: 'lp_cups', printer: printerName });
    });
  });
}

async function printWifi(ip, port, bytes) {
  return new Promise(resolve => {
    const sock  = new net.Socket();
    sock.setNoDelay(true);
    const timer = setTimeout(() => { sock.destroy(); resolve({ ok: false, error: `Timeout ${ip}:${port}` }); }, 7000);
    sock.connect(port, ip, () => {
      sock.write(Buffer.from(bytes), () => {
        clearTimeout(timer); sock.destroy();
        resolve({ ok: true, method: 'wifi_raw', printer: `${ip}:${port}` });
      });
    });
    sock.on('error', e => { clearTimeout(timer); resolve({ ok: false, error: e.message }); });
  });
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    req.on('data',  c => chunks.push(c));
    req.on('end',   () => { try { resolve(JSON.parse(Buffer.concat(chunks))); } catch(e) { reject(e); } });
    req.on('error', reject);
  });
}

// =============================================================================
//  eSSL 9500 FINGERPRINT BRIDGE (#8 / #14) ───────────────────────────────────
//  Endpoints expected by src/services/FingerprintService.js:
//    GET  /fingerprint/status     → { ok, deviceIp, devicePort, discovering, error }
//    POST /fingerprint/config     → { ip, port } → persisted to bridge-config.json
//    POST /fingerprint/reconnect  → re-checks connectivity (or re-discovers if no IP set)
//    GET  /fingerprint/last-scan?since=<ms> → { scan: { userId, time } | null }
//
//  NOTE: this layer handles connectivity/config (TCP reachability check,
//  device IP persistence, LAN auto-discovery on the eSSL default port 4370).
//  Parsing actual fingerprint scan EVENTS from the device requires the
//  vendor's binary protocol (ZKTeco/eSSL SDK) — that decoder is NOT included
//  here. `lastScan` stays null until that decoder pushes into `lastScan` via
//  `recordFingerprintScan(userId)`. The HTTP surface below is fully wired so
//  the React app's status/config/reconnect UI works end-to-end; only the
//  scan-decoding step is a stub.
// =============================================================================

let fpDiscovering = false;
let lastScan      = null; // { userId, time } | null

// Best-effort TCP reachability check on the configured device IP:port.
function checkDeviceReachable(ip, port, timeoutMs = 2500) {
  return new Promise(resolve => {
    if (!ip) return resolve(false);
    const sock = new net.Socket();
    const timer = setTimeout(() => { sock.destroy(); resolve(false); }, timeoutMs);
    sock.connect(port, ip, () => { clearTimeout(timer); sock.destroy(); resolve(true); });
    sock.on('error', () => { clearTimeout(timer); resolve(false); });
  });
}

// LAN auto-discovery: probe each host on the local /24 subnet on the eSSL
// default port (4370). Returns the first IP that accepts a TCP connection,
// or null if none found within the scan. Runs in the background — callers
// poll /fingerprint/status (fpDiscovering flag) for progress.
async function discoverDevice(port = 4370) {
  fpDiscovering = true;
  try {
    const ifaces = os.networkInterfaces();
    const local = Object.values(ifaces).flat()
      .find(i => i.family === 'IPv4' && !i.internal);
    if (!local) return null;
    const base = local.address.split('.').slice(0, 3).join('.');
    // Scan in small concurrent batches to avoid hammering the network
    const BATCH = 16;
    for (let start = 1; start <= 254; start += BATCH) {
      const batch = [];
      for (let h = start; h < Math.min(start + BATCH, 255); h++) {
        const ip = `${base}.${h}`;
        batch.push(checkDeviceReachable(ip, port, 400).then(ok => ok ? ip : null));
      }
      const results = await Promise.all(batch);
      const found = results.find(Boolean);
      if (found) return found;
    }
    return null;
  } finally {
    fpDiscovering = false;
  }
}

// Called by a vendor-SDK decoder (not included) when a real fingerprint scan
// event is received from the device. Kept here so the rest of the bridge
// (last-scan polling) works once that decoder is plugged in.
function recordFingerprintScan(userId) {
  lastScan = { userId: String(userId), time: Date.now() };
}

// =============================================================================
//  HTTP SERVER
// =============================================================================
const server = http.createServer(async (req, res) => {
  cors(res);
  if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
  const url = req.url.split('?')[0];

  // ── EXISTING: /health ────────────────────────────────────────────────────
  if (req.method === 'GET' && url === '/health') {
    const info = await getPrinterInfo();
    return jsonRes(res, 200, {
      ok: true, platform: PLATFORM,
      message: 'PS4 POS Print Bridge v3.0 (+ Notifications)',
      port: PORT,
      defaultPrinter:   info.default,
      printerCount:     info.list.length,
      rawprintReady:    rawprintExists(),
      sseClients:       sseClients.size,
      notificationCount: notificationStore.length,
      config:           bridgeConfig, // #14 — expose current bridge config
    });
  }

  // ── EXISTING: /printers ──────────────────────────────────────────────────
  if (req.method === 'GET' && url === '/printers') {
    const info = await getPrinterInfo();
    return jsonRes(res, 200, { ok: true, printers: info.list, defaultPrinter: info.default });
  }

  // ── EXISTING: /build ─────────────────────────────────────────────────────
  if (req.method === 'GET' && url === '/build') {
    if (rawprintExists()) return jsonRes(res, 200, { ok: true, message: 'rawprint.exe already exists' });
    const ok = buildRawprintExe();
    return jsonRes(res, ok ? 200 : 500, { ok, message: ok ? 'Compiled OK' : 'Compile failed — see console' });
  }

  // ── EXISTING: /print ─────────────────────────────────────────────────────
  if (req.method === 'POST' && url === '/print') {
    // #14 — gated by printerEnabled config flag
    if (!bridgeConfig.printerEnabled) {
      return jsonRes(res, 200, { ok: false, disabled: true, error: 'Printing is disabled in bridge config' });
    }
    let payload;
    try { payload = await readBody(req); }
    catch { return jsonRes(res, 400, { ok: false, error: 'Invalid JSON body' }); }
    const { bytes } = payload;
    const requestedName = (payload.printerName || '').trim();
    if (!bytes || !Array.isArray(bytes) || bytes.length === 0)
      return jsonRes(res, 400, { ok: false, error: 'Missing or empty bytes[]' });
    const { matched, all, usedDefault } = await matchPrinter(requestedName);
    if (!matched) return jsonRes(res, 404, { ok: false, error: 'No printer found', available: all });
    const result = PLATFORM === 'win32' ? await printWindows(matched, bytes) : await printUnix(matched, bytes);
    result.resolvedPrinter = matched;
    result.usedDefault     = usedDefault || false;
    return jsonRes(res, result.ok ? 200 : 500, result);
  }

  // ── EXISTING: /print-wifi ────────────────────────────────────────────────
  if (req.method === 'POST' && url === '/print-wifi') {
    // #14 — gated by printerEnabled config flag
    if (!bridgeConfig.printerEnabled) {
      return jsonRes(res, 200, { ok: false, disabled: true, error: 'Printing is disabled in bridge config' });
    }
    let payload;
    try { payload = await readBody(req); }
    catch { return jsonRes(res, 400, { ok: false, error: 'Invalid JSON body' }); }
    const { wifiIp, wifiPort = 9100, bytes } = payload;
    if (!bytes || !Array.isArray(bytes)) return jsonRes(res, 400, { ok: false, error: 'Missing bytes[]' });
    if (!wifiIp || !wifiIp.trim())       return jsonRes(res, 400, { ok: false, error: 'Missing wifiIp' });
    const result = await printWifi(wifiIp.trim(), parseInt(wifiPort) || 9100, bytes);
    return jsonRes(res, result.ok ? 200 : 500, result);
  }

  // ── NEW: POST /notify — Kitchen marks item ready ─────────────────────────
  // Body: { orderId, tableNo, itemName, captainName, salesType, type? }
  if (req.method === 'POST' && url === '/notify') {
    // #14 — gated by notificationEnabled config flag
    if (!bridgeConfig.notificationEnabled) {
      return jsonRes(res, 200, { ok: false, disabled: true, error: 'Notifications are disabled in bridge config' });
    }
    let payload;
    try { payload = await readBody(req); }
    catch { return jsonRes(res, 400, { ok: false, error: 'Invalid JSON' }); }

    const { orderId, tableNo, itemName, captainName, salesType } = payload;
    if (!orderId || !itemName) return jsonRes(res, 400, { ok: false, error: 'orderId and itemName are required' });

    const notif = pushNotification({
      type:        'ITEM_READY',
      orderId:     String(orderId),
      tableNo:     String(tableNo || ''),
      itemName:    String(itemName),
      captainName: String(captainName || ''),
      salesType:   String(salesType || 'Dine In'),
    });

    return jsonRes(res, 200, { ok: true, notif });
  }

  // ── NEW: GET /events — SSE stream for Flutter + React ───────────────────
  // Query params: ?captain=<captainName>  (optional filter)
  // Flutter/React keep this connection open; server pushes events as they arrive.
  if (req.method === 'GET' && url.startsWith('/events')) {
    // #14 — gated by notificationEnabled config flag
    if (!bridgeConfig.notificationEnabled) {
      return jsonRes(res, 200, { ok: false, disabled: true, error: 'Notifications are disabled in bridge config' });
    }
    const params      = new URLSearchParams(req.url.split('?')[1] || '');
    const captain     = params.get('captain') || '';
    const lastEventId = parseInt(req.headers['last-event-id'] || params.get('lastId') || '0', 10);

    // SSE headers
    res.writeHead(200, {
      'Content-Type':  'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection':    'keep-alive',
      'Access-Control-Allow-Origin': '*',
    });

    // Send any missed notifications since lastEventId
    const missed = notificationStore.filter(n => n.id > lastEventId);
    for (const n of missed) {
      res.write(`id: ${n.id}\ndata: ${JSON.stringify(n)}\n\n`);
    }

    // Send a heartbeat immediately so client knows connection is live
    res.write(`: connected captain=${captain || 'all'} clients=${sseClients.size + 1}\n\n`);

    // Register client
    sseClients.set(res, { captain, lastId: lastEventId });
    console.log(`[SSE] Client connected (captain="${captain || 'all'}") — total: ${sseClients.size}`);

    // Heartbeat every 25s to keep connection alive through proxies/NAT
    const heartbeat = setInterval(() => {
      try { res.write(`: heartbeat\n\n`); }
      catch { clearInterval(heartbeat); sseClients.delete(res); }
    }, 25_000);

    req.on('close', () => {
      clearInterval(heartbeat);
      sseClients.delete(res);
      console.log(`[SSE] Client disconnected — total: ${sseClients.size}`);
    });

    return; // keep alive
  }

  // ── NEW: GET /notifications — Poll last N notifications (no SSE needed) ──
  // Query: ?since=<id>&limit=<n>
  if (req.method === 'GET' && url.startsWith('/notifications')) {
    // #14 — gated by notificationEnabled config flag
    if (!bridgeConfig.notificationEnabled) {
      return jsonRes(res, 200, { ok: true, notifications: [], lastId: 0, disabled: true });
    }
    const params = new URLSearchParams(req.url.split('?')[1] || '');
    const since  = parseInt(params.get('since') || '0', 10);
    const limit  = Math.min(parseInt(params.get('limit') || '20', 10), 50);
    const result = notificationStore
      .filter(n => n.id > since)
      .slice(-limit);
    return jsonRes(res, 200, {
      ok: true,
      notifications: result,
      lastId: result.length ? result[result.length - 1].id : since,
    });
  }

  // ── NEW: GET /fingerprint/status — #8/#14 ────────────────────────────────
  // Returns { ok, deviceIp, devicePort, discovering, error }.
  // ok=true means the configured device IP:port is TCP-reachable.
  if (req.method === 'GET' && url === '/fingerprint/status') {
    if (!bridgeConfig.fingerprintEnabled) {
      return jsonRes(res, 200, { ok: false, enabled: false, deviceIp: bridgeConfig.esslDeviceIp, devicePort: bridgeConfig.esslDevicePort, discovering: false, error: 'Fingerprint module disabled in bridge config' });
    }
    if (!bridgeConfig.esslDeviceIp) {
      return jsonRes(res, 200, { ok: false, enabled: true, deviceIp: '', devicePort: bridgeConfig.esslDevicePort, discovering: fpDiscovering, error: 'No device configured — set IP via /fingerprint/config or trigger /fingerprint/reconnect to auto-discover' });
    }
    const reachable = await checkDeviceReachable(bridgeConfig.esslDeviceIp, bridgeConfig.esslDevicePort);
    return jsonRes(res, 200, {
      ok: reachable, enabled: true,
      deviceIp: bridgeConfig.esslDeviceIp, devicePort: bridgeConfig.esslDevicePort,
      discovering: fpDiscovering,
      error: reachable ? null : `Device ${bridgeConfig.esslDeviceIp}:${bridgeConfig.esslDevicePort} not reachable`,
    });
  }

  // ── NEW: POST /fingerprint/config — #8/#14 ───────────────────────────────
  // Body: { ip, port? }. Persists the eSSL device address to bridge-config.json.
  if (req.method === 'POST' && url === '/fingerprint/config') {
    let payload;
    try { payload = await readBody(req); }
    catch { return jsonRes(res, 400, { ok: false, error: 'Invalid JSON body' }); }
    const { ip, port } = payload;
    if (!ip || !String(ip).trim()) return jsonRes(res, 400, { ok: false, error: 'Missing ip' });
    bridgeConfig.esslDeviceIp   = String(ip).trim();
    bridgeConfig.esslDevicePort = parseInt(port) || 4370;
    saveConfig(bridgeConfig);
    const reachable = await checkDeviceReachable(bridgeConfig.esslDeviceIp, bridgeConfig.esslDevicePort);
    return jsonRes(res, 200, { ok: reachable, deviceIp: bridgeConfig.esslDeviceIp, devicePort: bridgeConfig.esslDevicePort, error: reachable ? null : 'Saved, but device not reachable yet' });
  }

  // ── NEW: POST /fingerprint/reconnect — #8/#14 ────────────────────────────
  // If a device IP is configured, re-checks connectivity. If not, runs
  // LAN auto-discovery on the eSSL default port (4370) in the background.
  if (req.method === 'POST' && url === '/fingerprint/reconnect') {
    if (!bridgeConfig.fingerprintEnabled) {
      return jsonRes(res, 200, { ok: false, error: 'Fingerprint module disabled in bridge config' });
    }
    if (bridgeConfig.esslDeviceIp) {
      const reachable = await checkDeviceReachable(bridgeConfig.esslDeviceIp, bridgeConfig.esslDevicePort);
      return jsonRes(res, 200, { ok: reachable, deviceIp: bridgeConfig.esslDeviceIp, devicePort: bridgeConfig.esslDevicePort, error: reachable ? null : 'Device not reachable' });
    }
    // No device configured — kick off background discovery, return immediately
    if (!fpDiscovering) {
      discoverDevice(bridgeConfig.esslDevicePort).then(ip => {
        if (ip) {
          bridgeConfig.esslDeviceIp = ip;
          saveConfig(bridgeConfig);
          console.log(`[fingerprint] auto-discovered device at ${ip}:${bridgeConfig.esslDevicePort}`);
        } else {
          console.log('[fingerprint] auto-discovery found no device on the local subnet');
        }
      });
    }
    return jsonRes(res, 200, { ok: false, discovering: true, error: 'No device configured — discovery started' });
  }

  // ── NEW: GET /fingerprint/last-scan — #8/#14 ─────────────────────────────
  // Query: ?since=<ms epoch>. Returns { scan: { userId, time } | null } —
  // only returns a scan if it happened AFTER `since`.
  if (req.method === 'GET' && url === '/fingerprint/last-scan') {
    if (!bridgeConfig.fingerprintEnabled) {
      return jsonRes(res, 200, { scan: null });
    }
    const params = new URLSearchParams(req.url.split('?')[1] || '');
    const since  = parseInt(params.get('since') || '0', 10);
    if (lastScan && lastScan.time > since) {
      return jsonRes(res, 200, { scan: lastScan });
    }
    return jsonRes(res, 200, { scan: null });
  }

  jsonRes(res, 404, { ok: false, error: 'Unknown endpoint' });
});

server.keepAliveTimeout = 65_000;   // longer than SSE heartbeat
server.headersTimeout   = 70_000;

server.listen(PORT, '0.0.0.0', async () => {   // 0.0.0.0 so LAN devices can reach it
  const info   = await getPrinterInfo();
  const hasExe = rawprintExists();
  const ifaces = os.networkInterfaces();
  const lanIps = Object.values(ifaces).flat()
    .filter(i => i.family === 'IPv4' && !i.internal)
    .map(i => i.address);

  console.log(`\n${'═'.repeat(62)}`);
  console.log(`  PS4 POS PRINT BRIDGE v3.1  –  Print + Notifications + Fingerprint`);
  console.log(`  Platform       : ${PLATFORM}`);
  console.log(`  LAN IPs        : ${lanIps.join(', ')}`);
  console.log(`  Port           : ${PORT}`);
  console.log(`  Default Printer: ${info.default || '(none)'}`);
  console.log(`  rawprint.exe   : ${hasExe ? '✓ READY' : '✗ NOT FOUND — building...'}`);
  console.log(`  ── Bridge Config (bridge-config.json) ───────────────`);
  console.log(`  Printer Enabled     : ${bridgeConfig.printerEnabled}`);
  console.log(`  Notification Enabled: ${bridgeConfig.notificationEnabled}`);
  console.log(`  Fingerprint Enabled : ${bridgeConfig.fingerprintEnabled}`);
  if (bridgeConfig.fingerprintEnabled) {
    console.log(`  eSSL Device         : ${bridgeConfig.esslDeviceIp || '(not set — will auto-discover)'}:${bridgeConfig.esslDevicePort}`);
  }
  console.log(`  ── Notification Endpoints ───────────────────────────`);
  console.log(`  POST /notify       — Kitchen sends "item ready"`);
  console.log(`  GET  /events       — SSE stream (Flutter + React)`);
  console.log(`  GET  /notifications — Poll fallback`);
  console.log(`  ── Fingerprint Endpoints ─────────────────────────────`);
  console.log(`  GET  /fingerprint/status     — connectivity check`);
  console.log(`  POST /fingerprint/config     — set device IP/port`);
  console.log(`  POST /fingerprint/reconnect  — reconnect / auto-discover`);
  console.log(`  GET  /fingerprint/last-scan  — poll for scan events`);
  info.list.forEach(p => console.log(`    • ${p}${p === info.default ? ' ← DEFAULT' : ''}`));
  console.log(`${'═'.repeat(62)}\n`);

  if (PLATFORM === 'win32' && !hasExe) buildRawprintExe();
});

server.on('error', e => {
  if (e.code === 'EADDRINUSE') console.error(`\n✗ Port ${PORT} in use.\n`);
  else console.error('Server error:', e);
  process.exit(1);
});
