// COLA DE TRANSMISION.
//
// La Providencia 000121 (art. 3, numeral 2) exige remision "continua, segura,
// correcta, integra, automatica, consecutiva, inmediata y fehaciente". O sea:
// cada documento sale apenas se sella, no en lotes al final del dia.
//
// Pero esto es Venezuela y el internet se cae. Entonces:
//   1. EL COBRO NUNCA ESPERA. Se sella, se guarda en el libro, el cliente se va
//      con su comida, y la transmision ocurre por detras.
//   2. Lo que no salio queda pendiente y se reintenta EN ORDEN, sin repetir y
//      sin saltarse ninguno, hasta que salga.
//   3. Si la imprenta RECHAZA el documento (datos malos), no se reintenta al
//      infinito: se marca y se avisa, porque reintentar no lo va a arreglar.
//
// El estado de transmision vive APARTE del libro, en su propio archivo que solo
// agrega: el libro no se puede tocar (romperia el hash), pero el estado de
// envio si cambia. Cada cambio es una linea nueva; la ultima manda.
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, writeSync } from 'node:fs';
import { dirname } from 'node:path';
import type { Asiento } from './tipos.js';
import type { Imprenta } from './imprenta.js';

export type EstadoEnvio = 'pendiente' | 'enviado' | 'rechazado';

export interface RegistroEnvio {
  seq: number;
  estado: EstadoEnvio;
  intentos: number;
  en: string;
  numeroControl?: string;
  fechaAsignacion?: string;
  /** Direccion del sello (el QR de la factura), si la imprenta la devolvio. */
  sello?: string;
  /**
   * QUE IMPRENTA le puso el numero a ESTE documento.
   *
   * Sin esto, un negocio con proveedor de respaldo configurado no puede
   * responder "de donde salio este numero de control", y eso es justo lo que
   * pide el articulo 3 de la 000121 (trazabilidad). Se guarda el nombre tal
   * como lo reporta el adaptador que emitio.
   */
  imprenta?: string;
  /** Rango del que salio el numero de control (art. 7 num. 5 de la 000102). */
  rango?: { desde: string; hasta: string } | null;
  /** Razon social, RIF y providencia de la imprenta (art. 7 num. 14). */
  imprentaFiscal?: {
    rif: string;
    razon_social: string;
    providencia?: string | null;
    providencia_fecha?: string | null;
  } | null;
  error?: string;
}

/** Errores que NO se arreglan reintentando: el documento esta malo. */
function esRechazo(e: unknown): boolean {
  const m = e instanceof Error ? e.message : String(e);
  return /rechaz|invalid|no acepto|falta la tasa|no devolvio numero/i.test(m);
}

export class ColaTransmision {
  private readonly ruta: string;
  private readonly imprenta: Imprenta;
  private readonly log: (m: string) => void;
  /** Ultimo estado conocido de cada asiento. */
  private estado = new Map<number, RegistroEnvio>();
  /** Los asientos que faltan por salir, en el orden en que se sellaron. */
  private pendientes: Asiento[] = [];

  constructor(op: { ruta: string; imprenta: Imprenta; log?: (m: string) => void }) {
    this.ruta = op.ruta;
    this.imprenta = op.imprenta;
    this.log = op.log ?? (() => {});
    mkdirSync(dirname(this.ruta), { recursive: true });
    if (existsSync(this.ruta)) this.cargar();
  }

  private cargar(): void {
    const lineas = readFileSync(this.ruta, 'utf8').split('\n').filter((l) => l.trim() !== '');
    for (const l of lineas) {
      const r = JSON.parse(l) as RegistroEnvio;
      this.estado.set(r.seq, r); // la ultima linea de cada seq manda
    }
  }

  private anotar(r: RegistroEnvio): void {
    const fd = openSync(this.ruta, 'a');
    try {
      writeSync(fd, JSON.stringify(r) + '\n');
      fsyncSync(fd);
    } finally {
      closeSync(fd);
    }
    this.estado.set(r.seq, r);
  }

