"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.NfcCardReader = void 0;
exports.handleCardWrite = handleCardWrite;
exports.handleCardErase = handleCardErase;
exports.handleCardRead = handleCardRead;
const fs = __importStar(require("fs"));
const events_1 = require("events");
const log_1 = require("../log");
const serial_tty_1 = require("./serial-tty");
// NFC / RFID card-reader handler. Two halves, mirroring how the codebase
// already treats peripherals:
//
//   READ  (event-driven, like handlers/scanner.ts)
//     The overwhelmingly common deployment is a USB NFC reader running in
//     keyboard-wedge / USB-COM mode (ACR122U "keyboard", Tera, generic
//     125kHz EM readers). On a tap it types the card's hardware UID
//     followed by Enter — byte-for-byte the same shape the barcode scanner
//     produces. So the read path is the scanner's HID/serial line reader,
//     just emitting `card-tap` { cardUid } instead of `scan` { code }.
//
//   WRITE / ERASE (command-driven, like handlers/printer.ts)
//     Writing a payload to / wiping a card needs PC/SC + APDUs (ACR122U
//     class readers). That requires the optional native `nfc-pcsc` module.
//     We lazy-require it the same way printer.ts lazy-requires
//     node-thermal-printer: installs without the native dep (or readers
//     that only do keyboard-wedge) still run — write/erase just returns a
//     clear, actionable error instead of crashing the agent.
//
// The server/UI only NEED the read path for login + card registration.
// Write/erase is the admin "encode this card" convenience.
const HID_KEYMAP = {
    0x04: ['a', 'A'], 0x05: ['b', 'B'], 0x06: ['c', 'C'], 0x07: ['d', 'D'],
    0x08: ['e', 'E'], 0x09: ['f', 'F'], 0x0a: ['g', 'G'], 0x0b: ['h', 'H'],
    0x0c: ['i', 'I'], 0x0d: ['j', 'J'], 0x0e: ['k', 'K'], 0x0f: ['l', 'L'],
    0x10: ['m', 'M'], 0x11: ['n', 'N'], 0x12: ['o', 'O'], 0x13: ['p', 'P'],
    0x14: ['q', 'Q'], 0x15: ['r', 'R'], 0x16: ['s', 'S'], 0x17: ['t', 'T'],
    0x18: ['u', 'U'], 0x19: ['v', 'V'], 0x1a: ['w', 'W'], 0x1b: ['x', 'X'],
    0x1c: ['y', 'Y'], 0x1d: ['z', 'Z'],
    0x1e: ['1', '!'], 0x1f: ['2', '@'], 0x20: ['3', '#'], 0x21: ['4', '$'],
    0x22: ['5', '%'], 0x23: ['6', '^'], 0x24: ['7', '&'], 0x25: ['8', '*'],
    0x26: ['9', '('], 0x27: ['0', ')'],
    0x2d: ['-', '_'], 0x2e: ['=', '+'], 0x33: [';', ':'], 0x34: ["'", '"'],
    0x36: [',', '<'], 0x37: ['.', '>'], 0x38: ['/', '?'],
};
const KEY_ENTER = 0x28;
// Reader-as-event-source. Identical transport handling to ScannerReader;
// only the emitted event name/shape differs (`card-tap` / { cardUid }).
class NfcCardReader extends events_1.EventEmitter {
    constructor(opts) {
        super();
        this.buffer = '';
        this.stream = null;
        this.closed = false;
        this.opts = opts;
        this.mode = /^\/dev\/hidraw\d+$/.test(opts.path) ? 'hid' : 'serial';
    }
    start() {
        try {
            // Bluetooth/serial NFC readers emit CR-terminated UIDs — without raw
            // mode the macOS line discipline never flushes them to us.
            if (this.mode === 'serial')
                (0, serial_tty_1.setSerialRaw)(this.opts.path);
            this.stream = fs.createReadStream(this.opts.path);
            this.stream.on('data', (chunk) => this.onChunk(chunk));
            this.stream.on('error', (e) => {
                log_1.log.error(`nfc-reader ${this.opts.endpointName} error`, { msg: e.message });
                this.emit('error', e);
            });
            this.stream.on('close', () => {
                if (!this.closed)
                    this.emit('disconnected');
            });
            log_1.log.info('nfc-reader attached', { endpoint: this.opts.endpointName, path: this.opts.path, mode: this.mode });
        }
        catch (e) {
            log_1.log.error(`nfc-reader ${this.opts.endpointName} open failed`, { msg: e.message });
            throw e;
        }
    }
    stop() {
        this.closed = true;
        this.stream?.destroy();
    }
    onChunk(chunk) {
        if (this.mode === 'serial')
            this.parseSerial(chunk);
        else
            this.parseHid(chunk);
    }
    commit(raw) {
        const uid = raw.trim();
        if (!uid)
            return;
        if (uid.length >= (this.opts.minLength ?? 4)) {
            this.emit('card-tap', uid);
        }
    }
    parseSerial(chunk) {
        this.buffer += chunk.toString('utf-8');
        let idx;
        while ((idx = this.buffer.search(/[\r\n]/)) !== -1) {
            const code = this.buffer.slice(0, idx);
            this.buffer = this.buffer.slice(idx + 1).replace(/^[\r\n]/, '');
            this.commit(code);
        }
    }
    parseHid(chunk) {
        for (let i = 0; i + 7 < chunk.length; i += 8) {
            const modifier = chunk[i];
            const keycode = chunk[i + 2];
            if (keycode === 0)
                continue;
            if (keycode === KEY_ENTER) {
                const code = this.buffer;
                this.buffer = '';
                this.commit(code);
                continue;
            }
            const map = HID_KEYMAP[keycode];
            if (!map)
                continue;
            const shift = (modifier & 0x22) !== 0;
            this.buffer += shift ? map[1] : map[0];
        }
    }
}
exports.NfcCardReader = NfcCardReader;
const PCSC_HINT = 'PC/SC write/erase needs the optional "nfc-pcsc" native module and a PC/SC ' +
    'reader (e.g. ACR122U). Install it on the hub (npm i nfc-pcsc) or use a ' +
    'reader in keyboard-wedge mode for tap-to-login only (no encode).';
