/**
 * Construye el manual del sistema (plan §6.9) a partir del código del panel y del backend.
 *
 *   pnpm build && pnpm manual:construir -- \
 *     --frontend ../frontend --frontend-ref origin/dev \
 *     --backend ../backend --backend-ref origin/pos-dev [--salida manual]
 *
 * Toma una foto del código de cada repo en la ref pedida (git archive: no depende de
 * lo que esté checkouteado) y arma una ficha por pantalla:
 *   - pantallas del menú (utils/dataEstatica.jsx + grupos de hooks/useMenuAcceso.js),
 *     rutas (routers/routes.jsx) y secciones de Configuración (pages/ConfiguracionNew.jsx);
 *   - de cada pantalla, los textos que ve el usuario (títulos, campos, botones, ayudas,
 *     mensajes), siguiendo los imports de sus componentes;
 *   - reglas del backend: los mensajes de error de cada dominio.
 * La foto del código queda en <salida>/codigo para que el agente lea un archivo puntual.
 */
import { parse } from '@babel/parser';
import traverseModule, { NodePath } from '@babel/traverse';
import type * as t from '@babel/types';
import { execFileSync } from 'child_process';
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'fs';
import { dirname, join, relative, resolve } from 'path';

const traverse = (traverseModule as unknown as { default?: typeof traverseModule }).default ?? traverseModule;

export interface Texto {
  t: string;
  archivo: string;
  linea: number;
}

export interface Ficha {
  id: string;
  tipo: 'pantalla' | 'reglas';
  titulo: string;
  /** Cómo llega el usuario: "Ventas › Facturación", "Configuración › Facturación › Métodos de Pago". */
  miga: string;
  ruta: string | null;
  /** Desde qué pantallas se llega acá (botones o links que navegan a esta ruta). */
  desde?: string[];
  /** Una línea para el índice de pantallas: títulos y botones del componente principal. */
  resumen?: string;
  archivos: string[];
  textos: Texto[];
}

export interface IndiceManual {
  generado: string;
  refs: Record<string, string>;
  fichas: Ficha[];
}

// ── Parseo ──────────────────────────────────────────────────────────────

const cacheAst = new Map<string, t.File | null>();

function ast(archivo: string): t.File | null {
  if (cacheAst.has(archivo)) return cacheAst.get(archivo) ?? null;
  let resultado: t.File | null = null;
  try {
    resultado = parse(readFileSync(archivo, 'utf8'), {
      sourceType: 'module',
      errorRecovery: true,
      plugins: archivo.endsWith('.ts') ? ['typescript', 'decorators-legacy'] : ['jsx'],
    });
  } catch {
    resultado = null;
  }
  cacheAst.set(archivo, resultado);
  return resultado;
}

/** Valor de texto de un nodo literal (string o template sin expresiones complejas). */
function literal(n: t.Node | null | undefined): string | null {
  if (!n) return null;
  if (n.type === 'StringLiteral') return n.value;
  if (n.type === 'TemplateLiteral') return n.quasis.map((q) => q.value.cooked ?? '').join('…');
  if (n.type === 'JSXExpressionContainer') return literal(n.expression);
  return null;
}

/**
 * Todos los textos literales de una expresión, siguiendo condiciones y alternativas:
 * `isMobile ? "Nueva" : "Nueva Condición"`, `error?.message || "2-15 caracteres"`.
 */
function literales(n: t.Node | null | undefined): string[] {
  if (!n) return [];
  if (n.type === 'JSXExpressionContainer') return literales(n.expression);
  if (n.type === 'ConditionalExpression') return [...literales(n.consequent), ...literales(n.alternate)];
  if (n.type === 'LogicalExpression') return [...literales(n.left), ...literales(n.right)];
  const v = literal(n);
  return v ? [v] : [];
}

