// Impresoras USB (Windows): manda ESC/POS CRUDO a una cola de impresion de
// Windows por winspool (WritePrinter con datatype RAW), sin compartir la
// impresora y sin drivers del fabricante (la cola se crea con el driver
// "Generic / Text Only" sobre el puerto USB00x).
//
// Como el cerebrito es un exe de Node sin modulos nativos, el envio se hace
// lanzando PowerShell con un script que hace el P/Invoke. Es ~1-2s por papel,
// de sobra para comandas. El script se escribe una sola vez en la carpeta
// temporal y se reusa.
import { spawn } from 'node:child_process';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const PS1 = String.raw`param([string]$Impresora, [string]$Archivo)
$code = @'
using System;
using System.Runtime.InteropServices;
public class RawPrinter {
  [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
  public class DOCINFOA {
    [MarshalAs(UnmanagedType.LPStr)] public string pDocName;
    [MarshalAs(UnmanagedType.LPStr)] public string pOutputFile;
    [MarshalAs(UnmanagedType.LPStr)] public string pDataType;
  }
  [DllImport("winspool.Drv", EntryPoint="OpenPrinterA", SetLastError=true, CharSet=CharSet.Ansi)]
  public static extern bool OpenPrinter(string szPrinter, out IntPtr hPrinter, IntPtr pd);
  [DllImport("winspool.Drv", EntryPoint="ClosePrinter", SetLastError=true)]
  public static extern bool ClosePrinter(IntPtr hPrinter);
  [DllImport("winspool.Drv", EntryPoint="StartDocPrinterA", SetLastError=true, CharSet=CharSet.Ansi)]
  public static extern bool StartDocPrinter(IntPtr hPrinter, int level, [In] DOCINFOA di);
  [DllImport("winspool.Drv", EntryPoint="EndDocPrinter", SetLastError=true)]
  public static extern bool EndDocPrinter(IntPtr hPrinter);
  [DllImport("winspool.Drv", EntryPoint="StartPagePrinter", SetLastError=true)]
  public static extern bool StartPagePrinter(IntPtr hPrinter);
  [DllImport("winspool.Drv", EntryPoint="EndPagePrinter", SetLastError=true)]
  public static extern bool EndPagePrinter(IntPtr hPrinter);
  [DllImport("winspool.Drv", EntryPoint="WritePrinter", SetLastError=true)]
  public static extern bool WritePrinter(IntPtr hPrinter, byte[] pBytes, int dwCount, out int dwWritten);
  public static bool Send(string printer, byte[] bytes) {
    IntPtr h;
    if (!OpenPrinter(printer, out h, IntPtr.Zero)) return false;
    DOCINFOA di = new DOCINFOA();
    di.pDocName = "Comanda Gustito"; di.pDataType = "RAW";
    bool ok = false;
    if (StartDocPrinter(h, 1, di)) {
      if (StartPagePrinter(h)) {
        int written;
        ok = WritePrinter(h, bytes, bytes.Length, out written);
        EndPagePrinter(h);
      }
      EndDocPrinter(h);
    }
    ClosePrinter(h);
    return ok;
  }
}
'@
Add-Type -TypeDefinition $code
$bytes = [System.IO.File]::ReadAllBytes($Archivo)

# El spooler de Windows acepta los bytes AUNQUE la impresora este pausada,
# apagada o sin papel: WritePrinter devuelve exito y el papel nunca sale. Como
# el negocio marca el pedido en preparacion cuando "salio el papel", eso hacia
# creer que la cocina tenia la comanda sin tenerla. Se revisa antes y despues.
$malo = 'Paused|Offline|Error|PaperOut|PaperJam|NotAvailable|NoToner|OutOfMemory|DoorOpen'
$antes = Get-Printer -Name $Impresora -ErrorAction SilentlyContinue
if ($antes -and ("$($antes.PrinterStatus)" -match $malo)) {
  Write-Output ("FALLO impresora " + $antes.PrinterStatus); exit 0
}

if (-not [RawPrinter]::Send($Impresora, $bytes)) {
  Write-Output ("FALLO " + [Runtime.InteropServices.Marshal]::GetLastWin32Error()); exit 0
}

# Se espera a que el trabajo salga de la cola. Si se acaba el tiempo con la
# impresora sana se da por bueno a proposito: mas vale una comanda lenta que una
# comanda repetida.
$fin = (Get-Date).AddSeconds(10)
while ((Get-Date) -lt $fin) {
  $t = @(Get-PrintJob -PrinterName $Impresora -ErrorAction SilentlyContinue)
  if ($t.Count -eq 0) { Write-Output 'OK'; exit 0 }
  $trabado = $t | Where-Object { "$($_.JobStatus)" -match $malo } | Select-Object -First 1
  if ($trabado) { Write-Output ("FALLO trabajo " + $trabado.JobStatus); exit 0 }
  $est = Get-Printer -Name $Impresora -ErrorAction SilentlyContinue
  if ($est -and ("$($est.PrinterStatus)" -match $malo)) {
    Write-Output ("FALLO impresora " + $est.PrinterStatus); exit 0
  }
  Start-Sleep -Milliseconds 400
}
Write-Output 'OK'
`;

