// AVISO DE VERSION NUEVA de la Estacion Fiscal.
//
// La Estacion NO se actualiza sola: los articulos 9 al 11 de la Providencia
// SNAT/2024/000121 exigen homologar CADA version del sistema antes de
// instalarla. Por eso aqui solo se REVISA el canal publicado y se avisa en el
// panel; aplicar la version es una decision del dueño, con su boton. (El
// Cerebrito si se aplica solo: aquel es un puente de impresion, no el sistema
// de facturacion.)
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';
import { contenidoVbs } from './arranque.js';

export const VERSION_ESTACION = process.env.FISCAL_VERSION ?? 'dev';
// EF_URL_VERSION solo lo usan las pruebas (apuntar a un servidor local).
const URL_VERSION = process.env.EF_URL_VERSION ?? 'https://img.gustitoxpress.app/estacion/version.json';

export interface Avisador {
  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? Numero a numero. Una
 * instalacion 'dev' (desarrollo y pruebas) nunca recibe aviso, y un canal
 * rezagado jamas propone volver atras.
 */
export function esMasNueva(remota: string, local: string): boolean {
  if (!/^\d+(\.\d+)*$/.test(remota) || !/^\d+(\.\d+)*$/.test(local)) return false;
  const a = remota.split('.').map(Number);
  const b = local.split('.').map(Number);
  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 crearAvisador(deps: { base: string; esExe: boolean; log: (m: string) => void }): Avisador {
  let disponible: string | null = null;
  let urlExe: string | null = null;
  let descargando = false;

  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 };
      if (j.version && j.url && esMasNueva(j.version, VERSION_ESTACION)) {
        if (disponible !== j.version) {
          deps.log(`Hay una version nueva de la Estacion: ${j.version} (esta instalacion tiene la ${VERSION_ESTACION}). Se aplica desde Inicio, cuando el dueño lo decida.`);
        }
        disponible = j.version;
        urlExe = j.url;
      } else {
        disponible = null;
        urlExe = null;
      }
    } catch {
      /* sin internet: se revisa luego */
    }
  }

  async function aplicar(): Promise<string | null> {
    if (!deps.esExe) return 'La actualizacion 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, 'Estacion-nueva.exe');
      await pipeline(Readable.fromWeb(r.body as import('node:stream/web').ReadableStream), createWriteStream(nuevo));
      // Descarga COMPLETA o nada: un exe cortado por el internet del local
      // dejaria el programa muerto.
      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); intente de nuevo`);
      }
      if (real < 10_000_000) {
        throw new Error('la descarga no parece un programa completo; intente de nuevo');
      }
      deps.log('Descarga completa. Aplicando y reiniciando...');
      const exeFinal = join(deps.base, 'Estacion-Fiscal.exe');
      // El relevo relanza OCULTO por el mismo .vbs del autoarranque; se
      // reescribe aqui por si el arranque con Windows estuviera apagado.
      const vbs = join(deps.base, 'arrancar_oculto.vbs');
      writeFileSync(vbs, contenidoVbs(`"${exeFinal}"`));
      const bat = join(deps.base, 'actualizar.bat');
      writeFileSync(
        bat,
        [
          '@echo off',
          'cd /d "%~dp0"',
          'timeout /t 2 /nobreak >nul',
          'taskkill /im Estacion-Fiscal.exe /f >nul 2>&1',
          // El reemplazo REINTENTA hasta 5 veces (Windows suelta el exe con
          // calma); si no lo logra, deja la version anterior 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',
          ')',
          `start "" wscript.exe "${vbs}"`,
          '(goto) 2>nul & del "%~f0"',
        ].join('\r\n'),
      );
      const hijo = spawn('cmd.exe', ['/c', 'start', '/min', '', bat], { cwd: deps.base, detached: true, stdio: 'ignore', windowsHide: true });
      hijo.unref();
      setTimeout(() => process.exit(0), 800); // el bat 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_ESTACION, disponible, descargando }),
    verificar,
    aplicar,
    detener: () => clearInterval(timer),
  };
}
