"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handlePrint = handlePrint;
exports.kickDrawer = kickDrawer;
const child_process_1 = require("child_process");
const log_1 = require("../log");
function isCupsInterface(iface) {
    return typeof iface === 'string' && iface.startsWith('printer:');
}
function cupsQueueName(iface) {
    return iface.replace(/^printer:/, '').split('/')[0];
}
// Run a shell command, capture stdout/stderr. Used for lpstat / cupsenable
// diagnostics around the actual `lp` send.
function runCmd(cmd, args) {
    return new Promise((resolve) => {
        const proc = (0, child_process_1.spawn)(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
        let stdout = '';
        let stderr = '';
        proc.stdout.on('data', (b) => { stdout += b.toString(); });
        proc.stderr.on('data', (b) => { stderr += b.toString(); });
        proc.on('error', (e) => resolve({ code: -1, stdout, stderr: e.message }));
        proc.on('close', (code) => resolve({ code: code ?? -1, stdout, stderr }));
    });
}
// CUPS has two independent toggles per queue:
//   enabled (cupsenable / cupsdisable)  → controls whether queued jobs
//                                          actually get sent to the printer
//   accepting (cupsaccept / cupsreject) → controls whether new jobs are
//                                          taken in at all
// On any error CUPS auto-flips enabled → off; the queue silently piles up
// jobs (lp returns success!) and nothing ever prints. Re-enabling on every
// send means a one-time hiccup doesn't permanently brick the queue.
//
// We also log the queue state every send so the operator can see the
// printer's idle/printing/disabled status alongside the job submission.
async function ensureQueueReady(queue) {
    const [pState, aState] = await Promise.all([
        runCmd('lpstat', ['-p', queue]),
        runCmd('lpstat', ['-a', queue]),
    ]);
    log_1.log.info('print: queue state', {
        queue,
        enabled: pState.stdout.trim() || pState.stderr.trim(),
        accepting: aState.stdout.trim() || aState.stderr.trim(),
    });
    if (/disabled/i.test(pState.stdout)) {
        log_1.log.warn('print: queue is disabled — running cupsenable', { queue });
        const r = await runCmd('cupsenable', [queue]);
        if (r.code !== 0)
            log_1.log.warn('print: cupsenable failed', { queue, stderr: r.stderr.trim() });
    }
    if (/not accepting/i.test(aState.stdout)) {
        log_1.log.warn('print: queue is rejecting — running cupsaccept', { queue });
        const r = await runCmd('cupsaccept', [queue]);
        if (r.code !== 0)
            log_1.log.warn('print: cupsaccept failed', { queue, stderr: r.stderr.trim() });
    }
}
// Pipe a buffer to `lp -d <queue> -o raw`. macOS and Linux ship lp; on
// Windows this branch is unreachable because we'd never produce a
// `printer:` interface there. Resolves on success, rejects with stderr.
async function lpRawPrint(queue, buffer) {
    await ensureQueueReady(queue);
    return new Promise((resolve, reject) => {
        const proc = (0, child_process_1.spawn)('lp', ['-d', queue, '-o', 'raw'], { stdio: ['pipe', 'pipe', 'pipe'] });
        let stdout = '';
        let stderr = '';
        proc.stdout.on('data', (b) => { stdout += b.toString(); });
        proc.stderr.on('data', (b) => { stderr += b.toString(); });
        proc.on('error', (e) => reject(new Error(`lp spawn failed: ${e.message}`)));
        proc.on('close', (code) => {
            if (code === 0) {
                // lp prints "request id is QUEUE-123" — extract for diagnostics.
                const match = stdout.match(/request id is (\S+)/);
                resolve({ jobId: match?.[1] });
            }
            else {
                reject(new Error(`lp exited ${code}: ${stderr.trim() || 'no stderr'}`));
            }
        });
        proc.stdin.write(buffer);
        proc.stdin.end();
    });
}
async function handlePrint(config, payload) {
    try {
        // Lazy require so installs without printer hardware don't crash on
        // node-thermal-printer's optional native deps.
        // eslint-disable-next-line @typescript-eslint/no-var-requires
        const { ThermalPrinter, PrinterTypes } = require('node-thermal-printer');
        const cups = isCupsInterface(config.interface);
        const printer = new ThermalPrinter({
            type: (config.driver === 'star') ? PrinterTypes.STAR : PrinterTypes.EPSON,
            // For CUPS queues we never call execute() — interface is irrelevant.
            // Use a sentinel that NTP's File path accepts without errors so the
            // buffer-formatting methods all work.
            interface: cups ? '/dev/null' : config.interface,
            characterSet: config.characterSet,
            removeSpecialCharacters: config.removeSpecialCharacters || false,
            lineCharacter: '-',
            width: config.width || 48,
        });
        if (!cups) {
            const isConnected = await printer.isPrinterConnected();
            if (!isConnected) {
                return { ok: false, error: `printer not connected at ${config.interface}` };
            }
        }
        const copies = Math.max(1, payload.copies || 1);
        for (let c = 0; c < copies; c++) {
            for (const line of (payload.lines || [])) {
                if (!line || typeof line !== 'object')
                    continue;
                switch (line.kind) {
                    case 'text':
                        if (line.bold)
                            printer.bold(true);
                        if (line.size === 'large')
                            printer.setTextSize(1, 1);
                        else if (line.size === 'huge')
                            printer.setTextSize(2, 2);
                        else
                            printer.setTextNormal();
                        printer.println(String(line.text ?? ''));
                        if (line.bold)
                            printer.bold(false);
                        printer.setTextNormal();
                        break;
                    case 'feed':
                        printer.newLine();
                        for (let i = 1; i < (line.lines || 1); i++)
                            printer.newLine();
                        break;
                    case 'cut':
                        printer.cut();
                        break;
                    case 'barcode':
                        if (line.type === 'QR')
                            printer.printQR(String(line.data || ''));
                        else
                            printer.printBarcode(String(line.data || ''));
                        break;
                    case 'image':
                        // Fetch + raster-print logo. NTP's printImage() handles
                        // dithering + ESC/POS raster encoding. Fail soft — a missing
                        // logo shouldn't kill the receipt.
                        if (line.url) {
                            try {
                                const resp = await fetch(String(line.url));
                                if (!resp.ok)
                                    throw new Error(`HTTP ${resp.status}`);
                                const ab = await resp.arrayBuffer();
                                const tmpPath = `/tmp/hub-agent-logo-${Date.now()}.${(line.url.split('.').pop() || 'png').replace(/[^a-z0-9]/gi, '')}`;
                                require('fs').writeFileSync(tmpPath, Buffer.from(ab));
                                await printer.printImage(tmpPath);
                                try {
                                    require('fs').unlinkSync(tmpPath);
                                }
                                catch { }
                            }
                            catch (e) {
                                log_1.log.warn('print: image fetch/render failed', { url: line.url, error: e?.message });
                            }
                        }
                        break;
                }
            }
        }
        if (cups) {
            const queue = cupsQueueName(config.interface);
            const buffer = printer.getBuffer();
            log_1.log.info('print: lp -d ' + queue + ' -o raw', { bytes: buffer.length });
            const { jobId } = await lpRawPrint(queue, buffer);
            return { ok: true, ref: jobId || `print-${Date.now()}` };
        }
        const ok = await printer.execute();
        return ok
            ? { ok: true, ref: `print-${Date.now()}` }
            : { ok: false, error: 'printer execute() returned false' };
    }
    catch (e) {
        return { ok: false, error: e?.message || 'print failed' };
    }
}
// Drawer kick — the drawer is wired to the printer's RJ12 port. ESC/POS
// command 0x1B 0x70 0x00 0x32 0xFA opens drawer #1 with default timing.
// We piggyback on node-thermal-printer to dispatch it.
async function kickDrawer(config) {
    try {
        // eslint-disable-next-line @typescript-eslint/no-var-requires
        const { ThermalPrinter, PrinterTypes } = require('node-thermal-printer');
        const cups = isCupsInterface(config.interface);
        const printer = new ThermalPrinter({
            type: (config.driver === 'star') ? PrinterTypes.STAR : PrinterTypes.EPSON,
            interface: cups ? '/dev/null' : config.interface,
        });
        if (!cups) {
            const connected = await printer.isPrinterConnected();
            if (!connected)
                return { ok: false, error: 'printer not connected — drawer rides on printer' };
        }
        printer.openCashDrawer();
        if (cups) {
            const queue = cupsQueueName(config.interface);
            const buffer = printer.getBuffer();
            log_1.log.info('drawer: lp -d ' + queue + ' -o raw', { bytes: buffer.length });
            const { jobId } = await lpRawPrint(queue, buffer);
            return { ok: true, ref: jobId || `drawer-${Date.now()}` };
        }
        const ok = await printer.execute();
        return ok
            ? { ok: true, ref: `drawer-${Date.now()}` }
            : { ok: false, error: 'drawer kick execute() returned false' };
    }
    catch (e) {
        return { ok: false, error: e?.message || 'drawer kick failed' };
    }
}
//# sourceMappingURL=printer.js.map