/** ¿Parece texto para personas? Descarta claves, rutas, clases css, colores, códigos. */
function esTextoVisible(s: string): boolean {
  const v = s.replace(/\s+/g, ' ').trim();
  if (v.length < 2 || v.length > 400) return false;
  if (!/[a-záéíóúñ]/i.test(v)) return false;
  if (/^[a-z0-9_.:/#-]+$/.test(v) && !v.includes(' '))
    return /^[a-záéíóúñ]+$/i.test(v) && v.length > 3 && /[A-ZÁÉÍÓÚ]/.test(v[0]);
  if (/^(lucide|mdi|material-symbols|ph|tabler):/.test(v)) return false;
  if (/^(#|rgb|hsl|var\()/.test(v)) return false;
  return true;
}

const ATRIBUTOS = new Set([
  'label',
  'title',
  'titulo',
  'placeholder',
  'helperText',
  'tooltip',
  'desc',
  'description',
  'descripcion',
  'text',
  'texto',
  'hint',
  'ayuda',
  'intro',
  'subtitle',
  'subtitulo',
  'emptyText',
  'mensaje',
  'message',
  'confirmText',
  'cancelText',
  'content',
  'aria-label',
]);

/** Textos que ve el usuario en un archivo del panel. */
export function textosDeArchivo(archivo: string, raizCodigo: string): Texto[] {
  const arbol = ast(archivo);
  if (!arbol) return [];
  const rel = relative(raizCodigo, archivo);
  const salida: Texto[] = [];
  const agregar = (s: string | null, n: t.Node) => {
    if (s && esTextoVisible(s))
      salida.push({ t: s.replace(/\s+/g, ' ').trim(), archivo: rel, linea: n.loc?.start.line ?? 0 });
  };
  traverse(arbol, {
    JSXText(p) {
      agregar(p.node.value, p.node);
    },
    // Hijos de un elemento que son expresiones: {isEdit ? "Editar" : "Nueva"}, {x || "Sin datos"}.
    JSXExpressionContainer(p) {
      if (p.parent.type === 'JSXElement' || p.parent.type === 'JSXFragment')
        for (const v of literales(p.node.expression)) agregar(v, p.node);
    },
    JSXAttribute(p) {
      const nombre = p.node.name.type === 'JSXIdentifier' ? p.node.name.name : '';
      if (ATRIBUTOS.has(nombre)) for (const v of literales(p.node.value)) agregar(v, p.node);
    },
    ObjectProperty(p) {
      const k = p.node.key;
      const nombre = k.type === 'Identifier' ? k.name : k.type === 'StringLiteral' ? k.value : '';
      if (ATRIBUTOS.has(nombre)) for (const v of literales(p.node.value)) agregar(v, p.node);
    },
    CallExpression(p) {
      const c = p.node.callee;
      const nombre =
        c.type === 'MemberExpression' && c.object.type === 'Identifier' && c.property.type === 'Identifier'
          ? `${c.object.name}.${c.property.name}`
          : c.type === 'Identifier'
            ? c.name
            : '';
      if (/^(toast(\.\w+)?|enqueueSnackbar|setError|mostrarError|alert)$/.test(nombre)) {
        agregar(literal(p.node.arguments[0]), p.node);
      }
    },
  });
  // Mismo texto en el mismo archivo: una vez.
  const vistos = new Set<string>();
  return salida.filter((x) => (vistos.has(x.t) ? false : (vistos.add(x.t), true)));
}

// ── Grafo de imports ────────────────────────────────────────────────────

const EXTENSIONES = ['', '.jsx', '.js', '/index.jsx', '/index.js'];
/** Carpetas compartidas por todo el panel: sus textos no describen ninguna pantalla en particular. */
const EXCLUIDAS =
  /\/(_standards|common|hooks|store|api|utils|tanstack|services|context|styles|assets|tours|routers|lib)\//;

function resolverImport(desde: string, spec: string): string | null {
  if (!spec.startsWith('.')) return null;
  const base = resolve(dirname(desde), spec);
  for (const e of EXTENSIONES) {
    const f = base + e;
    if (existsSync(f) && statSync(f).isFile()) return f;
  }
  return null;
}

function importsLocales(archivo: string): string[] {
  const arbol = ast(archivo);
  if (!arbol) return [];
  const salida: string[] = [];
  for (const n of arbol.program.body) {
    if (n.type === 'ImportDeclaration') {
      const f = resolverImport(archivo, n.source.value);
      if (f) salida.push(f);
    }
  }
  return salida;
}

/** Archivos de una pantalla: el componente y lo que importa, hasta `profundidad` niveles. */
function arbolDeArchivos(raiz: string, raizCodigo: string, profundidad = 3): string[] {
  const vistos = new Set<string>([raiz]);
  let nivel = [raiz];
  for (let i = 0; i < profundidad; i++) {
    const siguiente: string[] = [];
    for (const f of nivel) {
      for (const g of importsLocales(f)) {
        const rel = '/' + relative(raizCodigo, g);
        if (vistos.has(g) || EXCLUIDAS.test(rel) || /\/index\.jsx?$/.test(g)) continue;
        if (!/\.(jsx|js)$/.test(g)) continue;
        vistos.add(g);
        siguiente.push(g);
      }
    }
    nivel = siguiente;
  }
  return [...vistos];
}

// ── Menú, rutas y Configuración ─────────────────────────────────────────

/** Objetos de un array literal exportado (`export const X = [...]`), con sus props de texto. */
function arrayExportado(archivo: string, nombre: string): Array<Record<string, unknown>> {
  const arbol = ast(archivo);
  if (!arbol) return [];
  let arr: t.ArrayExpression | null = null;
  traverse(arbol, {
    VariableDeclarator(p) {
      if (p.node.id.type === 'Identifier' && p.node.id.name === nombre && p.node.init?.type === 'ArrayExpression') {
        arr = p.node.init;
      }
    },
  });
  return arr
    ? ((arr as t.ArrayExpression).elements.map((e) => objetoPlano(e as t.Node)).filter(Boolean) as Array<
        Record<string, unknown>
      >)
    : [];
}

function objetoPlano(n: t.Node | null): Record<string, unknown> | null {
  if (!n || n.type !== 'ObjectExpression') return null;
  const o: Record<string, unknown> = {};
  for (const p of n.properties) {
    if (p.type !== 'ObjectProperty') continue;
    const k = p.key.type === 'Identifier' ? p.key.name : p.key.type === 'StringLiteral' ? p.key.value : null;
    if (!k) continue;
    const v = p.value as t.Node;
    if (v.type === 'ArrayExpression') o[k] = v.elements.map((e) => objetoPlano(e as t.Node) ?? literal(e as t.Node));
    else o[k] = literal(v) ?? (v.type === 'BooleanLiteral' ? v.value : v.type === 'NullLiteral' ? null : undefined);
  }
  return o;
}

/** `const X = lazyNamed(() => import("../pages/X"), "X")` e imports estáticos → archivo del componente. */
function componentesDeArchivo(archivo: string): Map<string, string> {
  const arbol = ast(archivo);
  const mapa = new Map<string, string>();
  if (!arbol) return mapa;
  for (const n of arbol.program.body) {
    if (n.type === 'ImportDeclaration') {
      const f = resolverImport(archivo, n.source.value);
      if (f) for (const s of n.specifiers) mapa.set(s.local.name, f);
    }
  }
  traverse(arbol, {
    VariableDeclarator(p) {
      if (p.node.id.type !== 'Identifier' || !p.node.init) return;
      const nombre = p.node.id.name;
      let spec: string | null = null;
      p.traverse({
        Import(q: NodePath<t.Import>) {
          const llamada = q.parentPath?.node as t.CallExpression | undefined;
          if (llamada) spec = literal(llamada.arguments[0]);
        },
      });
      if (spec) {
        const f = resolverImport(archivo, spec);
        if (f) mapa.set(nombre, f);
      }
    },
  });
  return mapa;
}

const ENVOLTORIOS = new Set(['Layout', 'ProtectedRoute', 'Suspense', 'Navigate', 'Fragment', 'React.Fragment']);

function nombreJsx(n: t.JSXElement): string {
  const nm = n.openingElement.name;
  return nm.type === 'JSXIdentifier'
    ? nm.name
    : nm.type === 'JSXMemberExpression'
      ? `${(nm.object as t.JSXIdentifier).name}.${nm.property.name}`
      : '';
}

/** El primer componente dentro de un elemento que no sea un envoltorio (Layout, ProtectedRoute…). */
function componentePrincipal(n: t.Node): string | null {
  if (n.type === 'JSXExpressionContainer') return componentePrincipal(n.expression);
  if (n.type !== 'JSXElement') return null;
  const nombre = nombreJsx(n);
  if (!ENVOLTORIOS.has(nombre)) return nombre;
  for (const h of n.children) {
    const c = componentePrincipal(h);
    if (c) return c;
  }
  return null;
}

function rutas(archivoRutas: string): Array<{ ruta: string; componente: string; archivo: string }> {
  const arbol = ast(archivoRutas);
  if (!arbol) return [];
  const componentes = componentesDeArchivo(archivoRutas);
  const salida: Array<{ ruta: string; componente: string; archivo: string }> = [];
  traverse(arbol, {
    JSXElement(p) {
      if (nombreJsx(p.node) !== 'Route') return;
      let ruta: string | null = null;
      let elemento: t.Node | null = null;
      for (const a of p.node.openingElement.attributes) {
        if (a.type !== 'JSXAttribute' || a.name.type !== 'JSXIdentifier') continue;
        if (a.name.name === 'path') ruta = literal(a.value);
        if (a.name.name === 'element' && a.value) elemento = a.value;
      }
      const componente = elemento ? componentePrincipal(elemento) : null;
      const archivo = componente ? componentes.get(componente) : undefined;
      if (ruta && componente && archivo) salida.push({ ruta, componente, archivo });
    },
  });
  return salida;
}

/** Casos del switch de ConfiguracionNew: id de sección → componente que se muestra. */
function casosConfiguracion(archivo: string): Map<string, string> {
  const arbol = ast(archivo);
  const mapa = new Map<string, string>();
  if (!arbol) return mapa;
  const componentes = componentesDeArchivo(archivo);
  traverse(arbol, {
    SwitchCase(p) {
      const id = literal(p.node.test);
      if (!id) return;
      for (const s of p.node.consequent) {
        if (s.type === 'ReturnStatement' && s.argument) {
          const c = componentePrincipal(s.argument);
          const f = c ? componentes.get(c) : undefined;
          if (f) mapa.set(id, f);
        }
      }
    },
  });
  return mapa;
}

/** "POSAdmin" → "POS Admin", "NuevaNotaCreditoTemplate" → "Nueva Nota Credito". */
function nombreLegible(componente: string): string {
  return componente
    .replace(/(Page|Template|Tab|Standalone|Design|Module|Section|Config)$/, '')
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
    .trim();
}

// ── Backend ─────────────────────────────────────────────────────────────

function archivosTs(dir: string): string[] {
  const salida: string[] = [];
  for (const e of readdirSync(dir, { withFileTypes: true })) {
    const f = join(dir, e.name);
    if (e.isDirectory()) salida.push(...archivosTs(f));
    else if (/\.(service|dto|controller)\.ts$/.test(e.name) || /\/dto\/.*\.ts$/.test(f)) salida.push(f);
  }
  return salida.filter((f) => !/\.spec\.ts$/.test(f));
}

/** Mensajes de error y de validación del backend: las reglas que el usuario puede chocar. */
function reglasBackend(raizSrc: string, raizCodigo: string): Ficha[] {
  const porDominio = new Map<string, Texto[]>();
  for (const f of archivosTs(raizSrc)) {
    const arbol = ast(f);
    if (!arbol) continue;
    const dominio = relative(raizSrc, f).split('/')[0];
    const lista = porDominio.get(dominio) ?? [];
    traverse(arbol, {
      NewExpression(p) {
        const c = p.node.callee;
        if (c.type === 'Identifier' && /Exception$/.test(c.name)) {
          const s = literal(p.node.arguments[0]);
          if (s && esTextoVisible(s) && /\s/.test(s))
            lista.push({ t: s, archivo: relative(raizCodigo, f), linea: p.node.loc?.start.line ?? 0 });
        }
      },
      ObjectProperty(p) {
        const k = p.node.key;
        if (k.type === 'Identifier' && k.name === 'message') {
          const s = literal(p.node.value);
          if (s && esTextoVisible(s) && /\s/.test(s))
            lista.push({ t: s, archivo: relative(raizCodigo, f), linea: p.node.loc?.start.line ?? 0 });
        }
      },
    });
    porDominio.set(dominio, lista);
  }
  return [...porDominio.entries()]
    .filter(([, l]) => l.length)
    .map(([dominio, textos]) => ({
      id: `reglas:${dominio}`,
      tipo: 'reglas' as const,
      titulo: `Reglas: ${dominio.replace(/-/g, ' ')}`,
      miga: `Reglas del sistema (${dominio.replace(/-/g, ' ')})`,
      ruta: null,
      resumen: textos.slice(0, 6).map((x) => x.t).join(' · ').slice(0, 300),
      archivos: [...new Set(textos.map((x) => x.archivo))],
      textos,
    }));
}

// ── Armado ──────────────────────────────────────────────────────────────

function fichaPantalla(
  id: string,
  titulo: string,
  miga: string,
  ruta: string,
  archivo: string,
  raizCodigo: string,
): Ficha {
  const archivos = arbolDeArchivos(archivo, join(raizCodigo, 'frontend', 'src'));
  const textos = archivos.flatMap((f) => textosDeArchivo(f, raizCodigo));
  return {
    id,
    tipo: 'pantalla',
    titulo,
    miga,
    ruta,
    archivos: archivos.map((f) => relative(raizCodigo, f)),
    textos,
  };
}

/**
 * Una línea por pantalla para el índice: los títulos y botones más DISTINTIVOS (los que
 * aparecen en menos pantallas), así el cobro del POS lleva "Cobrar Venta", "Condición de
 * pago", "Venta a crédito" y no "Cancelar" o "Guardar".
 */
function resumirPantallas(fichas: Ficha[]) {
  const enCuantas = new Map<string, number>();
  for (const f of fichas) for (const t of new Set(f.textos.map((x) => x.t))) enCuantas.set(t, (enCuantas.get(t) ?? 0) + 1);
  for (const f of fichas) {
    if (f.tipo !== 'pantalla') continue;
    // Los más raros primero, pero a lo sumo 3 por componente: el resumen tiene que
    // representar toda la pantalla (el cobro del POS, no solo la apertura de caja).
    const porArchivo = new Map<string, number>();
    const cortos = [...new Map(f.textos.map((x) => [x.t, x])).values()]
      .filter((x) => x.t.length >= 4 && x.t.length <= 40 && /^[A-ZÁÉÍÓÚÑ¿]/.test(x.t) && !x.t.includes('…'))
      .sort((a, b) => (enCuantas.get(a.t) ?? 0) - (enCuantas.get(b.t) ?? 0))
      .filter((x) => {
        const n = porArchivo.get(x.archivo) ?? 0;
        porArchivo.set(x.archivo, n + 1);
        return n < 3;
      })
      .slice(0, 24)
      .map((x) => x.t);
    const largo = f.textos.map((x) => x.t).find((x) => x.length > 60 && (enCuantas.get(x) ?? 0) <= 2);
    const partes = cortos.join(' · ');
    f.resumen = (largo ? `${partes} — ${largo.slice(0, 160)}` : partes).slice(0, 480);
  }
}

export function construirIndice(raizCodigo: string, refs: Record<string, string>): IndiceManual {
  const src = join(raizCodigo, 'frontend', 'src');
  const fichas: Ficha[] = [];

  // Grupos del menú y etiquetas.
  const grupos = new Map<string, string>();
  const meta = ast(join(src, 'hooks/useMenuAcceso.js'));
  if (meta) {
    traverse(meta, {
      VariableDeclarator(p) {
        if (
          p.node.id.type === 'Identifier' &&
          p.node.id.name === 'MODULO_RAIZ_META' &&
          p.node.init?.type === 'ObjectExpression'
        ) {
          for (const prop of p.node.init.properties) {
            if (prop.type !== 'ObjectProperty') continue;
            const k = prop.key.type === 'Identifier' ? prop.key.name : '';
            const v = objetoPlano(prop.value);
            if (k && typeof v?.label === 'string') grupos.set(k, v.label);
          }
        }
      },
    });
  }
  const menu = [
    ...arrayExportado(join(src, 'utils/dataEstatica.jsx'), 'LinksArray'),
    ...arrayExportado(join(src, 'utils/dataEstatica.jsx'), 'SecondarylinksArray'),
  ];
  const migaDeRuta = new Map<string, string>();
  for (const item of menu) {
    if (typeof item.to !== 'string' || typeof item.label !== 'string') continue;
    const clave = typeof item.moduloRaiz === 'string' ? item.moduloRaiz : typeof item.modulo === 'string' ? item.modulo : '';
    const grupo = grupos.get(clave);
    migaDeRuta.set(item.to, grupo ? `${grupo} › ${item.label}` : item.label);
  }

  // Pantallas por ruta.
  const vistasArchivos = new Set<string>();
  const fuerasDelMenu = new Set<Ficha>();
  for (const r of rutas(join(src, 'routers/routes.jsx'))) {
    // Fuera del sistema: login, pagos y documentos públicos, pantallas de bloqueo.
    if (
      /^\/(login|register|registro|pago|pay|pub|publico|p\/|consulta|recuperar|reset|empresa-bloqueada)/.test(r.ruta) ||
      r.ruta === '*'
    )
      continue;
    // La página de Configuración es un contenedor: cada sección tiene su ficha (más abajo).
    if (r.ruta === '/configuracion') continue;
    const enMenu = migaDeRuta.has(r.ruta);
    const miga = migaDeRuta.get(r.ruta) ?? nombreLegible(r.componente);
    const id = `ruta:${r.ruta}`;
    if (vistasArchivos.has(id)) continue;
    vistasArchivos.add(id);
    const ficha = fichaPantalla(id, miga.split(' › ').at(-1) ?? miga, miga, r.ruta, r.archivo, raizCodigo);
    if (!enMenu) fuerasDelMenu.add(ficha);
    fichas.push(ficha);
  }

  // Secciones de Configuración.
  const conf = join(src, 'pages/ConfiguracionNew.jsx');
  const casos = casosConfiguracion(conf);
  for (const cat of arrayExportado(conf, 'menuCategories').length ? arrayExportado(conf, 'menuCategories') : []) {
    const items = Array.isArray(cat.items) ? (cat.items as Array<Record<string, unknown> | null>) : [];
    for (const it of items) {
      if (!it || typeof it.id !== 'string' || typeof it.label !== 'string') continue;
      const archivo = casos.get(it.id);
      if (!archivo) continue;
      fichas.push(
        fichaPantalla(
          `config:${it.id}`,
          it.label,
          `Configuración › ${String(cat.label)} › ${it.label}`,
          `/configuracion?seccion=${it.id}`,
          archivo,
          raizCodigo,
        ),
      );
    }
  }

  // Desde dónde se llega a cada ruta: literales "/ruta" en navigate(), to=, href= de otras pantallas.
  const porRuta = new Map(fichas.filter((f) => f.ruta).map((f) => [f.ruta as string, f]));
  for (const origen of fichas) {
    for (const archivo of origen.archivos) {
      const arbol = ast(join(raizCodigo, archivo));
      if (!arbol) continue;
      traverse(arbol, {
        StringLiteral(p) {
          const destino = porRuta.get(p.node.value.split('?')[0]);
          if (!destino || destino === origen) return;
          destino.desde = [...new Set([...(destino.desde ?? []), origen.miga])];
        },
      });
    }
  }

  // Una pantalla fuera del menú se nombra por la primera desde la que se abre:
  // "Ventas › Facturación › POS Admin" en vez de "POSAdmin".
  for (const f of fuerasDelMenu) {
    const origen = f.desde?.find((d) => d.includes('›'));
    if (origen) f.miga = `${origen} › ${f.titulo}`;
  }

  resumirPantallas(fichas);

  const backendSrc = join(raizCodigo, 'backend', 'src');
  if (existsSync(backendSrc)) fichas.push(...reglasBackend(backendSrc, raizCodigo));

  return { generado: new Date().toISOString(), refs, fichas };
}

// ── CLI ─────────────────────────────────────────────────────────────────

function arg(nombre: string, defecto?: string): string {
  const i = process.argv.indexOf(`--${nombre}`);
  const v = i >= 0 ? process.argv[i + 1] : defecto;
  if (v === undefined) throw new Error(`Falta --${nombre}`);
  return v;
}

/** Foto del código: `git archive <ref> src` del repo, extraída en destino. */
function foto(repo: string, ref: string, destino: string): string {
  mkdirSync(destino, { recursive: true });
  const tar = execFileSync('git', ['-C', repo, 'archive', ref, 'src'], { maxBuffer: 512 * 1024 * 1024 });
  execFileSync('tar', ['-x', '-C', destino], { input: tar });
  return execFileSync('git', ['-C', repo, 'log', '-1', '--format=%h %s', ref], { encoding: 'utf8' }).trim();
}

function main() {
  const salida = resolve(arg('salida', 'manual'));
  const codigo = join(salida, 'codigo');
  rmSync(codigo, { recursive: true, force: true });
  const refs: Record<string, string> = {};
  refs.frontend = foto(resolve(arg('frontend')), arg('frontend-ref'), join(codigo, 'frontend'));
  refs.backend = foto(resolve(arg('backend')), arg('backend-ref'), join(codigo, 'backend'));
  const indice = construirIndice(codigo, refs);
  writeFileSync(join(salida, 'indice.json'), JSON.stringify(indice));
  const pantallas = indice.fichas.filter((f) => f.tipo === 'pantalla');
  const textos = indice.fichas.reduce((a, f) => a + f.textos.length, 0);
  console.log(
    `Manual: ${pantallas.length} pantallas, ${indice.fichas.length - pantallas.length} grupos de reglas, ${textos} textos.\n` +
      `  frontend ${refs.frontend}\n  backend  ${refs.backend}\n  → ${join(salida, 'indice.json')}`,
  );
}

if (require.main === module) main();
