// EL PUENTE CON LA ESTACION FISCAL (el instalable hermano, 127.0.0.1:8739).
//
// Cada venta que la caja cierra se le ENTREGA DIRECTO a la Estacion, sin pasar
// por la nube: la Estacion sella local y transmite cuando pueda, asi el negocio
// factura aunque el internet este caido.
//
// La misma venta viaja despues a la nube por el buzon de salida, y la nube la
// vuelve a encolar para la Estacion. No importa: el origen_id (pedido:<uuid> /
// cuenta:<uuid>) es IDENTICO por las dos vias, porque sincronizar_venta_local
// inserta en la nube con los MISMOS uuid que genero esta laptop. El libro
// fiscal deduplica solo: el que llega segundo recibe el numero del documento
// que ya existe, nunca un documento nuevo.
//
// FRONTERA: aqui NO se calcula ni un centavo de impuesto. Se manda la venta
// cruda (renglones con su precio, tal como se cobro) y la alicuota la pone la
// Estacion, igual que cuando hala de la nube.
import type { Salon, VentaLocal } from './salon.js';
import type { Config } from './tipos.js';

export interface EstadoPuenteFiscal {
  /** ¿Esta laptop tiene la Estacion configurada? */
  configurada: boolean;
  /** null = todavia no se ha probado en esta corrida. */
  conectada: boolean | null;
  negocio: string | null;
  rif: string | null;
  /** El serial que la imprenta le asigno a la Estacion (va en el contrato). */
  serial: string | null;
  porSellar: number;
  /** "A-12" del ultimo documento sellado desde aqui. */
  ultimoSello: string | null;
  problema: string | null;
}

interface ResultadoVenta {
  sellada?: boolean;
  serie?: string;
  numero?: number;
  numeroControl?: string | null;
  sello?: string | null;
  problema?: string;
  ok?: boolean;
  negocio?: string;
  rif?: string;
  serial?: string | null;
}

/** Una venta local en la MISMA forma que viaja por la cola de la nube. */
interface CargaFiscal {
  tipo: 'pedido' | 'cuenta';
  origen_id: string;
  venta: Record<string, unknown>;
}

const REINTENTO_MS = 20_000;

