// Cola de impresion PACIENTE. Habla ESC/POS crudo por TCP al puerto 9100 (el
// estandar de las impresoras termicas de red).
// Robustez offline: si la impresora esta apagada o sin rollo, el trabajo NO se
// pierde: reintenta con espera creciente (3s -> 30s) durante ~13 minutos, y si
// aun asi no salio, queda guardado en "fallidas" para reintentarlo con un boton.
import { createConnection } from 'node:net';
import { enviarUsb } from './usb.js';
import type { ImpresoraCfg } from './tipos.js';

/** Etiqueta legible del destino (para logs): USB "POS-80" o 192.168.1.200:9100. */
export function dirImpresora(imp: ImpresoraCfg): string {
  return imp.tipo === 'usb' ? `USB "${imp.host}"` : `${imp.host}:${imp.puerto}`;
}

function normalizar(s: string): string {
  return s.normalize('NFD').replace(/[̀-ͯ]/g, '').trim().toLowerCase();
}

export function enviarTcp(host: string, puerto: number, datos: Buffer, timeoutMs = 5000): Promise<void> {
  return new Promise((resolve, reject) => {
    const sock = createConnection({ host, port: puerto });
    let listo = false;
    const fin = (err?: Error) => {
      if (listo) return;
      listo = true;
      sock.destroy();
      if (err) reject(err);
      else resolve();
    };
    sock.setTimeout(timeoutMs, () => fin(new Error('la impresora no respondio a tiempo')));
    sock.on('error', (e) => fin(e));
    sock.on('connect', () => {
      sock.end(datos, () => fin());
    });
  });
}

interface Trabajo {
  impresora: ImpresoraCfg;
  datos: Buffer;
  etiqueta: string;
  intentos: number;
  /** No reintentar antes de este momento (espera creciente). */
  proximoEn: number;
  /** Se llama UNA vez cuando el papel salio de verdad (exito real, no encolado). */
  alExito?: () => void;
}

export class ColaImpresion {
  private pendientes: Trabajo[] = [];
  private fallidas: Trabajo[] = [];
  private timer: NodeJS.Timeout;
  private ocupado = false;

  constructor(
    private impresoras: ImpresoraCfg[],
    private log: (m: string) => void,
    /** ~13 minutos de paciencia: 40 intentos con espera de hasta 30s. */
    private maxIntentos = 40,
  ) {
    this.timer = setInterval(() => void this.procesar(), 3000);
  }

  /** Encola una comanda para el puesto dado. Devuelve false si no hay impresora para ese puesto. */
  encolar(puesto: string, datos: Buffer, etiqueta: string, alExito?: () => void): boolean {
    const imp = this.impresoras.find((i) => normalizar(i.puesto) === normalizar(puesto));
    if (!imp) {
      this.log(`Sin impresora configurada para el puesto "${puesto}" (${etiqueta})`);
      return false;
    }
    this.pendientes.push({ impresora: imp, datos, etiqueta, intentos: 0, proximoEn: 0, alExito });
    void this.procesar();
    return true;
  }

  /** Cuantos papeles esperan impresora y cuantos se dieron por vencidos. */
  estado(): { pendientes: number; fallidas: number } {
    return { pendientes: this.pendientes.length, fallidas: this.fallidas.length };
  }

  /**
   * Aplica el mapeo nuevo puesto->impresora SIN reiniciar el programa (antes,
   * guardar la configuracion no surtia efecto hasta cerrar y abrir el exe).
   * Los trabajos ya encolados conservan su impresora original.
   */
  actualizarImpresoras(lista: ImpresoraCfg[]): void {
    this.impresoras = lista;
  }

  /** Vuelve a poner en cola todo lo que se dio por vencido (boton del asistente). */
  reintentarFallidas(): number {
    const n = this.fallidas.length;
    for (const t of this.fallidas) {
      t.intentos = 0;
      t.proximoEn = 0;
      this.pendientes.push(t);
    }
    this.fallidas = [];
    if (n) void this.procesar();
    return n;
  }

  private async procesar(): Promise<void> {
    if (this.ocupado) return;
    this.ocupado = true;
    try {
      const ahora = Date.now();
      const listos = this.pendientes.filter((t) => t.proximoEn <= ahora);
      this.pendientes = this.pendientes.filter((t) => t.proximoEn > ahora);
      for (const t of listos) {
        try {
          if (t.impresora.tipo === 'usb') await enviarUsb(t.impresora.host, t.datos);
          else await enviarTcp(t.impresora.host, t.impresora.puerto, t.datos);
          this.log(`Impreso ${t.etiqueta} (${dirImpresora(t.impresora)})`);
          try {
            t.alExito?.();
          } catch {
            /* el aviso no tumba la cola */
          }
          t.alExito = undefined;
        } catch (e) {
          t.intentos += 1;
          if (t.intentos >= this.maxIntentos) {
            this.fallidas.push(t);
            if (this.fallidas.length > 50) this.fallidas.shift();
            this.log(`SE VENCIO ${t.etiqueta} en ${t.impresora.puesto}: ${(e as Error).message}. Queda en "fallidas" para reintentar.`);
          } else {
            // Espera creciente: 3s, 6s, 12s, 24s y de ahi 30s fijo.
            t.proximoEn = Date.now() + Math.min(30_000, 3_000 * 2 ** Math.min(t.intentos, 4));
            if (t.intentos === 1 || t.intentos % 10 === 0) {
              this.log(`La impresora de ${t.impresora.puesto} no responde (${t.etiqueta}); sigo intentando...`);
            }
            this.pendientes.push(t);
          }
        }
      }
    } finally {
      this.ocupado = false;
    }
  }

  detener(): void {
    clearInterval(this.timer);
  }
}
