// El corazon del cerebrito (etapa 1: puente de impresion). Escucha el canal
// en vivo del restaurante y, cuando un pedido queda confirmado, parte la
// comanda por puesto y la manda a la impresora de cada cocina. Un barrido de
// respaldo cada 20s atrapa lo que el canal se haya perdido (reinicios, wifi
// caido, laptop dormida) y un ledger local evita repetir papel.
import { armarComandas, armarTicketCopiaMesa, armarTicketDespacho, consolidarPedidosMesa } from './comanda.js';
import { comandaABytes, comandaATexto } from './escpos.js';
import { ColaImpresion } from './impresoras.js';
import { Ledger } from './ledger.js';
import { conectarNube } from './nube.js';
import type { PrefsNube } from './nubeConfig.js';
import type { Config, Puesto } from './tipos.js';

const ESTADOS_IMPRIMIBLES = ['confirmado', 'preparando'];

export interface OpcionesMotor {
  log?: (m: string) => void;
  /** Espia para pruebas y previos: recibe cada comanda ya armada, en texto plano. */
  alImprimir?: (puesto: string, etiqueta: string, texto: string) => void;
  /** Cola compartida con el salon local (el dueño de su ciclo de vida es quien la creo). */
  colaExterna?: ColaImpresion;
  /** La config del negocio en la nube (impresoras/despacho) cambio: aplicarla aqui. */
  alConfigNube?: (prefs: PrefsNube) => void;
}

