// EL SALON LOCAL: mesas, pedidos y cobros que viven en la LAPTOP, sin depender
// del internet. Cada pedido imprime su comanda de una (cola local) y cada
// venta cerrada queda en el BUZON DE SALIDA (outbox) hasta que el
// sincronizador la suba a la nube. Todo persiste en disco con escritura
// atomica: un apagon no pierde ni mesas abiertas ni ventas por subir.
import { mkdirSync, readFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
import { join } from 'node:path';
import { armarComandas, armarTicketCopiaMesa, armarTicketCuenta } from './comanda.js';
import { comandaABytes } from './escpos.js';
import type { ColaImpresion } from './impresoras.js';
import { escribirAtomico, type Espejo } from './espejo.js';
import type { PedidoNube } from './tipos.js';

export interface ItemLocal {
  id: string;
  producto_id: string | null;
  nombre: string;
  precio_usd: number;
  cantidad: number;
  nota: string | null;
}

export interface PedidoLocal {
  id: string;
  codigo: string;
  tipo: 'mesa' | 'directo';
  mesa: string | null;
  para_llevar: boolean;
  nota: string | null;
  mesero_nombre: string | null;
  recibido_en: string;
  items: ItemLocal[];
  subtotal_usd: number;
}

export interface CuentaLocal {
  id: string;
  mesa: string;
  abierta_en: string;
  mesero_nombre: string | null;
  pedidos: PedidoLocal[];
}

export interface PagoLocal {
  id: string;
  pedido_id?: string;
  monto_usd: number;
  metodo: string;
  referencia: string | null;
  propina_usd: number | null;
  mesero_nombre: string | null;
}

export interface VentaLocal {
  id: string;
  tipo: 'mesa' | 'directo';
  cuenta?: { id: string; mesa: string; abierta_en: string; cerrada_en: string; mesero_nombre: string | null };
  pedidos: (Omit<PedidoLocal, 'tipo'> & { tipo_entrega: string; tasa_bs: number | null; metodo_pago: string | null; entregado_en: string; total_usd: number })[];
  pagos: PagoLocal[];
  creado_en: string;
  /**
   * El sello de la Estacion Fiscal, si esta venta ya se le entrego directo.
   * Vacio = pendiente (el puente reintenta; si la venta se va a la nube antes,
   * la cola de alla se encarga y el libro deduplica por el uuid).
   */
  fiscal?: { serie: string; numero: number; control?: string | null; sello?: string | null; sellada_en: string };
}

interface EstadoSalon {
  contador: number;
  mesas: Record<string, CuentaLocal>;
  outbox: VentaLocal[];
}

export interface ItemEntrante {
  producto_id: string;
  cantidad: number;
  nota?: string;
  /** Extra por opciones elegidas (lo calcula el POS con el espejo a la vista). */
  extra_usd?: number;
}

export function crearSalon(deps: {
  base: string;
  espejo: () => Espejo | null;
  cola: ColaImpresion;
  log: (m: string) => void;
  hayDespacho: boolean;
  /** Impresora donde sale la PRE-CUENTA (la de despacho si hay; si no, la primera). */
  puestoCuenta?: string | null;
  /** Se llama apenas una venta queda cerrada y en el buzon (el puente fiscal la entrega de una). */
  alVentaCerrada?: (v: VentaLocal) => void;
}) {
  const ruta = join(deps.base, 'local', 'salon.json');
  mkdirSync(join(deps.base, 'local'), { recursive: true });

  let estado: EstadoSalon = { contador: 0, mesas: {}, outbox: [] };
  try {
    estado = JSON.parse(readFileSync(ruta, 'utf8')) as EstadoSalon;
  } catch {
    /* primer arranque */
  }

  function persistir(): void {
    escribirAtomico(ruta, JSON.stringify(estado));
  }

  function armarItems(entrantes: ItemEntrante[]): ItemLocal[] {
    const e = deps.espejo();
    if (!e) throw new Error('El menu local aun no se ha descargado (conecta el internet una primera vez).');
    return entrantes.map((it) => {
      const prod = e.productos.find((p) => p.id === it.producto_id);
      if (!prod) throw new Error('Producto desconocido en el menu local.');
      return {
        id: randomUUID(),
        producto_id: prod.id,
        nombre: prod.nombre,
        precio_usd: Number(prod.precio_usd) + (Number(it.extra_usd) || 0),
        cantidad: Math.max(1, Math.floor(Number(it.cantidad) || 1)),
        nota: it.nota?.trim() ? it.nota.trim() : null,
      };
    });
  }

  /** Convierte un pedido local a la forma que entienden las comandas e imprime. */
  function imprimir(p: PedidoLocal): void {
    const e = deps.espejo();
    if (!e) return;
    const porProducto = new Map(e.productos.map((x) => [x.id, x]));
    const porCategoria = new Map(e.categorias.map((x) => [x.id, x]));
    const comoNube: PedidoNube = {
      id: p.id,
      codigo: p.codigo,
      estado: 'confirmado',
      tipo_entrega: p.tipo === 'mesa' ? 'mesa' : 'pickup',
      mesa: p.mesa,
      para_llevar: p.para_llevar,
      mesero_nombre: p.mesero_nombre,
      nota: p.nota,
      recibido_en: p.recibido_en,
      cliente_nombre: null,
      cliente_telefono: null,
      direccion: null,
      metodo_pago: null,
      total_usd: p.subtotal_usd,
      costo_delivery_usd: 0,
      tasa_bs: e.restaurante.tasa_bs,
      pedido_items: p.items.map((it) => {
        const prod = it.producto_id ? porProducto.get(it.producto_id) : undefined;
        const cat = prod?.categoria_id ? porCategoria.get(prod.categoria_id) : undefined;
        return {
          id: it.id,
          nombre: it.nombre,
          cantidad: it.cantidad,
          nota: it.nota,
          a_cocina: cat?.a_cocina ?? true,
          producto: { puesto_id: prod?.puesto_id ?? null, categoria: { puesto_id: cat?.puesto_id ?? null } },
        };
      }),
    };
    const comandas = armarComandas(comoNube, e.puestos, 'defecto');
    for (const c of comandas) {
      deps.cola.encolar(c.puesto.nombre, comandaABytes(c.lineas), `#${p.codigo} -> ${c.puesto.nombre}`);
    }
    deps.log(`LOCAL #${p.codigo}: ${comandas.length} comanda(s) a imprimir`);
  }

  function nuevoCodigo(): string {
    estado.contador += 1;
    const prefijo = deps.espejo()?.restaurante.prefijo ?? 'GX';
    return `${prefijo}-L${String(estado.contador).padStart(4, '0')}`;
  }

  return {
    /** Resumen de mesas para el POS (numero, renglones y total de cada cuenta abierta). */
    mesas(): { mesa: string; abierta: boolean; total_usd: number; renglones: number }[] {
      const n = deps.espejo()?.restaurante.num_mesas ?? 0;
      const lista: { mesa: string; abierta: boolean; total_usd: number; renglones: number }[] = [];
      for (let i = 1; i <= n; i++) {
        const c = estado.mesas[String(i)];
        const total = c ? c.pedidos.reduce((s, p) => s + p.subtotal_usd, 0) : 0;
        const renglones = c ? c.pedidos.reduce((s, p) => s + p.items.length, 0) : 0;
        lista.push({ mesa: String(i), abierta: !!c, total_usd: total, renglones });
      }
      return lista;
    },

    cuenta(mesa: string): CuentaLocal | null {
      return estado.mesas[mesa] ?? null;
    },

    /** Toma un pedido de MESA: abre la cuenta si hace falta, imprime y persiste. */
    tomarPedidoMesa(datos: { mesa: string; items: ItemEntrante[]; paraLlevar?: boolean; nota?: string; mesero?: string }): PedidoLocal {
      const items = armarItems(datos.items);
      if (!items.length) throw new Error('El pedido esta vacio.');
      const mesa = String(datos.mesa).trim();
      if (!mesa) throw new Error('Falta la mesa.');
      let c = estado.mesas[mesa];
      if (!c) {
        c = { id: randomUUID(), mesa, abierta_en: new Date().toISOString(), mesero_nombre: datos.mesero?.trim() || null, pedidos: [] };
        estado.mesas[mesa] = c;
      }
      const p: PedidoLocal = {
        id: randomUUID(),
        codigo: nuevoCodigo(),
        tipo: 'mesa',
        mesa,
        para_llevar: !!datos.paraLlevar,
        nota: datos.nota?.trim() || null,
        mesero_nombre: datos.mesero?.trim() || c.mesero_nombre,
        recibido_en: new Date().toISOString(),
        items,
        subtotal_usd: items.reduce((s, it) => s + it.precio_usd * it.cantidad, 0),
      };
      c.pedidos.push(p);
      persistir();
      imprimir(p);
      return p;
    },

    /** Cobra y cierra la cuenta de una mesa: la venta completa pasa al buzon de salida. */
    cobrarMesa(datos: { mesa: string; metodo: string; referencia?: string; propina_usd?: number; mesero?: string }): VentaLocal {
      const c = estado.mesas[String(datos.mesa)];
      if (!c || !c.pedidos.length) throw new Error('Esa mesa no tiene cuenta abierta.');
      const e = deps.espejo();
      const ahora = new Date().toISOString();
      const total = c.pedidos.reduce((s, p) => s + p.subtotal_usd, 0);
      const venta: VentaLocal = {
        id: randomUUID(),
        tipo: 'mesa',
        cuenta: { id: c.id, mesa: c.mesa, abierta_en: c.abierta_en, cerrada_en: ahora, mesero_nombre: c.mesero_nombre },
        pedidos: c.pedidos.map((p) => ({
          ...p,
          tipo_entrega: 'mesa',
          tasa_bs: e?.restaurante.tasa_bs ?? null,
          metodo_pago: datos.metodo,
          entregado_en: ahora,
          // El total viaja explicito: sin el, la nube lo dejaba en $0 (coalesce).
          total_usd: p.subtotal_usd,
        })),
        pagos: [
          {
            id: randomUUID(),
            monto_usd: total + (Number(datos.propina_usd) || 0),
            metodo: datos.metodo,
            referencia: datos.referencia?.trim() || null,
            propina_usd: Number(datos.propina_usd) || null,
            mesero_nombre: datos.mesero?.trim() || c.mesero_nombre,
          },
        ],
        creado_en: ahora,
      };
      estado.outbox.push(venta);
      delete estado.mesas[String(datos.mesa)];
      persistir();
      deps.log(`Mesa ${c.mesa} cobrada (${datos.metodo}); venta en el buzon de salida.`);
      try {
        deps.alVentaCerrada?.(venta);
      } catch {
        /* el cobro NUNCA se cae por el puente fiscal: el reintento lo agarra */
      }

      // COPIA-NEGOCIO: sale al COBRAR (el cliente ya cancelo), con el cobro real.
      if (deps.hayDespacho) {
        const fusion = new Map<string, { nombre: string; cantidad: number; nota: string | null }>();
        for (const p of c.pedidos) {
          for (const it of p.items) {
            const k = `${it.nombre}|${it.nota ?? ''}`;
            const ya = fusion.get(k);
            if (ya) ya.cantidad += it.cantidad;
            else fusion.set(k, { nombre: it.nombre, cantidad: it.cantidad, nota: it.nota ?? null });
          }
        }
        const lineas = armarTicketCopiaMesa({
          mesa: c.mesa,
          mesero: venta.pagos[0].mesero_nombre,
          items: [...fusion.values()],
          total_usd: total,
          propina_usd: Number(datos.propina_usd) || 0,
          tasa_bs: e?.restaurante.tasa_bs ?? null,
          pagos: [{ metodo: datos.metodo, monto_usd: total, referencia: datos.referencia?.trim() || null }],
        });
        const puesto = deps.puestoCuenta ?? e?.puestos.find((p) => p.activo)?.nombre ?? 'Cocina';
        deps.cola.encolar(puesto, comandaABytes(lineas), `mesa ${c.mesa} -> copia negocio`);
      }
      return venta;
    },

    /** Venta directa (mostrador o para llevar): pedido + cobro en un paso. */
    ventaDirecta(datos: { items: ItemEntrante[]; paraLlevar?: boolean; nota?: string; mesero?: string; metodo: string; referencia?: string }): PedidoLocal {
      const items = armarItems(datos.items);
      if (!items.length) throw new Error('El pedido esta vacio.');
      const e = deps.espejo();
      const ahora = new Date().toISOString();
      const p: PedidoLocal = {
        id: randomUUID(),
        codigo: nuevoCodigo(),
        tipo: 'directo',
        mesa: null,
        para_llevar: datos.paraLlevar !== false,
        nota: datos.nota?.trim() || null,
        mesero_nombre: datos.mesero?.trim() || null,
        recibido_en: ahora,
        items,
        subtotal_usd: items.reduce((s, it) => s + it.precio_usd * it.cantidad, 0),
      };
      const venta: VentaLocal = {
        id: randomUUID(),
        tipo: 'directo',
        // total_usd explicito: sin el, la nube lo dejaba en $0 (coalesce).
        pedidos: [{ ...p, tipo_entrega: 'pickup', tasa_bs: e?.restaurante.tasa_bs ?? null, metodo_pago: datos.metodo, entregado_en: ahora, total_usd: p.subtotal_usd }],
        pagos: [
          {
            id: randomUUID(),
            pedido_id: p.id,
            monto_usd: p.subtotal_usd,
            metodo: datos.metodo,
            referencia: datos.referencia?.trim() || null,
            propina_usd: null,
            mesero_nombre: datos.mesero?.trim() || null,
          },
        ],
        creado_en: ahora,
      };
      estado.outbox.push(venta);
      persistir();
      imprimir(p);
      deps.log(`Venta directa #${p.codigo} (${datos.metodo}); en el buzon de salida.`);
      try {
        deps.alVentaCerrada?.(venta);
      } catch {
        /* el cobro NUNCA se cae por el puente fiscal: el reintento lo agarra */
      }
      return p;
    },

    /** Imprime la PRE-CUENTA de una mesa (no fiscal) en la impresora de cuenta. */
    imprimirCuenta(mesa: string): void {
      const c = estado.mesas[String(mesa)];
      if (!c || !c.pedidos.length) throw new Error('Esa mesa no tiene cuenta abierta.');
      const e = deps.espejo();
      const renglones = c.pedidos.flatMap((p) => p.items.map((it) => ({ nombre: it.nombre, cantidad: it.cantidad, precio_usd: it.precio_usd })));
      const total = c.pedidos.reduce((s, p) => s + p.subtotal_usd, 0);
      const lineas = armarTicketCuenta({
        negocio: e?.restaurante.nombre ?? 'Gustito Xpress',
        mesa: c.mesa,
        renglones,
        total_usd: total,
        tasa_bs: e?.restaurante.tasa_bs ?? null,
      });
      const puesto = deps.puestoCuenta ?? deps.espejo()?.puestos.find((p) => p.activo)?.nombre ?? 'Cocina';
      deps.cola.encolar(puesto, comandaABytes(lineas), `cuenta mesa ${c.mesa}`);
      deps.log(`Pre-cuenta de la mesa ${c.mesa} a imprimir.`);
    },

    ventasPendientes(): VentaLocal[] {
      return [...estado.outbox];
    },

    /** Las que el puente fiscal todavia no ha entregado a la Estacion. */
    ventasSinSellar(): VentaLocal[] {
      return estado.outbox.filter((v) => !v.fiscal);
    },

    /**
     * Anota el sello que devolvio la Estacion. Si la venta ya se fue del buzon
     * (subio a la nube primero), no pasa nada: la cola de alla la sella y el
     * libro deduplica por el uuid.
     */
    marcarSellada(ventaId: string, fiscal: NonNullable<VentaLocal['fiscal']>): boolean {
      const v = estado.outbox.find((x) => x.id === ventaId);
      if (!v) return false;
      v.fiscal = fiscal;
      persistir();
      return true;
    },

    marcarSincronizada(ventaId: string): void {
      estado.outbox = estado.outbox.filter((v) => v.id !== ventaId);
      persistir();
    },

    resumen(): { mesasAbiertas: number; ventasPorSubir: number } {
      return { mesasAbiertas: Object.keys(estado.mesas).length, ventasPorSubir: estado.outbox.length };
    },
  };
}

export type Salon = ReturnType<typeof crearSalon>;
