"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const config_1 = require("./config");
const ws_client_1 = require("./ws-client");
const dispatcher_1 = require("./dispatcher");
const scanner_1 = require("./handlers/scanner");
const nfc_reader_1 = require("./handlers/nfc-reader");
const log_1 = require("./log");
// Hub agent entry point. Boots, opens the WS to appengine, attaches local
// scanner readers (push events up the WS), routes inbound commands to local
// handlers, posts responses back. Reconnect + heartbeat live in HubWsClient.
//
// Run via:
//   npx ts-node src/index.ts                  (dev)
//   node dist/index.js                        (prod)
//   systemctl start appmint-hub               (production install)
async function main() {
    const config = (0, config_1.loadConfig)(parseConfigArg());
    (0, log_1.setLogLevel)(config.logLevel);
    log_1.log.info('hub-agent boot', {
        hubName: config.hubName,
        server: config.serverUrl,
        endpoints: config.endpoints.map((e) => ({ name: e.name, kind: e.kind })),
    });
    const ws = new ws_client_1.HubWsClient(config);
    const ctx = { endpoints: config.endpoints };
    ws.on('command', async (cmd) => {
        const response = await (0, dispatcher_1.dispatchCommand)(ctx, cmd);
        ws.send(response);
    });
    // Real per-endpoint health on every heartbeat. `unknown` (e.g. a CUPS
    // queue that exists but whose printer power can't be confirmed) is
    // reported as `offline` — we never tell the UI a device is connected
    // unless we can prove it answers.
    ws.setEndpointStatusProvider(() => {
        const health = (0, dispatcher_1.computeEndpointStatus)(ctx.endpoints);
        const out = {};
        for (const [name, h] of Object.entries(health)) {
            out[name] = h === 'online' ? 'online' : 'offline';
        }
        return out;
    });
    /**
     * Attach scanner readers for the current endpoints list. Returns the
     * started readers so the caller can stop them on hot-reload.
     */
    function attachScanners(endpoints) {
        const started = [];
        for (const ep of endpoints) {
            if (ep.kind !== 'scanner')
                continue;
            const path = ep.config?.path || ep.path;
            if (!path) {
                log_1.log.warn('scanner endpoint has no path; skipping', { name: ep.name });
                continue;
            }
            const reader = new scanner_1.ScannerReader({ path, endpointName: ep.name, minLength: 4 });
            reader.on('scan', (code) => {
                log_1.log.info('scan', { endpoint: ep.name, code });
                ws.emitEvent('scan', ep.name, { code });
            });
            reader.on('error', (e) => {
                ws.emitEvent('printer-error', ep.name, { error: e.message });
            });
            try {
                reader.start();
                started.push(reader);
            }
            catch (e) {
                log_1.log.error(`scanner ${ep.name} failed to start`, { msg: e.message });
            }
        }
        return started;
    }
    /**
     * Attach NFC card readers (kind === 'nfc-reader'). Same transport as a
     * scanner — a keyboard-wedge reader types the UID on tap — but emits a
     * `card-tap` event so the server/UI can tell a staff badge tap apart from
     * a product barcode scan.
     */
    function attachCardReaders(endpoints) {
        const started = [];
        for (const ep of endpoints) {
            if (ep.kind !== 'nfc-reader')
                continue;
            const path = ep.config?.path || ep.path;
            if (!path) {
                log_1.log.warn('nfc-reader endpoint has no path; skipping', { name: ep.name });
                continue;
            }
            const reader = new nfc_reader_1.NfcCardReader({ path, endpointName: ep.name, minLength: 4 });
            reader.on('card-tap', (cardUid) => {
                log_1.log.info('card-tap', { endpoint: ep.name, cardUid });
                ws.emitEvent('card-tap', ep.name, { cardUid });
            });
            reader.on('error', (e) => {
                ws.emitEvent('printer-error', ep.name, { error: e.message });
            });
            try {
                reader.start();
                started.push(reader);
            }
            catch (e) {
                log_1.log.error(`nfc-reader ${ep.name} failed to start`, { msg: e.message });
            }
        }
        return started;
    }
    let scanners = attachScanners(config.endpoints);
    let cardReaders = attachCardReaders(config.endpoints);
    /**
     * Server-pushed config update — replace / merge / remove endpoints.
     * Hot-swaps the dispatcher context, restarts scanner readers, persists
     * the new endpoints to config.json, and emits a fresh `hello` so the
     * server's auto-discovery upserts the matching device_config records.
     */
    ws.on('update-config', async (msg) => {
        try {
            let next;
            if (msg.action === 'replace') {
                next = Array.isArray(msg.endpoints) ? msg.endpoints : [];
            }
            else if (msg.action === 'merge') {
                const incoming = Array.isArray(msg.endpoints) ? msg.endpoints : [];
                const byName = new Map();
                for (const e of ctx.endpoints)
                    byName.set(e.name, e);
                for (const e of incoming)
                    byName.set(e.name, e);
                next = Array.from(byName.values());
            }
            else if (msg.action === 'remove') {
                const drop = new Set(msg.names || []);
                next = ctx.endpoints.filter((e) => !drop.has(e.name));
            }
            else {
                ws.send({
                    type: 'config-updated',
                    reqId: msg.reqId,
                    ok: false,
                    endpoints: ctx.endpoints,
                    error: `unknown action "${msg.action}"`,
                });
                return;
            }
            // Stop existing readers, swap context, restart for the new list.
            scanners.forEach((s) => s.stop());
            cardReaders.forEach((r) => r.stop());
            ctx.endpoints = next;
            scanners = attachScanners(next);
            cardReaders = attachCardReaders(next);
            // Persist so a process restart preserves the new endpoints.
            try {
                const written = (0, config_1.writeConfig)({ ...config, endpoints: next });
                log_1.log.info('hub: config updated and persisted', { path: written, count: next.length });
            }
            catch (e) {
                log_1.log.warn('hub: persist failed (in-memory update still applied)', { msg: e.message });
            }
            // Ack to the server caller.
            ws.send({
                type: 'config-updated',
                reqId: msg.reqId,
                ok: true,
                endpoints: next,
            });
            // Re-announce so the server's auto-discovery picks up the new shape.
            ws.send({
                type: 'hello',
                hubName: config.hubName,
                apiKey: config.apiKey,
                version: undefined,
                endpoints: next,
            });
        }
        catch (e) {
            log_1.log.error('hub: update-config failed', { msg: e.message });
            ws.send({
                type: 'config-updated',
                reqId: msg.reqId,
                ok: false,
                endpoints: ctx.endpoints,
                error: e?.message || 'apply failed',
            });
        }
    });
    ws.start();
    // Graceful shutdown.
    const shutdown = (sig) => {
        log_1.log.info('hub-agent shutdown', { signal: sig });
        ws.stop();
        scanners.forEach((s) => s.stop());
        cardReaders.forEach((r) => r.stop());
        setTimeout(() => process.exit(0), 200);
    };
    process.on('SIGINT', () => shutdown('SIGINT'));
    process.on('SIGTERM', () => shutdown('SIGTERM'));
}
function parseConfigArg() {
    for (const a of process.argv.slice(2)) {
        if (a.startsWith('--config='))
            return a.slice('--config='.length);
    }
    return undefined;
}
main().catch((e) => {
    console.error('hub-agent fatal:', e?.message || e);
    process.exit(1);
});
//# sourceMappingURL=index.js.map