// AUTO-ACTUALIZACION del cerebrito: el exe revisa (al arrancar y cada 12h) el
// archivo de version publicado en el R2 de Gustito. Si la version remota es MAS
// NUEVA, se aplica SOLA al minuto (sin tocar nada): descarga el exe nuevo al
// lado, escribe un .bat que cierra el programa, lo reemplaza y lo relanza
// oculto, y se despide. El boton manual del asistente sigue existiendo (sirve
// tambien para volver atras a proposito si un dia hace falta).
// En modo desarrollo (node dist) solo avisa, no aplica.
import { spawn } from 'node:child_process';
import { createWriteStream, statSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';

export const VERSION_CEREBRITO = '2.8.1';
// GX_URL_VERSION solo lo usan las pruebas (apuntar a un servidor local).
const URL_VERSION = process.env.GX_URL_VERSION ?? 'https://img.gustitoxpress.app/cerebrito/version.json';

export interface Actualizador {
  estado(): { actual: string; disponible: string | null; descargando: boolean };
  verificar(): Promise<void>;
  aplicar(): Promise<string | null>;
  detener(): void;
}

// ¿La version remota es MAS NUEVA que la instalada? Compara numero a numero
// (2.6.10 gana a 2.6.9). Sin este guard, un version.json rezagado degradaria
// solas todas las maquinas instaladas.
function esMasNueva(remota: string, local: string): boolean {
  const a = remota.split('.').map((n) => parseInt(n, 10) || 0);
  const b = local.split('.').map((n) => parseInt(n, 10) || 0);
  for (let i = 0; i < Math.max(a.length, b.length); i++) {
    const d = (a[i] ?? 0) - (b[i] ?? 0);
    if (d !== 0) return d > 0;
  }
  return false;
}

export function crearActualizador(deps: { base: string; esExe: boolean; log: (m: string) => void }): Actualizador {
  let disponible: string | null = null;
  let urlExe: string | null = null;
  let descargando = false;
  let timerAuto: ReturnType<typeof setTimeout> | null = null;

  // La actualizacion se aplica SOLA (nadie en el local tiene que tocar nada).
  // Al minuto y no al instante: deja que la maquina termine de arrancar y de
  // recoger lo pendiente. Si falla, se reintenta en la proxima revision.
  function programarAutoAplicar(): void {
    if (!deps.esExe || timerAuto || descargando) return;
    deps.log(`El cerebrito se va a actualizar solo a la version ${disponible} en 1 minuto.`);
    timerAuto = setTimeout(() => {
      void (async () => {
        const err = await aplicar();
        timerAuto = null;
        if (err) deps.log('La actualizacion automatica no se pudo aplicar: ' + err);
      })();
    }, 60_000);
  }

  async function verificar(): Promise<void> {
    try {
      const r = await fetch(URL_VERSION, { headers: { 'cache-control': 'no-cache' } });
      if (!r.ok) return;
      const j = (await r.json()) as { version?: string; url?: string };
      // Se avisa SOLO si de verdad es mas nueva. Comparar por "distinta" hacia
      // que un version.json rezagado ofreciera DEGRADAR (paso de verdad: el
      // aviso decia "hay una version nueva: 2.7.6" teniendo la 2.8.0).
      if (j.version && j.url && esMasNueva(j.version, VERSION_CEREBRITO)) {
        if (disponible !== j.version) deps.log(`Hay una version nueva del cerebrito: ${j.version} (tienes ${VERSION_CEREBRITO}).`);
        disponible = j.version;
        urlExe = j.url;
        programarAutoAplicar();
      } else {
        disponible = null;
        urlExe = null;
        if (timerAuto) {
          clearTimeout(timerAuto);
          timerAuto = null;
        }
      }
    } catch {
      /* sin internet: se revisa luego */
    }
  }

  async function aplicar(): Promise<string | null> {
    if (!deps.esExe) return 'La actualizacion automatica solo aplica en el programa instalado (.exe).';
    if (!disponible || !urlExe) return 'No hay version nueva que aplicar.';
    if (descargando) return 'Ya hay una descarga en curso.';
    descargando = true;
    try {
      deps.log(`Descargando la version ${disponible}...`);
      const r = await fetch(urlExe);
      if (!r.ok || !r.body) throw new Error('descarga fallida (' + r.status + ')');
      const nuevo = join(deps.base, 'Cerebrito-nuevo.exe');
      await pipeline(Readable.fromWeb(r.body as import('node:stream/web').ReadableStream), createWriteStream(nuevo));
      // Descarga COMPLETA o nada: un exe cortado por el wifi del local dejaria
      // el programa muerto (paso en el piloto: "no hay recursos de memoria").
      const esperado = Number(r.headers.get('content-length') ?? 0);
      const real = statSync(nuevo).size;
      if (esperado > 0 && real !== esperado) {
        throw new Error(`descarga incompleta (${real} de ${esperado} bytes); intenta de nuevo`);
      }
      if (real < 10_000_000) {
        throw new Error('la descarga no parece un programa completo; intenta de nuevo');
      }
      deps.log('Descarga lista y completa. Aplicando y reiniciando...');

      const exeFinal = join(deps.base, 'Cerebrito-Gustito.exe');
      const bat = join(deps.base, 'actualizar.bat');
      const lanzarVbs = join(deps.base, 'actualizar_oculto.vbs');
      const relanzarVbs = join(deps.base, 'relanzar_oculto.vbs');
      writeFileSync(
        bat,
        [
          '@echo off',
          'cd /d "%~dp0"',
          'timeout /t 2 /nobreak >nul',
          'taskkill /im Cerebrito-Gustito.exe /f >nul 2>&1',
          // El reemplazo REINTENTA hasta 5 veces (Windows a veces suelta el exe
          // con calma); si no lo logra, deja el programa viejo andando.
          'set n=0',
          ':reintento',
          'timeout /t 2 /nobreak >nul',
          `move /y "${nuevo}" "${exeFinal}" >nul 2>&1`,
          `if exist "${nuevo}" (`,
          '  set /a n+=1',
          '  if %n% lss 5 goto reintento',
          ')',
          // El relanzamiento va por wscript con ventana 0: cero consolas a la
          // vista (relanzar el exe directo con start abria una).
          `start "" wscript.exe "${relanzarVbs}"`,
          'timeout /t 2 /nobreak >nul',
          // Se recogen los temporales: si quedaran, el proximo arranque los veria.
          `del "${lanzarVbs}" >nul 2>&1`,
          `del "${relanzarVbs}" >nul 2>&1`,
          // Autodestruccion SIN el error "No se ha encontrado el archivo por lotes".
          '(goto) 2>nul & del "%~f0"',
        ].join('\r\n'),
      );
      // Los .vbs van con comillas DUPLICADAS (asi se escapan en VBScript);
      // jamas con JSON.stringify, que fue el 800A0401 del autoarranque.
      writeFileSync(lanzarVbs, `CreateObject("Wscript.Shell").Run "cmd.exe /c ""${bat}""", 0, False\r\n`);
      writeFileSync(relanzarVbs, `CreateObject("Wscript.Shell").Run """${exeFinal}"" --silencioso --oculto", 0, False\r\n`);
      // wscript es de ventana propia (GUI): lanzar el bat por aqui no abre ni
      // una consola minimizada en la barra, que era lo que quedaba antes.
      const hijo = spawn('wscript.exe', [lanzarVbs], { cwd: deps.base, detached: true, stdio: 'ignore', windowsHide: true });
      hijo.unref();
      setTimeout(() => process.exit(0), 800); // el bat nos remata si no salimos solos
      return null;
    } catch (e) {
      descargando = false;
      return 'No se pudo actualizar: ' + (e as Error).message;
    }
  }

  void verificar();
  const timer = setInterval(() => void verificar(), 12 * 3600_000);

  return {
    estado: () => ({ actual: VERSION_CEREBRITO, disponible, descargando }),
    verificar,
    aplicar,
    detener: () => {
      clearInterval(timer);
      if (timerAuto) clearTimeout(timerAuto);
    },
  };
}