// Auto-detecta e INSTALA una impresora termica USB aunque Windows no le haya
// creado la cola: agrega el driver "Generic / Text Only" (que Windows ya trae) y
// crea la cola en el puerto USB correcto, sin que el usuario tenga que saber el
// puerto. Devuelve una linea: OK:<nombre>:<puerto> | YA:<nombre> | NOPORT | ERR:<msg>.
const PS_INSTALAR = String.raw`$ErrorActionPreference = 'Stop'
$virt = 'PDF|XPS|OneNote|Fax|Print to|Microsoft Shared|Send To'
try {
  if (-not (Get-PrinterDriver -Name 'Generic / Text Only' -ErrorAction SilentlyContinue)) {
    Add-PrinterDriver -Name 'Generic / Text Only'
  }
  $exist = Get-Printer -ErrorAction SilentlyContinue | Where-Object { $_.Name -notmatch $virt } | Select-Object -First 1
  if ($exist) { Write-Output "YA:$($exist.Name)"; exit 0 }
  $port = $null
  $devs = Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue | Where-Object {
    ($_.Class -eq 'Printer') -or ($_.InstanceId -like 'USBPRINT*') -or
    ($_.FriendlyName -match 'print|pos-?\d|receipt|thermal|termic')
  }
  foreach ($d in $devs) { if ($d.InstanceId -match '(USB\d{3})') { $port = $Matches[1]; break } }
  if (-not $port) {
    $p = Get-PrinterPort -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^USB\d{3}$' } | Select-Object -First 1
    if ($p) { $port = $p.Name }
  }
  if (-not $port) {
    $hay = $devs | Where-Object { $_.Status -eq 'OK' } | Select-Object -First 1
    if ($hay) {
      if (-not (Get-PrinterPort -Name 'USB001' -ErrorAction SilentlyContinue)) { try { Add-PrinterPort -Name 'USB001' } catch {} }
      if (Get-PrinterPort -Name 'USB001' -ErrorAction SilentlyContinue) { $port = 'USB001' }
    }
  }
  if (-not $port) { Write-Output 'NOPORT'; exit 0 }
  $name = 'POS-80'
  if (Get-Printer -Name $name -ErrorAction SilentlyContinue) { $name = 'POS-80-' + (Get-Random -Maximum 99) }
  Add-Printer -Name $name -DriverName 'Generic / Text Only' -PortName $port
  Write-Output ('OK:' + $name + ':' + $port)
} catch { Write-Output ('ERR:' + $_.Exception.Message) }
`;

let carpetaPs1: string | null = null;

function carpeta(): string {
  if (!carpetaPs1) carpetaPs1 = mkdtempSync(join(tmpdir(), 'cerebrito-usb-'));
  return carpetaPs1;
}

function rutaScript(): string {
  const c = carpeta();
  writeFileSync(join(c, 'raw.ps1'), PS1, 'utf8');
  return join(c, 'raw.ps1');
}

/**
 * Detecta e instala la impresora USB automaticamente (crea la cola de Windows).
 * Corre con permisos del usuario; crear la cola no siempre exige admin, pero si
 * Windows lo pide y no hay permiso, devuelve un error claro.
 */