export async function iniciarCerebrito(cfg: Config, op: OpcionesMotor = {}) {
  const log = op.log ?? ((m: string) => console.log(`[cerebrito] ${m}`));
  const nube = await conectarNube(cfg, log);
  let puestos: Puesto[] = await nube.puestos();
  if (!puestos.some((p) => p.activo)) {
    log('OJO: el restaurante no tiene puestos configurados; no hay nada que imprimir');
  }
  const cola = op.colaExterna ?? new ColaImpresion(cfg.impresoras, log);
  const colaPropia = !op.colaExterna;
  const ledger = new Ledger(cfg.carpetaEstado ?? './estado');
  let detenido = false;
  let canalVivo = false;
  // Memoria corta para "reimprimir la ultima comanda" (papel atascado, sin rollo).
  const ultimas: { codigo: string; partes: { puesto: string; etiqueta: string; bytes: Buffer }[] }[] = [];

  const tiposDespacho = cfg.despacho?.tipos ?? ['delivery', 'pickup'];

  // Normaliza igual que la cola (sin tildes, minusculas) para casar puestos.
  const normPuesto = (s: string) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').trim().toLowerCase();
  // El despacho y su copia van a la impresora de su puesto; si ese puesto no
  // tiene impresora (config a medias, o un solo equipo para todo el negocio),
  // caen a la PRIMERA impresora configurada: el papel NUNCA se pierde en silencio.
  const puestoConImpresora = (pref: string | undefined): string => {
    if (pref && cfg.impresoras.some((i) => normPuesto(i.puesto) === normPuesto(pref))) return pref;
    return cfg.impresoras[0]?.puesto ?? pref ?? 'Despacho';
  };

  // ===== UNA MESA = UNA ORDEN: lote por cuenta con ventana de 5 segundos =====
  // Los 7 celulares del QR (o los envios del mesero) confirman casi juntos; se
  // espera un momentico, se juntan TODOS los confirmados de esa cuenta y sale
  // UNA sola comanda por puesto con los renglones sumados. Nada de 7 papeles.
  const lotesMesa = new Map<string, NodeJS.Timeout>();
  const secuenciaMesa = new Map<string, number>();

  function encolarLoteMesa(cuentaId: string): void {
    if (lotesMesa.has(cuentaId)) return;
    lotesMesa.set(
      cuentaId,
      setTimeout(() => {
        lotesMesa.delete(cuentaId);
        void imprimirLoteMesa(cuentaId);
      }, 5_000),
    );
  }

  async function imprimirLoteMesa(cuentaId: string): Promise<void> {
    if (detenido) return;
    const todos = (await nube.pedidosDeCuenta(cuentaId)).filter((p) => !ledger.tiene(p.id));
    if (!todos.length) return;
    for (const p of todos) ledger.marcar(p.id);
    const seq = (secuenciaMesa.get(cuentaId) ?? 0) + 1;
    secuenciaMesa.set(cuentaId, seq);
    const lote = consolidarPedidosMesa(todos, seq);
    const comandas = armarComandas(lote, puestos, cfg.sinPuesto ?? 'defecto');
    let yaAvisado = false;
    const alSalirElPapel = () => {
      if (yaAvisado || detenido) return;
      yaAvisado = true;
      for (const p of todos) void nube.marcarPreparando(p.id);
    };
    const partes: { puesto: string; etiqueta: string; bytes: Buffer }[] = [];
    for (const c of comandas) {
      const etiqueta = `#${lote.codigo} -> ${c.puesto.nombre}`;
      const bytes = comandaABytes(c.lineas);
      partes.push({ puesto: c.puesto.nombre, etiqueta, bytes });
      cola.encolar(c.puesto.nombre, bytes, etiqueta, alSalirElPapel);
      op.alImprimir?.(c.puesto.nombre, etiqueta, comandaATexto(c.lineas));
    }

    // La COPIA-NEGOCIO de la mesa NO sale aqui: sale cuando el cliente CANCELA
    // (la mesera cobra y libera), con el cobro real. Ver imprimirCopiaMesa.

    if (partes.length) {
      ultimas.push({ codigo: lote.codigo, partes });
      if (ultimas.length > 10) ultimas.shift();
    }
    log(`Mesa ${lote.mesa}: ${todos.length} pedido(s) en UNA orden #${lote.codigo} (${comandas.length} comanda(s))`);
  }

  // COPIA-NEGOCIO de la mesa: sale UNA vez, cuando la cuenta queda CERRADA
  // (el cliente cancelo y la mesera dio "cobrar y liberar"), con el cobro REAL
  // (metodo, referencia, propina). Requiere despacho.tipos con 'mesa'.
  async function imprimirCopiaMesa(cuentaId: string, origen: string): Promise<void> {
    if (detenido || !cfg.despacho || !tiposDespacho.includes('mesa')) return;
    const clave = `copia:${cuentaId}`;
    if (ledger.tiene(clave)) return;
    const datos = await nube.cuentaCobrada(cuentaId);
    if (!datos || !datos.pedidos.length) return;
    if (ledger.tiene(clave)) return; // el vivo y el barrido pudieron cruzarse
    ledger.marcar(clave);

    const lote = consolidarPedidosMesa(datos.pedidos, 1);
    const total = datos.pagos.reduce((s, p) => s + (p.monto_usd ?? 0), 0) || datos.pedidos.reduce((s, p) => s + (p.total_usd ?? 0), 0);
    const propina = datos.pagos.reduce((s, p) => s + (p.propina_usd ?? 0), 0);
    const tasa = datos.pedidos.map((p) => p.tasa_bs).find((t) => t && t > 0) ?? null;
    const lineas = armarTicketCopiaMesa({
      mesa: String(lote.mesa ?? '?'),
      mesero: lote.mesero_nombre,
      items: (lote.pedido_items ?? []).map((it) => ({ nombre: it.nombre, cantidad: it.cantidad, nota: it.nota })),
      total_usd: total,
      propina_usd: propina,
      tasa_bs: tasa,
      pagos: datos.pagos,
    });
    const destino = puestoConImpresora(cfg.despacho.puesto);
    const etiqueta = `mesa ${lote.mesa ?? '?'} -> copia negocio`;
    const bytes = comandaABytes(lineas);
    ultimas.push({ codigo: `M${lote.mesa ?? '?'}`, partes: [{ puesto: destino, etiqueta, bytes }] });
    if (ultimas.length > 10) ultimas.shift();
    cola.encolar(destino, bytes, etiqueta);
    op.alImprimir?.(destino, etiqueta, comandaATexto(lineas));
    log(`Mesa ${lote.mesa ?? '?'} cobrada: copia del negocio a imprimir (${origen})`);
  }

  async function procesar(id: string, origen: string): Promise<void> {
    if (ledger.tiene(id)) return;
    const p = await nube.pedido(id);
    if (!p) return;
    // VENTA DE CAJA: nace ENTREGADA (cobrada en el momento); igual la cocina
    // recibe su comanda (membrete CAJA), pero sin despacho ni tocar estados.
    const ventaCaja = p.origen === 'caja' && p.estado === 'entregado';
    if (!ESTADOS_IMPRIMIBLES.includes(p.estado) && !ventaCaja) return;
    if (ledger.tiene(id)) return; // el vivo y el barrido pudieron cruzarse

    // Pedidos de MESA con cuenta: van al lote (una comanda por mesa), no sueltos.
    if (p.tipo_entrega === 'mesa' && p.cuenta_id && p.estado === 'confirmado') {
      encolarLoteMesa(p.cuenta_id);
      return;
    }
    ledger.marcar(id);

    const partes: { puesto: string; etiqueta: string; bytes: Buffer }[] = [];

    // MODELO DE COCINA DE PAPEL: cuando la PRIMERA comanda de este pedido sale
    // impresa de verdad, el pedido pasa solo a "preparando" (el cocinero ya lo
    // tiene en la mano y no toca pantallas). "Listo" lo marca quien despacha.
    let yaAvisado = false;
    const alSalirElPapel =
      p.estado === 'confirmado'
        ? () => {
            if (yaAvisado || detenido) return;
            yaAvisado = true;
            void nube.marcarPreparando(p.id);
          }
        : undefined;

    // Comandas de cocina: cada puesto recibe SOLO lo suyo (sin datos del cliente).
    const comandas = armarComandas(p, puestos, cfg.sinPuesto ?? 'defecto');
    for (const c of comandas) {
      const etiqueta = `#${p.codigo} -> ${c.puesto.nombre}`;
      const bytes = comandaABytes(c.lineas);
      partes.push({ puesto: c.puesto.nombre, etiqueta, bytes });
      cola.encolar(c.puesto.nombre, bytes, etiqueta, alSalirElPapel);
      op.alImprimir?.(c.puesto.nombre, etiqueta, comandaATexto(c.lineas));
    }

    // Delivery/pickup: DOS papeles con los datos del cliente, direccion y cobro:
    //   1) el que va CON el pedido (para el cliente / repartidor)
    //   2) la COPIA que se queda en el negocio (respaldo)
    // Los dos salen por la impresora de despacho (o la primera si esa no existe).
    // Venta de CAJA: SOLO la copia del negocio (el cliente esta parado ahi;
    // el cobro sale como "Pagado", ya la caja cobro y verifico).
    let despachos = 0;
    if (cfg.despacho && (ventaCaja || tiposDespacho.includes(p.tipo_entrega))) {
      const destino = puestoConImpresora(cfg.despacho.puesto);
      const copias: [string, string][] = ventaCaja
        ? [['COPIA-NEGOCIO', 'negocio']]
        : [
            ['DESPACHO', 'cliente'],
            ['COPIA-NEGOCIO', 'negocio'],
          ];
      for (const [titulo, sufijo] of copias) {
        const lineas = armarTicketDespacho(p, titulo, ventaCaja);
        const etiqueta = `#${p.codigo} -> ${sufijo}`;
        const bytes = comandaABytes(lineas);
        partes.push({ puesto: destino, etiqueta, bytes });
        // El despacho tambien cuenta como "papel afuera" (pedidos de solo bebidas).
        cola.encolar(destino, bytes, etiqueta, alSalirElPapel);
        op.alImprimir?.(destino, etiqueta, comandaATexto(lineas));
        despachos += 1;
      }
    }

    if (partes.length) {
      ultimas.push({ codigo: p.codigo, partes });
      if (ultimas.length > 10) ultimas.shift();
    }

    if (!comandas.length && !despachos) {
      log(`Pedido #${p.codigo} sin renglones que imprimir (${origen})`);
      return;
    }
    log(`Pedido #${p.codigo}: ${comandas.length} comanda(s) + ${despachos} despacho (${origen})`);
  }

  // Canal en vivo con reconexion propia (backoff con tope de 30s).
  let canalActivo: { quitar: () => void } | null = null;
  let intentos = 0;
  function conectarCanal(): void {
    if (detenido) return;
    canalActivo?.quitar();
    canalActivo = nube.escuchar(
      (c) => {
        if (c.tabla === 'pedidos' && c.nuevo?.estado === 'confirmado' && c.viejo?.estado !== 'confirmado') {
          void procesar(c.id, 'en vivo');
        }
        // Venta de caja: nace ENTREGADA directo (INSERT); la comanda sale igual.
        if (c.tabla === 'pedidos' && c.accion === 'INSERT' && c.nuevo?.estado === 'entregado') {
          void procesar(c.id, 'venta de caja');
        }
        // Mesa cobrada y liberada: sale la COPIA-NEGOCIO con el cobro real.
        if (c.tabla === 'cuentas_mesa' && c.nuevo?.estado === 'cerrada' && c.viejo?.estado !== 'cerrada') {
          void imprimirCopiaMesa(c.id, 'en vivo');
        }
      },
      (status) => {
        if (detenido) return;
        if (status === 'SUBSCRIBED') {
          intentos = 0;
          canalVivo = true;
          log('Canal en vivo conectado');
        } else if (['CHANNEL_ERROR', 'TIMED_OUT', 'CLOSED'].includes(status)) {
          canalVivo = false;
          const espera = Math.min(30_000, 1_000 * 2 ** Math.min(intentos, 5));
          intentos += 1;
          setTimeout(conectarCanal, espera);
        }
      },
    );
  }
  conectarCanal();

  // ESTRENO de la copia del negocio: la primera vez que esta funcion se activa,
  // las mesas YA cerradas antes de arrancar se marcan como listas (no imprimir
  // una pila de copias viejas). Despues, el barrido SI rescata cierres que
  // pasaron con el cerebrito caido.
  if (cfg.despacho && tiposDespacho.includes('mesa') && !ledger.algunoConPrefijo('copia:')) {
    try {
      for (const { id } of await nube.cuentasCerradas(6)) ledger.marcar(`copia:${id}`);
    } catch {
      /* sin internet ahora: peor caso, el barrido imprime alguna copia vieja */
    }
  }

  // Barrido de respaldo: atrapa lo confirmado que el canal no entrego, y las
  // mesas cobradas cuya copia del negocio no salio (caida justo al cobrar).
  const barrido = setInterval(() => {
    void (async () => {
      try {
        for (const { id } of await nube.pendientes(6)) {
          if (!ledger.tiene(id)) await procesar(id, 'barrido');
        }
        for (const { id } of await nube.ventasCaja(6)) {
          if (!ledger.tiene(id)) await procesar(id, 'barrido caja');
        }
        if (cfg.despacho && tiposDespacho.includes('mesa')) {
          for (const { id } of await nube.cuentasCerradas(6)) {
            if (!ledger.tiene(`copia:${id}`)) await imprimirCopiaMesa(id, 'barrido');
          }
        }
      } catch (e) {
        log('Barrido fallo: ' + (e as Error).message);
      }
    })();
  }, 20_000);

  // LA NUBE MANDA: baja la config del negocio (impresoras/despacho/pin) al
  // arrancar y en cada refresco. Lo que se configura en UNA laptop aparece en
  // las demas sin tocar nada (y un arreglo remoto cura el local al reiniciar).
  async function revisarConfigNube(): Promise<void> {
    if (!op.alConfigNube) return;
    try {
      const { data } = await nube.supabase
        .from('cerebrito_config_nube')
        .select('config')
        .eq('restaurante_id', nube.restauranteId)
        .maybeSingle();
      const prefs = (data as { config?: PrefsNube } | null)?.config;
      if (prefs && Array.isArray(prefs.impresoras) && prefs.impresoras.length) op.alConfigNube(prefs);
    } catch {
      /* al proximo refresco */
    }
  }
  void revisarConfigNube();

  // Refresco de puestos (por si el dueno reorganiza sus cocinas) + config nube.
  const refresco = setInterval(() => {
    void nube
      .puestos()
      .then((p) => {
        puestos = p;
      })
      .catch(() => {
        /* al proximo refresco */
      });
    void revisarConfigNube();
  }, 300_000);

  log(
    `Listo: puente de impresion de "${nube.nombre}" ` +
      `(${cfg.impresoras.length} impresora(s), ${puestos.filter((p) => p.activo).length} puesto(s))`,
  );

  return {
    procesar, // expuesto para pruebas
    cola,
    nombreRestaurante: nube.nombre,
    /** Puestos activos cargados (para que el asistente pinte las casillas en estado). */
    puestos: () => puestos.filter((p) => p.activo).map((p) => ({ id: p.id, nombre: p.nombre })),
    estado() {
      return { canalVivo, ultimas: ultimas.map((u) => u.codigo), cola: cola.estado() };
    },
    /** Reencola los papeles que se dieron por vencidos (impresora apagada mucho rato). */
    reintentarFallidas(): number {
      return cola.reintentarFallidas();
    },
    /** Vuelve a mandar TODAS las partes de la ultima comanda impresa. */
    reimprimirUltima(): string | null {
      const u = ultimas[ultimas.length - 1];
      if (!u) return null;
      for (const p of u.partes) cola.encolar(p.puesto, p.bytes, p.etiqueta + ' (reimpresa)');
      log(`Reimpresa #${u.codigo} (${u.partes.length} papel(es))`);
      return u.codigo;
    },
    detener() {
      detenido = true;
      canalActivo?.quitar();
      clearInterval(barrido);
      clearInterval(refresco);
      for (const t of lotesMesa.values()) clearTimeout(t);
      lotesMesa.clear();
      if (colaPropia) cola.detener();
      nube.supabase.realtime.disconnect();
    },
  };
}
