// Arranque del cerebrito (etapa 2: salon local-first).
//   Sin configuracion -> abre el ASISTENTE en el navegador (primera vez).
//   Con configuracion -> arranca TODO el local:
//     - espejo del menu (de disco; se refresca cuando hay nube)
//     - cola de impresion compartida
//     - salon local + POS para las tablets (puerto 8738, funciona SIN internet)
//     - sincronizador (espejo + latido + subir ventas del buzon)
//     - puente de impresion de la nube (pedidos web/QR; reintenta hasta tener internet)
//   --silencioso: no abre el navegador (lo usa el arranque con Windows).
import { exec, spawn } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { basename, dirname, join } from 'node:path';
import { crearActualizador } from './actualizador.js';
import { iniciarAsistente, PUERTO_ASISTENTE, type Contexto } from './asistente.js';
import { cargarEspejo, type Espejo } from './espejo.js';
import { ColaImpresion } from './impresoras.js';
import { iniciarCerebrito } from './motor.js';
import { iniciarPos, PUERTO_POS } from './pos.js';
import { crearPuenteFiscal } from './puenteFiscal.js';
import { crearSalon, type Salon } from './salon.js';
import { iniciarSincronizador, type Sincronizador } from './sincronizador.js';
import type { Config } from './tipos.js';

function esExe(): boolean {
  try {
    // En el exe (bundle CJS de SEA) `require` SI existe en el ambito del modulo.
    // En desarrollo (ESM con node) `typeof require` es 'undefined' y no explota.
    if (typeof require === 'function') {
      // eslint-disable-next-line @typescript-eslint/no-require-imports -- en el bundle CJS del exe no existe import(), solo require
      return (require('node:sea') as { isSea(): boolean }).isSea();
    }
  } catch {
    /* sigue el respaldo */
  }
  // Respaldo: si el ejecutable no es node, somos el exe empaquetado.
  return !/^node(\.exe)?$/i.test(basename(process.execPath));
}

const EXE = esExe();
const BASE = EXE ? dirname(process.execPath) : process.cwd();
const RUTA_CFG = process.argv.find((a) => a.endsWith('.json')) ?? join(BASE, 'cerebrito.config.json');
const SILENCIOSO = process.argv.includes('--silencioso');
const OCULTO = process.argv.includes('--oculto');

// SIEMPRE en segundo plano, SIN ventana negra: si nos abrieron con una consola
// visible (doble clic o PowerShell), nos relanzamos ocultos y detachados, y
// salimos. Asi cerrar esa consola/PowerShell ya NO apaga el cerebrito: queda
// corriendo de fondo. El browser (si toca) lo abre la copia oculta.
if (EXE && !OCULTO && process.platform === 'win32') {
  try {
    const hijo = spawn(process.execPath, [...process.argv.slice(1), '--oculto'], {
      detached: true,
      windowsHide: true,
      stdio: 'ignore',
    });
    hijo.unref();
    process.exit(0);
  } catch {
    /* si el relanzamiento falla, seguimos visibles: mejor visible que apagado */
  }
}

const bitacora: string[] = [];
function log(m: string): void {
  const linea = `${new Date().toLocaleTimeString('es-VE', { hour12: false })} ${m}`;
  bitacora.push(linea);
  if (bitacora.length > 200) bitacora.shift();
  console.log(`[cerebrito] ${m}`);
}

let config: Config | null = null;
try {
  if (existsSync(RUTA_CFG)) config = JSON.parse(readFileSync(RUTA_CFG, 'utf8')) as Config;
} catch (e) {
  log('La configuracion esta dañada, se ignora: ' + (e as Error).message);
}

// ---- Piezas del local (viven mientras haya configuracion) ----
let espejo: Espejo | null = null;
let cola: ColaImpresion | null = null;
let salon: Salon | null = null;
let sincro: Sincronizador | null = null;
let motor: Awaited<ReturnType<typeof iniciarCerebrito>> | null = null;
let reintentoPuente: NodeJS.Timeout | null = null;
let arrancando = false;

// El puente con la Estacion Fiscal: vive desde el arranque (aunque no haya
// salon todavia) y lee la config al momento, asi conectar la Estacion desde el
// asistente no necesita reiniciar nada.
const puenteFiscal = crearPuenteFiscal({ config: () => config, salon: () => salon, log });

function armarLocal(): void {
  if (!config) return;
  espejo = espejo ?? cargarEspejo(BASE);
  if (!cola) cola = new ColaImpresion(config.impresoras, log);
  else cola.actualizarImpresoras(config.impresoras);
  if (!salon) {
    salon = crearSalon({
      base: BASE,
      espejo: () => espejo,
      cola,
      log,
      hayDespacho: !!config.despacho,
      puestoCuenta: config.despacho?.puesto ?? config.impresoras[0]?.puesto ?? null,
      alVentaCerrada: () => puenteFiscal.disparar(),
    });
  }
  if (!sincro && !config.enReserva) {
    sincro = iniciarSincronizador({
      cfg: config,
      base: BASE,
      salon,
      espejoSet: (e) => {
        espejo = e;
      },
      log,
    });
  }
}