export async function instalarImpresoraAuto(): Promise<{ ok: boolean; nombre?: string; mensaje: string }> {
  if (process.platform !== 'win32') return { ok: false, mensaje: 'La instalacion automatica solo funciona en Windows.' };
  const script = join(carpeta(), 'instalar.ps1');
  writeFileSync(script, PS_INSTALAR, 'utf8');
  try {
    const { salida } = await correrPowershell(['-File', script], 60000);
    const linea = salida.split(/\r?\n/).map((s) => s.trim()).filter(Boolean).pop() ?? '';
    if (linea.startsWith('OK:')) {
      const [, nombre] = linea.split(':');
      return { ok: true, nombre, mensaje: `Impresora instalada como "${nombre}". Ya puedes buscarla y probarla.` };
    }
    if (linea.startsWith('YA:')) {
      const nombre = linea.slice(3);
      return { ok: true, nombre, mensaje: `Windows ya tenia la impresora "${nombre}" instalada.` };
    }
    if (linea === 'NOPORT') {
      return { ok: false, mensaje: 'No detecte ninguna impresora conectada. Revisa que este PRENDIDA y bien conectada por USB, y vuelve a intentar.' };
    }
    if (linea.startsWith('ERR:')) {
      return { ok: false, mensaje: `Windows no dejo instalarla: ${linea.slice(4)}. Prueba abrir el Cerebrito como administrador.` };
    }
    return { ok: false, mensaje: 'No pude confirmar la instalacion. Revisa que la impresora este prendida.' };
  } catch (e) {
    return { ok: false, mensaje: `No pude ejecutar la instalacion: ${(e as Error).message}` };
  }
}

function correrPowershell(args: string[], timeoutMs: number): Promise<{ salida: string; codigo: number }> {
  return new Promise((resolve, reject) => {
    const p = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', ...args], {
      windowsHide: true,
    });
    let salida = '';
    let listo = false;
    const fin = (err?: Error, codigo = -1) => {
      if (listo) return;
      listo = true;
      clearTimeout(reloj);
      if (err) reject(err);
      else resolve({ salida, codigo });
    };
    const reloj = setTimeout(() => {
      p.kill();
      fin(new Error('PowerShell no respondio a tiempo'));
    }, timeoutMs);
    p.stdout.on('data', (d) => (salida += String(d)));
    p.stderr.on('data', (d) => (salida += String(d)));
    p.on('error', (e) => fin(e));
    p.on('close', (codigo) => fin(undefined, codigo ?? -1));
  });
}

/** Manda los bytes ESC/POS a la cola de impresion de Windows con ese nombre. */
export async function enviarUsb(nombreImpresora: string, datos: Buffer, timeoutMs = 20000): Promise<void> {
  if (process.platform !== 'win32') throw new Error('la impresion USB solo funciona en Windows');
  const script = rutaScript(); // inicializa la carpeta temporal y escribe raw.ps1 si hace falta
  const bin = join(carpetaPs1 as string, `t-${Date.now()}-${Math.random().toString(36).slice(2, 7)}.bin`);
  writeFileSync(bin, datos);
  try {
    const { salida } = await correrPowershell(['-File', script, '-Impresora', nombreImpresora, '-Archivo', bin], timeoutMs);
    // La ultima linea es el veredicto: 'OK' o 'FALLO ...'. Buscar 'OK' suelto en
    // toda la salida daria por bueno un fallo que lo mencione de pasada.
    const veredicto = salida.trim().split(/\r?\n/).filter(Boolean).pop() ?? '';
    if (veredicto.trim() !== 'OK') {
      throw new Error(`Windows no acepto el trabajo (${salida.trim().slice(0, 120) || 'sin detalle'}). ¿La impresora "${nombreImpresora}" existe y esta prendida?`);
    }
  } finally {
    try {
      rmSync(bin, { force: true });
    } catch {
      /* temporal; no importa */
    }
  }
}

/** Nombres de las colas de impresion de Windows, sin las virtuales (PDF, XPS, Fax...). */
export async function listarImpresorasUsb(): Promise<string[]> {
  if (process.platform !== 'win32') return [];
  const VIRTUALES = /pdf|xps|onenote|fax|print to/i;
  try {
    const { salida } = await correrPowershell(
      ['-Command', 'Get-Printer | Select-Object -ExpandProperty Name'],
      15000,
    );
    return salida
      .split(/\r?\n/)
      .map((s) => s.trim())
      .filter((s) => s.length > 0 && !VIRTUALES.test(s));
  } catch {
    return [];
  }
}
