import { readFile } from 'node:fs/promises';
import { readFileSync } from 'node:fs';
import { resolve, join, dirname } from 'node:path';
import { pathToFileURL } from 'node:url';
import { info, warn } from '@zen/core/shared/logger';
import { registerModule } from './registry.js';

/**
 * Sources de modules `@zen/module-*` activés dans le projet consommateur.
 *
 * Le projet consommateur fournit un manifeste statique (généré par
 * `npx zen-modules sync`) et le passe à `initializeZen({ modules })`. Le
 * manifeste a la forme :
 *
 *   import * as posts from '@zen/module-posts';
 *   export const modules = [{ name: '@zen/module-posts', exports: posts }];
 *   await Promise.all(modules.map(m => m.exports.register?.()));
 *
 * Le `import *` rend l'arbre d'imports du module visible aux deux bundles
 * Next.js (server + client) ; Turbopack/Webpack le bundlent comme n'importe
 * quel autre fichier source. C'est ce qui permet au module de référencer du
 * JSX, `next/headers`, `next/navigation`, etc. — chaque côté reçoit la bonne
 * condition.
 *
 * Cette fonction ne fait QUE peupler le registre interne du core (pour que
 * `getRegisteredModules()` retourne les bons objets côté serveur). Le top-level
 * await dans le manifeste a déjà appelé `register()` au moment de son import.
 */
export function registerModules(modules) {
  if (!Array.isArray(modules)) return;

  for (const entry of modules) {
    const ex = entry?.exports;
    const name = entry?.name ?? ex?.manifest?.name ?? '<unknown>';

    if (!ex?.manifest || typeof ex.register !== 'function') {
      warn(`zen-modules: "${name}" missing manifest/register — skipping`);
      continue;
    }

    registerModule({
      manifest: ex.manifest,
      register: ex.register,
      createTables: ex.createTables,
      dropTables: ex.dropTables,
    });
    info(`zen-modules: registered ${ex.manifest.name}@${ex.manifest.version ?? '?'}`);
  }
}

// ---------------------------------------------------------------------------
// Helpers de scan (utilisés par le CLI `zen-modules sync` et l'env validation).
// ---------------------------------------------------------------------------

const NAME_PREFIX = /^(@zen\/module-|zen-module-)/;

export function isCandidateName(name) {
  return NAME_PREFIX.test(name);
}

export async function readJson(path) {
  try {
    return JSON.parse(await readFile(path, 'utf-8'));
  } catch {
    return null;
  }
}

/**
 * Résout le chemin du `package.json` d'un module installé. Ne dépend PAS de
 * `require.resolve(name)` (qui peut échouer si la sous-entrée `./package.json`
 * n'est pas exposée par `exports`) ni de `import.meta.resolve` (variable selon
 * la version Node). On remonte simplement depuis `projectCwd` en cherchant
 * `node_modules/<name>/package.json` à chaque niveau — c'est le mécanisme de
 * résolution npm standard.
 */
function resolveModulePackageJson(name, projectCwd) {
  let dir = projectCwd;
  while (true) {
    const candidate = join(dir, 'node_modules', name, 'package.json');
    try {
      const pkg = JSON.parse(readFileSync(candidate, 'utf-8'));
      return { path: candidate, pkg };
    } catch {
      // pas trouvé à ce niveau, remonter
    }
    const parent = dirname(dir);
    if (parent === dir) return null;
    dir = parent;
  }
}

export async function isThirdPartyModule(name, projectCwd) {
  const found = resolveModulePackageJson(name, projectCwd);
  return Array.isArray(found?.pkg?.keywords) && found.pkg.keywords.includes('zen-module');
}

/**
 * Vérifie si un module installé expose une sous-entrée `./client` dans son
 * `package.json#exports`. Utilisé par le CLI `zen-modules sync` pour décider
 * si le manifeste client doit l'importer. Les modules sans partie cliente
 * (modules purement back-end / API / DB) ne sont pas inclus dans le manifeste
 * client — ça évite d'embarquer leur graphe d'imports serveur dans le bundle
 * browser.
 */
