#!/usr/bin/env node

/**
 * Zen Modules CLI
 *
 * Génère `app/.zen/modules.generated.js` à partir des dépendances `@zen/module-*`
 * détectées dans le `package.json` du projet consommateur. Le fichier généré est
 * ensuite importé par `instrumentation.js` (côté serveur) et par `app/layout.js`
 * (côté client) pour rendre les modules visibles aux deux bundles Next.js.
 *
 * Usage : `npx zen-modules sync`
 *
 * Conçu pour être appelé depuis postinstall + dev/build du projet consommateur.
 * Idempotent : si le contenu généré est identique au fichier existant, aucune
 * écriture n'est effectuée.
 */

import { writeFile, mkdir, readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { step, done, warn, fail } from '@zen/core/shared/logger';
import { findInstalledModuleNames, moduleHasClientEntry } from './discover.server.js';

const OUTPUT_SERVER = 'app/.zen/modules.generated.js';
const OUTPUT_CLIENT = 'app/.zen/modules.client.js';

function safeIdentifier(name, idx) {
  // `@zen/module-posts` → `m0_zen_module_posts`
  const cleaned = name.replace(/[^a-zA-Z0-9]/g, '_');
  return `m${idx}_${cleaned}`;
}

function renderServerManifest(names) {
  const header = '// AUTO-GÉNÉRÉ par `npx zen-modules sync` — ne pas modifier à la main.\n';

  if (names.length === 0) {
    return header + '\nexport const modules = [];\n';
  }

  const imports = names
    .map((name, i) => `import * as ${safeIdentifier(name, i)} from '${name}';`)
    .join('\n');

  const entries = names
    .map((name, i) => `  { name: '${name}', exports: ${safeIdentifier(name, i)} },`)
    .join('\n');

  return [
    header,
    imports,
    '',
    'export const modules = [',
    entries,
    '];',
    '',
    '// Top-level await : déclenche register() de chaque module au moment de l\'import',
    '// côté serveur. instrumentation.js importe ce fichier au boot.',
    'await Promise.all(modules.map(m => m.exports.register?.()));',
    '',
  ].join('\n');
}

function renderClientManifest(clientNames) {
  // Exporte un Component React `ZenModulesClient` que le layout consommateur doit
  // RENDRE dans son tree (`<ZenModulesClient />`). C'est le seul moyen fiable
  // sous Next.js 15+/Turbopack pour garantir que les side-effects top-level
  // (registerPage, registerWidget) s'exécutent côté browser. Un simple
  // `import './.zen/modules.client.js'` dans un Server Component met bien le
  // fichier dans le bundle client mais n'en exécute jamais le code top-level
  // — la transformation 'use client' s'applique aux Components, pas aux
  // side-effect imports orphelins.
  //
  // Importe `@zen/module-X/client` (sous-entrée 'use client') et NON le main
  // entry du module — le main entry tire createTables/registerApiRoutes/etc.
  // qui dépendent de pg/fs/net et ne peuvent pas être bundlés pour le browser.
  const header = [
    '// AUTO-GÉNÉRÉ par `npx zen-modules sync` — ne pas modifier à la main.',
    "'use client';",
    '',
  ].join('\n');

  const body = [
    'export default function ZenModulesClient() {',
    '  return null;',
    '}',
    '',
  ].join('\n');

  if (clientNames.length === 0) return header + '\n' + body;

  const imports = clientNames.map(name => `import '${name}/client';`).join('\n');
  return [header, imports, '', body].join('\n');
}

async function readIfExists(path) {
  try {
    return await readFile(path, 'utf-8');
  } catch {
    return null;
  }
}

async function writeIfChanged(path, contents) {
  const prev = await readIfExists(path);
  if (prev === contents) return false;
  await mkdir(dirname(path), { recursive: true });
  await writeFile(path, contents, 'utf-8');
  return true;
}

async function syncCommand({ cwd = process.cwd() } = {}) {
  const names = await findInstalledModuleNames({ cwd });
  const clientNames = [];
  for (const name of names) {
    if (await moduleHasClientEntry(name, cwd)) clientNames.push(name);
  }

  const serverCount = `(${names.length} module${names.length === 1 ? '' : 's'})`;
  const clientCount = `(${clientNames.length} client entr${clientNames.length === 1 ? 'y' : 'ies'})`;

  const serverPath = resolve(cwd, OUTPUT_SERVER);
  const clientPath = resolve(cwd, OUTPUT_CLIENT);

  const wroteServer = await writeIfChanged(serverPath, renderServerManifest(names));
  const wroteClient = await writeIfChanged(clientPath, renderClientManifest(clientNames));

  if (!wroteServer && !wroteClient) {
    step(`zen-modules: manifests already up to date ${serverCount} ${clientCount}`);
    return;
  }

  if (wroteServer) done(`zen-modules: wrote ${OUTPUT_SERVER} ${serverCount}`);
  if (wroteClient) done(`zen-modules: wrote ${OUTPUT_CLIENT} ${clientCount}`);
  for (const name of names) {
    const hasClient = clientNames.includes(name);
    step(`  → ${name}${hasClient ? '  (+ /client)' : ''}`);
  }
}

function printHelp() {
  console.log(`
Zen Modules CLI

Usage:
  npx zen-modules <command>

Commands:
  sync     Régénère app/.zen/modules.generated.js à partir des @zen/module-*
           déclarés dans le package.json du projet courant.
  help     Affiche cette aide.
`);
}

const [, , command] = process.argv;

try {
  switch (command) {
    case 'sync':
      await syncCommand();
      break;
    case 'help':
    case '--help':
    case '-h':
    case undefined:
      printHelp();
      break;
    default:
      warn(`Unknown command: ${command}`);
      printHelp();
      process.exit(1);
  }
} catch (err) {
  fail(`zen-modules: ${err.message}`);
  process.exit(1);
}
