'use strict';

let iconv;
try { iconv = require('iconv-lite'); } catch (_) { iconv = null; }
const QRCode = require('qrcode');

const ESC = 0x1b;
const GS = 0x1d;

function encodeText(text) {
  if (iconv) return iconv.encode(String(text || ''), 'cp437');
  return Buffer.from(String(text || ''), 'binary');
}

function init() { return Buffer.from([ESC, 0x40]); }
function alignLeft() { return Buffer.from([ESC, 0x61, 0x00]); }
function alignCenter() { return Buffer.from([ESC, 0x61, 0x01]); }
function setLeftMargin(dots = 0) {
  const n = Math.max(0, Math.min(65535, Number(dots) || 0));
  return Buffer.from([GS, 0x4c, n & 0xff, (n >> 8) & 0xff]);
}
function selectCodePage(page = 0) { return Buffer.from([ESC, 0x74, page]); }
function feed(lines = 1) { return Buffer.from([ESC, 0x64, Math.max(0, Math.min(255, Number(lines) || 0))]); }
function cut() { return Buffer.from([ESC, 0x69]); }
function text(value = '') { return encodeText(value); }
function line(value = '') { return Buffer.concat([encodeText(value), Buffer.from('\n', 'ascii')]); }

function stripCutCommands(value = '') {
  return String(value || '')
    .replace(/\x1b[\x69\x6d]/g, '')
    .replace(/\x1dV[\x00-\x42]?/g, '');
}

function qrModel(model = 0x32) {
  return Buffer.from([GS, 0x28, 0x6b, 0x04, 0x00, 0x31, 0x41, model, 0x00]);
}

function qrSize(size = 5) {
  return Buffer.from([GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, Math.max(1, Math.min(16, size))]);
}

function qrErrorCorrection(level = 48) {
  const normalized = typeof level === 'string'
    ? ({ L: 48, M: 49, Q: 50, H: 51 }[level.toUpperCase()] || 48)
    : level;
  return Buffer.from([GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, Math.max(48, Math.min(51, normalized))]);
}

function qrStore(data) {
  const payload = Buffer.from(String(data || ''), 'utf8');
  const length = payload.length + 3;
  return Buffer.concat([
    Buffer.from([GS, 0x28, 0x6b, length & 0xff, (length >> 8) & 0xff, 0x31, 0x50, 0x30]),
    payload,
  ]);
}

function qrPrint() {
  return Buffer.from([GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30]);
}

function normalizeQrOptions(options = {}) {
  const paperWidthMm = Number(options.qrPaperWidth || options.paperWidthMm) <= 58 ? 58 : 80;
  const mode = ['native', 'raster', 'auto'].includes(String(options.qrMode || options.mode || '').toLowerCase())
    ? String(options.qrMode || options.mode).toLowerCase()
    : 'auto';
  const qrDotSize = Number(options.qrSize) || (paperWidthMm <= 58 ? 5 : 6);
  const errorCorrection = options.qrErrorCorrection || options.errorCorrection || 'L';
  return {
    paperWidthMm,
    mode,
    qrDotSize: Math.max(1, Math.min(16, qrDotSize)),
    errorCorrection,
    feedBefore: Math.max(0, Math.min(8, Number(options.qrFeedBefore ?? options.feedBefore ?? 1))),
    feedAfter: Math.max(0, Math.min(8, Number(options.qrFeedAfter ?? options.feedAfter ?? 3))),
    leftMarginDots: options.leftMarginDots,
  };
}

function buildQrNativeEscPosBuffer(data, options = {}) {
  const normalized = normalizeQrOptions(options);
  const paperWidthMm = normalized.paperWidthMm;
  const marginDots = options.leftMarginDots ?? (paperWidthMm <= 58 ? 24 : 40);
  return Buffer.concat([
    selectCodePage(0),
    setLeftMargin(marginDots),
    alignCenter(),
    qrModel(0x32),
    qrSize(normalized.qrDotSize),
    qrErrorCorrection(normalized.errorCorrection),
    feed(normalized.feedBefore),
    qrStore(data),
    qrPrint(),
    feed(normalized.feedAfter),
    setLeftMargin(0),
    alignLeft(),
  ]);
}