// LA NUBE MANDA: si la config del negocio en la nube (impresoras, despacho,
// pin) es distinta a la local, se aplica y el motor se reinicia solo. Asi lo
// configurado en una laptop aparece en las otras, y un arreglo remoto cura
// esta maquina sin tocarla. Credenciales y datos locales NO se tocan.
function aplicarConfigNube(prefs: import('./nubeConfig.js').PrefsNube): void {
  if (!config || config.enReserva) return;
  const clave = (c: { impresoras?: { puesto: string; tipo?: string; host: string; puerto?: number }[]; despacho?: { puesto: string; tipos?: string[] }; sinPuesto?: string; pinSala?: string }) =>
    JSON.stringify({
      i: (c.impresoras ?? []).map((x) => [x.puesto, x.tipo ?? 'tcp', x.host, x.puerto ?? 0]),
      d: c.despacho ? [c.despacho.puesto, [...(c.despacho.tipos ?? [])].sort()] : null,
      s: c.sinPuesto ?? 'defecto',
      p: c.pinSala ?? null,
    });
  const base = config;
  if (clave(base) === clave(prefs)) return;
  const nueva: Config = {
    ...base,
    impresoras: prefs.impresoras ?? base.impresoras,
    sinPuesto: prefs.sinPuesto ?? 'defecto',
  };
  if (prefs.despacho) nueva.despacho = prefs.despacho;
  else delete nueva.despacho;
  if (prefs.pinSala) nueva.pinSala = prefs.pinSala;
  else delete nueva.pinSala;
  config = nueva;
  writeFileSync(RUTA_CFG, JSON.stringify(nueva, null, 2));
  log('La configuracion del negocio cambio en la nube: aplicada aqui tambien.');
  // Fuera del callback del motor (que se va a detener a si mismo).
  setTimeout(() => void reiniciarMotor(), 0);
}

/** El puente de la nube reintenta solo: arrancar sin internet NO lo mata. */
async function reiniciarMotor(): Promise<string | null> {
  if (arrancando) return 'Ya hay un arranque en curso, espera un momento.';
  arrancando = true;
  try {
    if (reintentoPuente) {
      clearTimeout(reintentoPuente);
      reintentoPuente = null;
    }
    if (motor) {
      motor.detener();
      motor = null;
    }
    if (!config || config.enReserva) {
      log(config?.enReserva ? 'Laptop EN RESERVA: no imprime ni vende.' : 'Sin configuracion.');
      return null;
    }
    armarLocal();
    try {
      motor = await iniciarCerebrito(config, { log, colaExterna: cola ?? undefined, alConfigNube: aplicarConfigNube });
      return null;
    } catch (e) {
      log('El puente de la nube no arranco (¿sin internet?): ' + (e as Error).message + '. El SALON LOCAL sigue andando; reintento en 30s.');
      reintentoPuente = setTimeout(() => void reiniciarMotor(), 30_000);
      return null; // el salon local basta para operar
    }
  } finally {
    arrancando = false;
  }
}

const actualizador = crearActualizador({ base: BASE, esExe: EXE, log });

const ctx: Contexto = {
  base: BASE,
  esExe: EXE,
  urlSupabase: () => config?.supabaseUrl ?? process.env.GX_URL ?? '',
  anonKey: () => config?.anonKey ?? process.env.GX_ANON ?? '',
  obtenerConfig: () => config,
  guardarConfig(cfg: Config) {
    config = cfg;
    writeFileSync(RUTA_CFG, JSON.stringify(cfg, null, 2));
    log('Configuracion guardada.');
  },
  motor: () => motor,
  reiniciarMotor,
  logs: () => bitacora,
  log,
  comandoArranque: () =>
    EXE ? `"${process.execPath}" --silencioso --oculto` : `"${process.execPath}" "${process.argv[1] ?? ''}" --silencioso`,
  extras: () => ({
    puertoPos: PUERTO_POS,
    pinListo: !!config?.pinSala,
    salon: salon?.resumen() ?? null,
    nube: sincro?.estado() ?? null,
    espejoListo: !!espejo,
    version: actualizador.estado(),
    fiscal: puenteFiscal.estado(),
  }),
  aplicarActualizacion: () => actualizador.aplicar(),
  verificarActualizacion: () => void actualizador.verificar(),
  probarEstacionFiscal: () => puenteFiscal.probar(),
  dispararPuenteFiscal: () => puenteFiscal.disparar(),
};

// El proceso NO se muere por un error suelto: en una laptop de restaurante
// nadie lo va a relanzar a mitad de servicio. Se anota y se sigue.
process.on('uncaughtException', (e) => log('Error inesperado (sigo andando): ' + e.message));
process.on('unhandledRejection', (e) => log('Promesa rechazada (sigo andando): ' + String(e)));

iniciarAsistente(ctx);
iniciarPos({
  pin: () => config?.pinSala ?? null,
  espejo: () => espejo,
  salon: () => salon,
  enReserva: () => !!config?.enReserva,
  nubeViva: () => sincro?.estado().nubeViva ?? false,
  log,
});
void reiniciarMotor();

if (!SILENCIOSO && !config) {
  exec(`start "" "http://127.0.0.1:${PUERTO_ASISTENTE}"`, { windowsHide: true });
}