/**
 * Wait for one card on a PC/SC reader, run `fn(reader, card)`, resolve.
 * Lazy-requires nfc-pcsc so a hub without it (or without a PC/SC reader)
 * still boots — exactly the printer.ts node-thermal-printer pattern.
 */
function withCard(timeoutMs, fn) {
    return new Promise((resolve) => {
        let nfcPcsc;
        try {
            // eslint-disable-next-line @typescript-eslint/no-var-requires
            nfcPcsc = require('nfc-pcsc');
        }
        catch {
            resolve({ ok: false, error: PCSC_HINT });
            return;
        }
        let settled = false;
        const finish = (r) => {
            if (settled)
                return;
            settled = true;
            try {
                nfc?.close?.();
            }
            catch { /* noop */ }
            resolve(r);
        };
        const timer = setTimeout(() => finish({ ok: false, error: `no card presented within ${timeoutMs}ms` }), timeoutMs);
        const { NFC } = nfcPcsc;
        const nfc = new NFC();
        nfc.on('reader', (reader) => {
            reader.on('card', async (card) => {
                try {
                    const data = await fn(reader, card);
                    clearTimeout(timer);
                    finish({ ok: true, data });
                }
                catch (e) {
                    clearTimeout(timer);
                    finish({ ok: false, error: e?.message || 'card operation failed' });
                }
            });
            reader.on('error', (e) => log_1.log.warn('nfc-reader pcsc error', { msg: e?.message }));
        });
        nfc.on('error', (e) => {
            clearTimeout(timer);
            finish({ ok: false, error: e?.message || 'PC/SC init failed' });
        });
    });
}
// NTAG/Mifare Ultralight user memory starts at page 4; ASCII payload, NUL-padded.
const USER_PAGE = 4;
async function handleCardWrite(payload) {
    const text = String(payload?.text ?? payload?.data ?? '');
    const timeoutMs = Number(payload?.timeoutMs) || 15000;
    const r = await withCard(timeoutMs, async (reader, card) => {
        const bytes = Buffer.from(text, 'utf-8');
        const pages = Math.ceil((bytes.length + 1) / 4); // +1 NUL terminator
        const padded = Buffer.alloc(pages * 4);
        bytes.copy(padded);
        for (let p = 0; p < pages; p++) {
            await reader.write(USER_PAGE + p, padded.subarray(p * 4, p * 4 + 4), 4);
        }
        return { uid: card?.uid, bytesWritten: bytes.length };
    });
    return r.ok
        ? { ok: true, ref: `card-write-${Date.now()}`, data: r.data }
        : { ok: false, error: r.error };
}
async function handleCardErase(payload) {
    const pages = Math.max(1, Number(payload?.pages) || 8);
    const timeoutMs = Number(payload?.timeoutMs) || 15000;
    const zero = Buffer.alloc(4);
    const r = await withCard(timeoutMs, async (reader, card) => {
        for (let p = 0; p < pages; p++) {
            await reader.write(USER_PAGE + p, zero, 4);
        }
        return { uid: card?.uid, pagesCleared: pages };
    });
    return r.ok
        ? { ok: true, ref: `card-erase-${Date.now()}`, data: r.data }
        : { ok: false, error: r.error };
}
// One-shot read via PC/SC (admin "read this card now" — distinct from the
// always-on keyboard-wedge tap stream).
async function handleCardRead(payload) {
    const timeoutMs = Number(payload?.timeoutMs) || 15000;
    const r = await withCard(timeoutMs, async (_reader, card) => ({ uid: card?.uid }));
    return r.ok
        ? { ok: true, ref: `card-read-${Date.now()}`, data: r.data }
        : { ok: false, error: r.error };
}
//# sourceMappingURL=nfc-reader.js.map