function buildQrMatrix(data, errorCorrection = 'M') {
  const value = String(data || '').trim();
  if (!value) throw new Error('qr_data_required');
  const qr = QRCode.create(value, { errorCorrectionLevel: String(errorCorrection || 'M').toUpperCase() });
  const size = qr.modules.size;
  return Array.from({ length: size }, (_, y) =>
    Array.from({ length: size }, (_, x) => (qr.modules.get(x, y) ? 1 : 0))
  );
}

function matrixToRasterBytes(matrix, scale = 4, quiet = 4) {
  const modules = matrix.length + quiet * 2;
  const width = modules * scale;
  const height = modules * scale;
  const bytesPerRow = Math.ceil(width / 8);
  const image = Buffer.alloc(bytesPerRow * height, 0);
  for (let my = 0; my < matrix.length; my++) {
    for (let mx = 0; mx < matrix.length; mx++) {
      if (!matrix[my][mx]) continue;
      for (let sy = 0; sy < scale; sy++) {
        for (let sx = 0; sx < scale; sx++) {
          const x = (mx + quiet) * scale + sx;
          const y = (my + quiet) * scale + sy;
          image[y * bytesPerRow + (x >> 3)] |= 0x80 >> (x & 7);
        }
      }
    }
  }
  return { image, widthBytes: bytesPerRow, height };
}

function buildQrRasterEscPosBuffer(data, options = {}) {
  const normalized = normalizeQrOptions(options);
  const scale = Math.max(3, Math.min(8, normalized.qrDotSize));
  const matrix = buildQrMatrix(data, normalized.errorCorrection);
  const raster = matrixToRasterBytes(matrix, scale, 4);
  const xL = raster.widthBytes & 0xff;
  const xH = (raster.widthBytes >> 8) & 0xff;
  const yL = raster.height & 0xff;
  const yH = (raster.height >> 8) & 0xff;
  return Buffer.concat([
    selectCodePage(0),
    setLeftMargin(0),
    alignCenter(),
    feed(normalized.feedBefore),
    Buffer.from([GS, 0x76, 0x30, 0x00, xL, xH, yL, yH]),
    raster.image,
    feed(normalized.feedAfter),
    alignLeft(),
  ]);
}

function buildQrEscPosBuffer(data, options = {}) {
  const normalized = normalizeQrOptions(options);
  if (normalized.mode === 'raster') return buildQrRasterEscPosBuffer(data, options);
  return buildQrNativeEscPosBuffer(data, options);
}

function buildPlainTicketBuffer({ ticketText }) {
  return Buffer.concat([
    init(),
    setLeftMargin(0),
    selectCodePage(0),
    alignLeft(),
    text(stripCutCommands(ticketText)),
    feed(5),
    cut(),
    setLeftMargin(0),
    alignLeft(),
  ]);
}