export async function moduleHasClientEntry(name, projectCwd) {
  const found = resolveModulePackageJson(name, projectCwd);
  return Boolean(found?.pkg?.exports?.['./client']);
}

/**
 * Scanne `package.json` du projet consommateur et retourne la liste des noms
 * de packages `@zen/module-*` (ou compatibles). N'effectue AUCUN import — le
 * CLI `zen-modules sync` consomme cette liste pour générer le manifeste
 * statique.
 */
export async function findInstalledModuleNames({ cwd = process.cwd() } = {}) {
  const pkg = await readJson(resolve(cwd, 'package.json'));
  if (!pkg) return [];

  const allDeps = {
    ...(pkg.dependencies ?? {}),
    ...(pkg.devDependencies ?? {}),
  };

  const out = [];
  for (const name of Object.keys(allDeps)) {
    if (isCandidateName(name) || (await isThirdPartyModule(name, cwd))) {
      out.push(name);
    }
  }
  return out.sort();
}

/**
 * Résout l'URL ESM du main entry d'un module à partir de son `package.json`.
 * On lit `exports['.'].import` puis `main`, sinon `./index.js` par défaut.
 * Pas de `require.resolve` : un module `@zen/module-*` peut déclarer uniquement
 * la condition `"import"` (ESM-only) — la résolution CJS échouerait alors avec
 * "No exports main defined". Comme on a déjà localisé le `package.json` via
 * `resolveModulePackageJson`, on construit l'URL nous-mêmes.
 */
function resolveModuleEntryUrl(found) {
  const pkgDir = dirname(found.path);
  const main = found.pkg?.exports?.['.']?.import ?? found.pkg?.main ?? './index.js';
  return pathToFileURL(resolve(pkgDir, main)).href;
}

/**
 * Variante "Node-only" du chargement de modules — utilisée par le CLI
 * `zen-db init` qui ne passe jamais par un bundler. Charge dynamiquement
 * chaque module depuis `node_modules/` du projet, pour que le CLI ait accès
 * à `manifest`, `createTables`, `dropTables`. Ne déclenche PAS `register()`
 * (la chaîne register-server tirerait des imports Next.js incompatibles avec
 * le contexte CLI).
 *
 * À ne PAS utiliser depuis le runtime Next.js — utiliser le manifeste statique
 * via `initializeZen({ modules })`.
 */
export async function loadModulesForCli({ cwd = process.cwd() } = {}) {
  const names = await findInstalledModuleNames({ cwd });

  for (const name of names) {
    const found = resolveModulePackageJson(name, cwd);
    if (!found) {
      warn(`zen-modules: cannot find package "${name}" in node_modules`);
      continue;
    }

    let mod;
    try {
      mod = await import(resolveModuleEntryUrl(found));
    } catch (err) {
      warn(`zen-modules: failed to import "${name}" — ${err.message}`);
      continue;
    }

    if (!mod.manifest || typeof mod.register !== 'function') {
      warn(`zen-modules: "${name}" missing manifest/register — skipping`);
      continue;
    }

    registerModule({
      manifest: mod.manifest,
      register: mod.register,
      createTables: mod.createTables,
      dropTables: mod.dropTables,
    });
    info(`zen-modules: loaded ${mod.manifest.name}@${mod.manifest.version ?? '?'}`);
  }
}

/**
 * Valide les variables d'environnement requises par chaque module.
 * Ne lance pas — log un warning pour chaque variable absente.
 */
export function validateModuleEnvVars(modules) {
  for (const mod of modules) {
    const envVars = mod.manifest?.envVars ?? [];
    for (const v of envVars) {
      if (v.required && !process.env[v.key]) {
        warn(`zen-modules: ${mod.manifest.name} requires env var "${v.key}" — ${v.description ?? ''}`);
      }
    }
  }
}