export function crearPuenteFiscal(deps: {
  config: () => Config | null;
  salon: () => Salon | null;
  log: (m: string) => void;
}) {
  let conectada: boolean | null = null;
  let negocio: string | null = null;
  let rif: string | null = null;
  let serialEstacion: string | null = null;
  let ultimoSello: string | null = null;
  let problema: string | null = null;
  let corriendo: Promise<void> | null = null;
  let repetir = false;
  let avisoCaida = false;

  const cfgFiscal = () => deps.config()?.estacionFiscal ?? null;

  async function llamar(ruta: string, cuerpo?: unknown): Promise<Response> {
    const ef = cfgFiscal();
    if (!ef) throw new Error('Sin Estacion configurada');
    const control = new AbortController();
    const t = setTimeout(() => control.abort(), 8_000);
    try {
      return await fetch(ef.url.replace(/\/+$/, '') + ruta, {
        method: cuerpo === undefined ? 'GET' : 'POST',
        headers: {
          authorization: `Bearer ${ef.llave}`,
          ...(cuerpo === undefined ? {} : { 'content-type': 'application/json' }),
        },
        body: cuerpo === undefined ? undefined : JSON.stringify(cuerpo),
        signal: control.signal,
      });
    } finally {
      clearTimeout(t);
    }
  }

  /**
   * Arma la carga con los MISMOS campos que la nube manda en 'pendientes'
   * (fiscal.ventas_pendientes): una venta de mesa es UN documento por cuenta;
   * una venta directa, un documento por pedido.
   */
  function cargasDe(v: VentaLocal): CargaFiscal[] {
    const consumidor = { nombre: null, rif: null, razon_social: null, direccion: null, correo: null, telefono: null };
    if (v.cuenta) {
      const c = v.cuenta;
      return [{
        tipo: 'cuenta',
        origen_id: c.id,
        venta: {
          codigo: `MESA-${c.mesa}`,
          fecha: c.cerrada_en,
          tipo_entrega: 'mesa',
          mesa: c.mesa,
          metodo_pago: v.pagos[0]?.metodo ?? null,
          tasa_bs: v.pedidos[0]?.tasa_bs ?? null,
          total_usd: v.pedidos.reduce((s, p) => s + p.total_usd, 0),
          costo_delivery_usd: 0,
          // La propina va aparte: es servicio voluntario, no base imponible.
          propina_usd: v.pagos.reduce((s, p) => s + (Number(p.propina_usd) || 0), 0),
          comprador: consumidor,
          renglones: v.pedidos.flatMap((p) =>
            p.items.map((it) => ({ nombre: it.nombre, cantidad: it.cantidad, precio_usd: it.precio_usd })),
          ),
        },
      }];
    }
    return v.pedidos.map((p) => ({
      tipo: 'pedido' as const,
      origen_id: p.id,
      venta: {
        codigo: p.codigo,
        fecha: p.entregado_en,
        tipo_entrega: p.tipo_entrega,
        mesa: null,
        metodo_pago: p.metodo_pago,
        origen: 'caja',
        tasa_bs: p.tasa_bs,
        total_usd: p.total_usd,
        costo_delivery_usd: 0,
        comprador: consumidor,
        renglones: p.items.map((it) => ({ nombre: it.nombre, cantidad: it.cantidad, precio_usd: it.precio_usd })),
      },
    }));
  }

  /** Un problema solo se anota una vez por caida, no cada 20 segundos. */
  function anotarCaida(m: string): void {
    problema = m;
    if (!avisoCaida) {
      deps.log(m + ' Las ventas quedan en cola local y la via de la nube sigue andando.');
      avisoCaida = true;
    }
  }

  /** Una pasada por lo que falte por sellar. */
  async function unaPasada(): Promise<void> {
    const ef = cfgFiscal();
    const salon = deps.salon();
    if (!ef || !salon) return;
    const pendientes = salon.ventasSinSellar();
    if (!pendientes.length) return;
    for (const v of pendientes) {
      let sello: NonNullable<VentaLocal['fiscal']> | null = null;
      for (const carga of cargasDe(v)) {
        let r: Response;
        try {
          r = await llamar('/api/venta', carga);
        } catch {
          conectada = false;
          anotarCaida('La Estacion Fiscal no responde.');
          return;
        }
        const cuerpo = (await r.json().catch(() => ({}))) as ResultadoVenta;
        if (r.status === 401) {
          conectada = true;
          anotarCaida('La Estacion no acepta la llave de esta laptop. Revisala en la pantalla de la Estacion.');
          return;
        }
        if (r.status === 503) {
          conectada = true;
          anotarCaida('La Estacion esta abierta pero sin configurar (falta el RIF del negocio).');
          return;
        }
        if (!cuerpo.sellada) {
          // Reloj corrido u otro freno de la Estacion: afecta a todas las
          // ventas, se reintenta completo en la proxima ronda.
          conectada = true;
          anotarCaida(`La Estacion no sello: ${cuerpo.problema ?? 'sin detalle'}.`);
          return;
        }
        conectada = true;
        avisoCaida = false;
        problema = null;
        sello = {
          serie: cuerpo.serie ?? '',
          numero: cuerpo.numero ?? 0,
          control: cuerpo.numeroControl ?? null,
          sello: cuerpo.sello ?? null,
          sellada_en: new Date().toISOString(),
        };
        ultimoSello = `${sello.serie}-${sello.numero}`;
        deps.log(`Venta ${carga.venta.codigo as string} sellada por la Estacion: documento ${ultimoSello}.`);
      }
      if (sello) salon.marcarSellada(v.id, sello);
    }
  }

  /**
   * Entrega a la Estacion lo que falte por sellar. Nunca lanza: un problema
   * del puente no puede tumbar la caja.
   *
   * Si ya hay una corrida en vuelo, se ESPERA esa misma (no se pisa), y si
   * mientras corria entro una venta nueva, se da una pasada mas al terminar:
   * asi ni se duplica trabajo ni se queda nada sin sellar hasta el timer.
   */
  function sellarAhora(): Promise<void> {
    if (corriendo) {
      repetir = true;
      return corriendo;
    }
    corriendo = (async () => {
      do {
        repetir = false;
        await unaPasada();
      } while (repetir);
    })().finally(() => {
      corriendo = null;
    });
    return corriendo;
  }

  /** El apreton de manos: ¿la llave sirve y a que negocio le hablamos? */
  async function probar(): Promise<{ ok: boolean; negocio?: string | null; rif?: string | null; problema?: string }> {
    try {
      const r = await llamar('/api/puente');
      const cuerpo = (await r.json().catch(() => ({}))) as ResultadoVenta;
      if (!r.ok || !cuerpo.ok) {
        conectada = true; // respondio, aunque haya dicho que no
        const p = cuerpo.problema ?? `La Estacion respondio ${r.status}`;
        problema = p;
        return { ok: false, problema: p };
      }
      conectada = true;
      avisoCaida = false;
      problema = null;
      negocio = cuerpo.negocio ?? null;
      rif = cuerpo.rif ?? null;
      serialEstacion = cuerpo.serial ?? null;
      return { ok: true, negocio, rif };
    } catch {
      conectada = false;
      const p = 'La Estacion no respondio. ¿Esta abierta en esta computadora?';
      problema = p;
      return { ok: false, problema: p };
    }
  }

  const timer = setInterval(() => void sellarAhora(), REINTENTO_MS);

  // El apreton de manos al arrancar (si hay Estacion configurada): asi la
  // tarjeta muestra el negocio de una vez y una Estacion caida se nota sin
  // esperar a que haya una venta que sellar.
  setTimeout(() => {
    if (cfgFiscal()) void probar();
  }, 3_000);

  return {
    /** Lo llama la caja apenas cierra una venta (y el timer, de respaldo). */
    disparar(): void {
      void sellarAhora();
    },
    /** La version que espera: para las pruebas y el primer enganche. */
    sellarAhora,
    probar,
    estado(): EstadoPuenteFiscal {
      return {
        configurada: !!cfgFiscal(),
        conectada,
        negocio,
        rif,
        serial: serialEstacion,
        porSellar: deps.salon()?.ventasSinSellar().length ?? 0,
        ultimoSello,
        problema,
      };
    },
    detener(): void {
      clearInterval(timer);
    },
  };
}

export type PuenteFiscal = ReturnType<typeof crearPuenteFiscal>;
