"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.dispatchCommand = dispatchCommand;
exports.discoverDevicePaths = discoverDevicePaths;
exports.probeEndpoint = probeEndpoint;
exports.computeEndpointStatus = computeEndpointStatus;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const child_process_1 = require("child_process");
const printer_1 = require("./handlers/printer");
const nfc_reader_1 = require("./handlers/nfc-reader");
const log_1 = require("./log");
async function dispatchCommand(ctx, cmd) {
    // Agent-level capabilities — no endpoint required. Server-driven diagnostics
    // (ping, status) ride the same WS as device commands so the round-trip path
    // is identical to a real print/scan/kick.
    if (cmd.capability === 'ping') {
        const sentTs = cmd.data?.ts;
        return wrap(cmd, {
            ok: true,
            data: {
                pong: true,
                agentTs: Date.now(),
                receivedTs: sentTs,
                endpoints: ctx.endpoints.length,
            },
        });
    }
    if (cmd.capability === 'status') {
        return wrap(cmd, {
            ok: true,
            data: {
                endpoints: ctx.endpoints.map((e) => ({
                    name: e.name,
                    kind: e.kind,
                    capabilities: e.capabilities,
                })),
                endpointCount: ctx.endpoints.length,
            },
        });
    }
    if (cmd.capability === 'discover-paths') {
        return wrap(cmd, {
            ok: true,
            data: {
                platform: process.platform,
                candidates: discoverDevicePaths(ctx.endpoints),
            },
        });
    }
    const endpoint = (cmd.endpointName && ctx.endpoints.find((e) => e.name === cmd.endpointName)) ||
        ctx.endpoints.find((e) => e.capabilities.includes(cmd.capability));
    if (!endpoint) {
        return {
            type: 'response',
            commandId: cmd.commandId,
            ok: false,
            error: `No local endpoint for capability "${cmd.capability}"${cmd.endpointName ? ` (endpointName=${cmd.endpointName})` : ''}`,
        };
    }
    // Promoted from debug → info so operators can see commands landing
    // without changing log level. This is the canonical "did my Test reach
    // the agent" diagnostic line.
    log_1.log.info('dispatch', { commandId: cmd.commandId, capability: cmd.capability, endpoint: endpoint.name });
    try {
        switch (cmd.capability) {
            case 'print': {
                const cfg = (endpoint.config || {});
                log_1.log.info('print: starting', { interface: cfg?.interface, driver: cfg?.driver || 'epson(default)', lines: Array.isArray(cmd.data?.lines) ? cmd.data.lines.length : 0 });
                const result = await (0, printer_1.handlePrint)(cfg, cmd.data);
                log_1.log.info('print: result', { ok: result.ok, ref: result.ref, error: result.error });
                return wrap(cmd, result);
            }
            case 'drawer': {
                // Drawer typically rides the printer's RJ12 — find a printer endpoint
                // and kick via that, regardless of endpointName.
                const printerEndpoint = ctx.endpoints.find((e) => e.name === endpoint.config?.printerEndpoint) ||
                    ctx.endpoints.find((e) => e.kind === 'printer');
                if (!printerEndpoint) {
                    return wrap(cmd, { ok: false, error: 'no printer endpoint to kick the drawer through' });
                }
                log_1.log.info('drawer: kicking via printer', { printer: printerEndpoint.name });
                const result = await (0, printer_1.kickDrawer)(printerEndpoint.config);
                log_1.log.info('drawer: result', { ok: result.ok, ref: result.ref, error: result.error });
                return wrap(cmd, result);
            }
            case 'card-write': {
                log_1.log.info('card-write: starting');
                const result = await (0, nfc_reader_1.handleCardWrite)(cmd.data);
                log_1.log.info('card-write: result', { ok: result.ok, ref: result.ref, error: result.error });
                return wrap(cmd, result);
            }
            case 'card-erase': {
                log_1.log.info('card-erase: starting');
                const result = await (0, nfc_reader_1.handleCardErase)(cmd.data);
                log_1.log.info('card-erase: result', { ok: result.ok, ref: result.ref, error: result.error });
                return wrap(cmd, result);
            }
            case 'card-read': {
                log_1.log.info('card-read: starting');
                const result = await (0, nfc_reader_1.handleCardRead)(cmd.data);
                log_1.log.info('card-read: result', { ok: result.ok, ref: result.ref, error: result.error });
                return wrap(cmd, result);
            }
            default:
                return wrap(cmd, { ok: false, error: `Capability "${cmd.capability}" not implemented in hub agent` });
        }
    }
    catch (e) {
        log_1.log.error('dispatch failed', { commandId: cmd.commandId, msg: e?.message });
        return wrap(cmd, { ok: false, error: e?.message || 'dispatch error' });
    }
}
function discoverDevicePaths(currentEndpoints) {
    // Build a quick map of paths already claimed by an endpoint so the UI
    // can avoid offering duplicates. Same logic across platforms.
    const claimed = new Map();
    for (const ep of currentEndpoints) {
        const cfgPath = ep.config?.path;
        const cfgIface = ep.config?.interface;
        if (typeof cfgPath === 'string')
            claimed.set(cfgPath, ep.name);
        if (typeof cfgIface === 'string') {
            const m = cfgIface.match(/printer:(\/dev\/[^\s]+|[^\s]+)/);
            if (m)
                claimed.set(m[1], ep.name);
        }
    }
    if (process.platform === 'darwin')
        return discoverDarwin(claimed);
    if (process.platform === 'linux')
        return discoverLinux(claimed);
    return discoverWindows(claimed);
}
// ── Linux ────────────────────────────────────────────────────────────────────
function discoverLinux(claimed) {
    const out = [];
    function listDir(dir, prefix) {
        try {
            return fs.readdirSync(dir).filter((f) => prefix.test(f)).map((f) => path.join(dir, f)).sort();
        }
        catch {
            return [];
        }
    }
    function readUevent(sysClassDir, devName) {
        try {
            const ueventPath = path.join(sysClassDir, devName, 'device', 'uevent');
            if (!fs.existsSync(ueventPath))
                return {};
            const text = fs.readFileSync(ueventPath, 'utf-8');
            const meta = {};
            for (const line of text.split('\n')) {
                const eq = line.indexOf('=');
                if (eq < 0)
                    continue;
                const k = line.slice(0, eq);
                const v = line.slice(eq + 1);
                if (k === 'HID_NAME' || k === 'PRODUCT' || k === 'NAME')
                    meta.product = v;
                if (k === 'HID_ID' || k === 'MANUFACTURER')
                    meta.vendor = v;
            }
            return meta;
        }
        catch {
            return {};
        }
    }
    for (const p of listDir('/dev', /^hidraw\d+$/)) {
        const dev = path.basename(p);
        out.push({ path: p, hintKind: 'scanner', driveable: true, ...readUevent('/sys/class/hidraw', dev), inUseBy: claimed.get(p) });
    }
    for (const p of listDir('/dev/usb', /^lp\d+$/)) {
        const dev = path.basename(p);
        out.push({ path: p, hintKind: 'printer', driveable: true, ...readUevent('/sys/class/usbmisc', dev), inUseBy: claimed.get(p) });
    }
    for (const p of [...listDir('/dev', /^ttyUSB\d+$/), ...listDir('/dev', /^ttyACM\d+$/)]) {
        const dev = path.basename(p);
        out.push({ path: p, hintKind: 'serial', driveable: true, ...readUevent('/sys/class/tty', dev), inUseBy: claimed.get(p) });
    }
    return out;
}
// ── macOS ────────────────────────────────────────────────────────────────────
//
// macOS doesn't expose HID devices as readable device files (HID input is
// mediated by IOKit and requires native code). What we CAN enumerate:
//   • USB-serial:  /dev/cu.usbmodem*, /dev/cu.usbserial*, /dev/cu.SLAB_*
//   • CUPS printers: `lpstat -p` lists installed print queues; ESC/POS
//     printers can be driven via the `printer:<queue>` interface
//   • All connected USB devices: `system_profiler SPUSBDataType -json`
//     useful for telling the operator "we see your scanner, but on macOS
//     it has to be in serial / vendor mode for the agent to read it"
function discoverDarwin(claimed) {
    const out = [];
    // 1. Serial device files the agent can read directly. This covers BOTH
    //    • USB-CDC:        cu.usbmodem* / cu.usbserial* / cu.SLAB_* / cu.wch*
    //    • Bluetooth SPP:  cu.<DeviceName>  (a paired SPP scanner shows up as
    //      e.g. /dev/cu.BARCODESCANNER — NOT a usb* name, so the old
    //      USB-only filter silently dropped every Bluetooth scanner).
    //  We enumerate all /dev/cu.* and only exclude the known non-data ports
    //  (the Mac's own Bluetooth RFCOMM listener + serial debug console).
    const CU_DENYLIST = /^cu\.(Bluetooth-Incoming-Port|debug-console|wlan-debug)$/i;
    try {
        const cuPaths = fs.readdirSync('/dev')
            .filter((f) => f.startsWith('cu.') && !CU_DENYLIST.test(f))
            .map((f) => path.join('/dev', f))
            .sort();
        for (const p of cuPaths) {
            const base = path.basename(p);
            const isUsb = /^cu\.(usbmodem|usbserial|SLAB_USBtoUART|wchusbserial)/.test(base);
            out.push({
                path: p,
                hintKind: 'serial',
                driveable: true,
                inUseBy: claimed.get(p),
                // Name-based hint so the operator can tell a BT scanner apart from a
                // generic USB-serial dongle in the picker.
                product: isUsb ? undefined : base.replace(/^cu\./, ''),
                note: isUsb ? undefined : 'Bluetooth serial (SPP) port — pair the device in SPP mode, then attach it here.',
            });
        }
    }
    catch { /* /dev not readable — unlikely */ }
    // 2. CUPS print queues — interface = "printer:<queueName>"
    // Full path: launchd daemons run with a minimal PATH (/usr/bin:/bin) so
    // bare `lpstat` happens to work, but bare `system_profiler` (in /usr/sbin)
    // does NOT — that's the exact bug where the UI saw only printers and the
    // CLI saw everything. Use absolute paths everywhere to be safe.
    try {
        const lp = (0, child_process_1.execSync)('/usr/bin/lpstat -p 2>/dev/null', { encoding: 'utf-8' });
        for (const line of lp.split('\n')) {
            const m = line.match(/^printer\s+(\S+)/);
            if (!m)
                continue;
            const queueName = m[1];
            const ifacePath = `printer:${queueName}`;
            out.push({
                path: ifacePath,
                hintKind: 'printer',
                product: queueName,
                driveable: true,
                inUseBy: claimed.get(ifacePath),
                note: 'CUPS print queue',
            });
        }
    }
    catch { /* no CUPS / lpstat not present */ }
    // 3. All USB devices (informational). Surfaces HID scanners so the
    // operator knows they're plugged in even though we can't read them
    // directly — note explains how to make them readable.
    try {
        const json = (0, child_process_1.execSync)('/usr/sbin/system_profiler SPUSBDataType -json 2>/dev/null', { encoding: 'utf-8', maxBuffer: 4 * 1024 * 1024 });
        const data = JSON.parse(json);
        const tree = data?.SPUSBDataType || [];
        walkUsb(tree, claimed, out);
    }
    catch (e) {
        // Absolute path failed — log into the agent's stderr so the next time
        // someone reports "UI missing devices" the cause is visible.
        log_1.log.warn('macOS USB enumeration failed', { msg: e?.message });
    }
    return out;
}
function walkUsb(nodes, claimed, out) {
    // What we ignore — internal Mac infrastructure, not user peripherals.
    const skipPatterns = /hub|apple t2|apple internal|bluetooth usb host|ambient light|usb 3.[01] bus|usb 2.0 bus|usb31bus/i;
    for (const node of nodes || []) {
        const name = node?._name || '';
        const isContainer = Array.isArray(node?._items);
        if (name && !isContainer && !skipPatterns.test(name)) {
            // Classify by name keywords — but if nothing matches, still include
            // it with hintKind=unknown so the operator can see EVERYTHING the
            // OS sees. Many HID scanners identify as "USB HID Keyboard" or by
            // generic vendor strings; the prior filter dropped them silently.
            let hintKind = 'unknown';
            let note;
            if (/nfc|rfid|contactless|mifare|acr122|acr12|acr1255|pn53|pn532|smart\s?card|ccid|prox|hid\s?omnikey|omnikey/i.test(name)) {
                hintKind = 'nfc-reader';
                note = 'NFC / card reader detected. On macOS it is NOT directly driveable: a HID-keyboard reader must be switched to USB-COM mode (vendor barcode) to appear as /dev/cu.usbmodem*; a PC/SC reader (ACR122U-class) needs pcscd + nfc-pcsc and is best run on a Linux hub. Then attach it as kind "nfc-reader".';
            }
            else if (/scanner|barcode|imager|symbol|honeywell|zebra|datalogic/i.test(name)) {
                hintKind = 'scanner';
                note = 'Likely a barcode scanner. On macOS HID scanners are usually in keyboard mode — the agent can\'t read them directly. Either set the scanner to USB-COM mode (its manual has a setup barcode), or run the hub on Linux where /dev/hidraw* is readable.';
            }
            else if (/keyboard/i.test(name)) {
                // Many barcode scanners identify as "HID Keyboard" — surface them
                // explicitly so the operator can decide.
                hintKind = 'scanner';
                note = 'USB HID Keyboard — could be a real keyboard OR a barcode scanner in default keyboard mode. If it\'s a scanner, reconfigure it for USB-COM mode so it appears as /dev/cu.usbmodem*.';
            }
            else if (/printer|epson|star|bixolon|brother|pos[\s\d]?/i.test(name)) {
                hintKind = 'printer';
                note = 'Add the printer in macOS System Settings → Printers & Scanners so it appears as a CUPS queue, then drive it as printer:<queue-name> from this list.';
            }
            else if (/scale|weigh/i.test(name)) {
                hintKind = 'serial';
                note = 'USB scale — needs to expose a serial interface for the agent to read. Most do; check vendor docs.';
            }
            out.push({
                path: `usb:${name}`, // synthetic — informational, not a real device file
                hintKind,
                product: name,
                vendor: node?.manufacturer,
                note,
                driveable: false, // can't open a `usb:Name` path
            });
        }
        if (isContainer)
            walkUsb(node._items, claimed, out);
    }
}
// ── Windows ─────────────────────────────────────────────────────────────────
//
// Three Powershell queries cover the operator-relevant device set:
//   • Win32_SerialPort       — COM ports (USB-serial, scales, pole displays)
//   • Get-Printer            — installed printer queues (drive ESC/POS via
//                              `printer:<ShareName>` or the spooler)
//   • Get-PnpDevice -Class HIDClass — HID readers (scanners, RFID).
//     On Windows HID is exposed at \\.\HID#... which the agent can read via
//     a native module; for now we surface the device + a note.
function discoverWindows(claimed) {
    const out = [];
    function ps(script) {
        try {
            const raw = (0, child_process_1.execSync)(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"')}"`, { encoding: 'utf-8', maxBuffer: 4 * 1024 * 1024 });
            const trimmed = raw.trim();
            if (!trimmed)
                return [];
            const parsed = JSON.parse(trimmed);
            return Array.isArray(parsed) ? parsed : [parsed];
        }
        catch {
            return [];
        }
    }
    // 1. COM ports (serial) — driveable.
    for (const p of ps('Get-WmiObject Win32_SerialPort | Select-Object DeviceID, Description, ProviderType | ConvertTo-Json')) {
        if (!p?.DeviceID)
            continue;
        out.push({
            path: p.DeviceID,
            hintKind: 'serial',
            product: p.Description,
            driveable: true,
            inUseBy: claimed.get(p.DeviceID),
        });
    }
    // 2. Installed printers — addressable via the Windows print spooler.
    for (const p of ps('Get-Printer | Select-Object Name, ShareName, DriverName, PortName | ConvertTo-Json')) {
        if (!p?.Name)
            continue;
        const ifacePath = `printer:${p.Name}`;
        out.push({
            path: ifacePath,
            hintKind: 'printer',
            product: p.Name,
            vendor: p.DriverName,
            driveable: true,
            inUseBy: claimed.get(ifacePath),
            note: `Windows print queue (port=${p.PortName || '?'})`,
        });
    }
    // 3. HID-class devices (scanners, etc.) — informational, NOT driveable.
    for (const p of ps('Get-PnpDevice -Class HIDClass -PresentOnly | Where-Object { $_.Status -eq \'OK\' } | Select-Object FriendlyName, InstanceId | ConvertTo-Json')) {
        const name = p?.FriendlyName || '';
        if (!name)
            continue;
        if (/scanner|barcode|imager|reader|keyboard/i.test(name)) {
            out.push({
                path: `hid:${p.InstanceId || name}`,
                hintKind: 'scanner',
                product: name,
                driveable: false,
                note: 'Windows HID device — likely a barcode scanner in keyboard mode. Set the scanner to USB-COM (serial) mode via its setup barcode so it appears under COM ports above and the agent can read it.',
            });
        }
    }
    return out;
}
function wrap(cmd, r) {
    return {
        type: 'response',
        commandId: cmd.commandId,
        ok: r.ok,
        ref: r.ref,
        data: r.data,
        error: r.error,
    };
}
function probeEndpoint(ep) {
    const cfgPath = ep.config?.path;
    const iface = ep.config?.interface;
    // Raw device files (HID/serial scanners, NFC readers, USB lp): the OS
    // device node must exist. This one IS reliable — no node, no device.
    if (cfgPath) {
        try {
            return fs.existsSync(cfgPath)
                ? { health: 'online', detail: cfgPath }
                : { health: 'offline', detail: `${cfgPath} not present (unplugged?)` };
        }
        catch {
            return { health: 'offline', detail: `${cfgPath} unreadable` };
        }
    }
    if (iface) {
        const tcp = iface.match(/^tcp:\/\/([^:/]+):?(\d+)?/);
        if (tcp)
            return tcpProbe(tcp[1], Number(tcp[2] || 9100));
        if (iface.startsWith('printer:')) {
            return cupsProbe(iface.replace(/^printer:/, '').split('/')[0]);
        }
        return { health: 'unknown', detail: iface };
    }
    // Cash drawer kicks through a printer — it has no transport of its own.
    if (ep.kind === 'drawer')
        return { health: 'unknown', detail: 'kicks through a printer' };
    return { health: 'unknown', detail: 'no probeable transport' };
}
function tcpProbe(host, port) {
    // bash /dev/tcp is available on macOS (bash 3.2) and any Linux hub with
    // bash. A successful connect is real proof the printer answers.
    try {
        (0, child_process_1.execSync)(`bash -c ': < /dev/tcp/${host}/${port}' 2>/dev/null`, { timeout: 1200 });
        return { health: 'online', detail: `${host}:${port} reachable` };
    }
    catch {
        return { health: 'offline', detail: `${host}:${port} not answering` };
    }
}
function cupsProbe(queue) {
    const q = queue.replace(/[^A-Za-z0-9_.@-]/g, '');
    const run = (cmd) => {
        try {
            return (0, child_process_1.execSync)(cmd, { encoding: 'utf-8', timeout: 4000 });
        }
        catch (e) {
            return (e?.stdout?.toString?.() || '');
        }
    };
    const state = run(`lpstat -p ${q} 2>/dev/null`);
    if (!state.trim())
        return { health: 'offline', detail: `no CUPS queue "${queue}"` };
    if (/disabled|stopped/i.test(state))
        return { health: 'offline', detail: 'CUPS queue disabled/stopped — printer not responding' };
    const reasons = run(`lpstat -l -p ${q} 2>/dev/null`);
    if (/offline-report|connecting-to-device|waiting-for-printer|timed-out|paused|shutdown|cups-waiting/i.test(reasons)) {
        return { health: 'offline', detail: 'printer reported offline / not connected' };
    }
    // Enabled + idle + no offline reason. CUPS still cannot prove a USB/BT
    // printer is actually powered until a job is attempted, so this is the
    // honest answer: NOT a confirmed connection.
    return { health: 'unknown', detail: 'CUPS queue enabled — printer power/link not confirmed until a print runs' };
}
/**
 * Per-endpoint health for the configured endpoints. `liveReaderNames` are
 * scanner/nfc-reader endpoints whose read stream the running agent
 * currently holds open — for those, an open fd is the strongest possible
 * proof of connection, so it wins over a filesystem stat.
 */
function computeEndpointStatus(endpoints, liveReaderNames) {
    const out = {};
    for (const ep of endpoints || []) {
        if (!ep?.name)
            continue;
        if (liveReaderNames?.has(ep.name)) {
            out[ep.name] = 'online';
            continue;
        }
        out[ep.name] = probeEndpoint(ep).health;
    }
    return out;
}
//# sourceMappingURL=dispatcher.js.map