function buildTicketBuffer({ cleanedTicketText, qrData, config, total }) {
  const isWhatsapp = config.mode === 'whatsapp';
  const parts = [
    init(),
    setLeftMargin(0),
    selectCodePage(0),
    alignLeft(),
    text(stripCutCommands(cleanedTicketText)),
    alignCenter(),
  ];

  const introLines = isWhatsapp
    ? [config.line1 || 'Si requiere factura, Escanee el QR para facturar por whatsapp.']
    : [config.line1, config.line2];
  for (const value of introLines) {
    if (value) parts.push(line(value));
  }

  parts.push(
    buildQrEscPosBuffer(qrData, {
      paperWidthMm: config.qrPaperWidth || config.paperWidthMm,
      qrSize: config.qrSize || (Number(config.paperWidthMm) <= 58 ? config.qrSize58 : config.qrSize80),
      qrMode: config.qrMode,
      qrErrorCorrection: config.qrErrorCorrection,
      qrFeedBefore: config.qrFeedBefore,
      qrFeedAfter: config.qrFeedAfter,
    }),
    alignCenter()
  );

  if (isWhatsapp) {
    if (config.whatsappNumber) parts.push(line(`WHATSAPP: ${config.whatsappNumber}`));
    if (total !== null && total !== undefined) parts.push(line(`TOTAL: ${Number(total).toFixed(2)}`));
    if (config.validityText) parts.push(line(`VIGENCIA: ${config.validityText}`));
  } else {
    if (config.billingCode) parts.push(line(`CODIGO FACTURACION: ${config.billingCode}`));
    if (config.branchName) parts.push(line(`SUCURSAL: ${config.branchName}`));
    if (config.validityText) parts.push(line(`VIGENCIA: ${config.validityText}`));
  }

  parts.push(
    Buffer.from([ESC, 0x4d, 0x01]),
    line(`${config.line3 ? config.line3 + '\n' : ''}FACIL, RAPIDO Y SIN COMPLICACIONES`),
    Buffer.from([ESC, 0x4d, 0x00]),
    alignLeft(),
    feed(5),
    cut()
  );
  return Buffer.concat(parts);
}

function findCommandIndexes(buffer, command) {
  const indexes = [];
  let offset = 0;
  while (offset < buffer.length) {
    const found = buffer.indexOf(command, offset);
    if (found === -1) break;
    indexes.push(found);
    offset = found + 1;
  }
  return indexes;
}

function detectQrInsertion(buffer) {
  if (!Buffer.isBuffer(buffer)) {
    return { inserted: false, mode: null, qrBytesLength: 0, insertedBeforeCut: false };
  }
  const nativeStore = Buffer.from([GS, 0x28, 0x6b]);
  const nativePrint = Buffer.from([GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30]);
  const rasterPrint = Buffer.from([GS, 0x76, 0x30]);
  const escCuts = [
    ...findCommandIndexes(buffer, Buffer.from([ESC, 0x69])),
    ...findCommandIndexes(buffer, Buffer.from([ESC, 0x6d])),
    ...findCommandIndexes(buffer, Buffer.from([GS, 0x56])),
  ];
  const lastCutIndex = escCuts.length ? Math.max(...escCuts) : -1;
  const nativeStoreIndex = buffer.indexOf(nativeStore);
  const nativePrintIndex = buffer.indexOf(nativePrint);
  const rasterIndex = buffer.indexOf(rasterPrint);
  const mode = nativeStoreIndex >= 0 && nativePrintIndex > nativeStoreIndex
    ? 'native'
    : rasterIndex >= 0
      ? 'raster'
      : null;
  const qrIndex = mode === 'native' ? nativeStoreIndex : rasterIndex;
  const inserted = qrIndex >= 0;
  return {
    inserted,
    mode,
    qrBytesLength: inserted && mode === 'native'
      ? Math.max(0, nativePrintIndex + nativePrint.length - nativeStoreIndex)
      : inserted
        ? Math.max(0, buffer.length - rasterIndex)
        : 0,
    insertedBeforeCut: inserted && (lastCutIndex === -1 || qrIndex < lastCutIndex),
    qrIndex,
    lastCutIndex,
  };
}

module.exports = {
  init,
  alignLeft,
  alignCenter,
  setLeftMargin,
  selectCodePage,
  feed,
  cut,
  qrModel,
  qrSize,
  qrErrorCorrection,
  qrStore,
  qrPrint,
  text,
  line,
  normalizeQrOptions,
  buildQrNativeEscPosBuffer,
  buildQrRasterEscPosBuffer,
  buildQrMatrix,
  buildQrEscPosBuffer,
  buildPlainTicketBuffer,
  buildTicketBuffer,
  detectQrInsertion,
  stripCutCommands,
  encodeText,
};