  /** Estado de un asiento. Si nunca se intento, no hay registro. */
  ver(seq: number): RegistroEnvio | null {
    return this.estado.get(seq) ?? null;
  }

  /** Lo que falta por salir. */
  cuantosPendientes(): number {
    return this.pendientes.length;
  }

  /** Documentos que la imprenta rechazo: alguien tiene que mirarlos. */
  rechazados(): RegistroEnvio[] {
    return [...this.estado.values()].filter((r) => r.estado === 'rechazado');
  }

  /**
   * Mete un asiento a la cola y trata de mandarlo YA. Nunca lanza: si falla,
   * queda pendiente y la venta sigue su curso.
   */
  async encolar(asiento: Asiento): Promise<RegistroEnvio> {
    const previo = this.estado.get(asiento.seq);
    if (previo?.estado === 'enviado') return previo; // ya salio, no se manda dos veces
    if (!this.pendientes.some((a) => a.seq === asiento.seq)) this.pendientes.push(asiento);
    await this.procesar();
    return this.estado.get(asiento.seq) ?? { seq: asiento.seq, estado: 'pendiente', intentos: 0, en: new Date().toISOString() };
  }

  /**
   * Recorre los pendientes EN ORDEN. Si uno falla por red, se detiene ahi: no
   * se salta al siguiente, porque la norma exige remision consecutiva.
   */
  async procesar(): Promise<{ enviados: number; pendientes: number }> {
    let enviados = 0;
    while (this.pendientes.length > 0) {
      const asiento = this.pendientes[0];
      const previo = this.estado.get(asiento.seq);
      const intentos = (previo?.intentos ?? 0) + 1;
      try {
        const acuse = await this.imprenta.emitir(asiento);
        this.anotar({
          seq: asiento.seq,
          estado: 'enviado',
          intentos,
          en: new Date().toISOString(),
          numeroControl: acuse.numeroControl,
          fechaAsignacion: acuse.fechaAsignacion,
          sello: acuse.sello,
          // Queda escrito CUAL imprenta numero este documento: con proveedor
          // de respaldo configurado, esa pregunta la hace un fiscal.
          imprenta: this.imprenta.nombre,
          // Lo que la factura debe imprimir y solo la imprenta sabe: el rango
          // de la numeracion y su identidad fiscal (art. 7 nums. 5 y 14).
          rango: acuse.rango ?? null,
          imprentaFiscal: acuse.imprentaFiscal ?? null,
        });
        this.pendientes.shift();
        enviados += 1;
        this.log(`Documento ${asiento.tipo} ${asiento.numero}: numero de control ${acuse.numeroControl} (${this.imprenta.nombre})`);
      } catch (e) {
        const msg = e instanceof Error ? e.message : String(e);
        if (esRechazo(e)) {
          // No se arregla reintentando: se saca de la fila y se avisa, pero el
          // documento YA esta en el libro (no se borra nada jamas).
          this.anotar({ seq: asiento.seq, estado: 'rechazado', intentos, en: new Date().toISOString(), error: msg });
          this.pendientes.shift();
          this.log(`Atencion: la imprenta rechazo el documento ${asiento.numero}: ${msg}`);
          continue;
        }
        // Problema de red o de la imprenta: se queda de primero en la fila.
        this.anotar({ seq: asiento.seq, estado: 'pendiente', intentos, en: new Date().toISOString(), error: msg });
        this.log(`Sin salida hacia la imprenta (${msg}). Quedan ${this.pendientes.length} por transmitir.`);
        break;
      }
    }
    return { enviados, pendientes: this.pendientes.length };
  }

  /**
   * Al arrancar el programa: vuelve a poner en la fila lo que quedo sin salir
   * en la corrida anterior, en el orden del libro.
   */
  recuperar(asientos: Asiento[]): number {
    for (const a of asientos) {
      const r = this.estado.get(a.seq);
      if (r?.estado === 'enviado' || r?.estado === 'rechazado') continue;
      if (!this.pendientes.some((p) => p.seq === a.seq)) this.pendientes.push(a);
    }
    this.pendientes.sort((x, y) => x.seq - y.seq);
    return this.pendientes.length;
  }
}
