import { readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, existsSync, rmSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { execSync } from "node:child_process";

const __dirname = dirname(fileURLToPath(import.meta.url));
const FRAMEWORK_ROOT = join(__dirname, "..");
const MODULES_DIR = join(FRAMEWORK_ROOT, "modules");
const MANIFEST_PATH = join(FRAMEWORK_ROOT, "config", "manifest.json");

/**
 * Return manifest data for every module in modules/.
 */
export function listAvailableModules() {
  const entries = readdirSync(MODULES_DIR, { withFileTypes: true });
  const modules = [];
  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const manifestPath = join(MODULES_DIR, entry.name, "manifest.json");
    if (!existsSync(manifestPath)) continue;
    modules.push(JSON.parse(readFileSync(manifestPath, "utf8")));
  }
  return modules;
}

function getEntryPoint(pkgPath) {
  try {
    const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
    const start = pkg.scripts?.start ?? "";
    const match = start.match(/node\s+(.+\.js)/);
    return match ? match[1] : "dist/index.js";
  } catch {
    return "dist/index.js";
  }
}

/**
 * Copy a module to the install path, run npm install (and build if a build
 * script exists), write any module-specific config files, register the module
 * in config/manifest.json, and generate a PM2 ecosystem config.
 *
 * @param {string} moduleName
 * @param {object} systemConfig  - parsed config/system.json
 * @param {object} userConfig    - values returned by the module's setup.js
 *   Special key: `config` (object) — written to <installPath>/data/config.json
 *   All other keys are treated as environment variables in the PM2 config.
 */
export function installModule(moduleName, systemConfig, userConfig = {}) {
  const moduleDir = join(MODULES_DIR, moduleName);
  if (!existsSync(moduleDir)) {
    throw new Error(`Module '${moduleName}' not found in ${MODULES_DIR}`);
  }

  const manifestPath = join(moduleDir, "manifest.json");
  if (!existsSync(manifestPath)) {
    throw new Error(`Module '${moduleName}' has no manifest.json`);
  }
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));

  const installBase = systemConfig.installPath || process.cwd();
  const installPath = join(installBase, "services", moduleName);
  mkdirSync(installPath, { recursive: true });

  // Copy all module files to the install directory
  cpSync(moduleDir, installPath, { recursive: true });

  // npm install
  const pkgPath = join(installPath, "package.json");
  if (existsSync(pkgPath)) {
    console.log(`[modules] Installing npm dependencies for '${moduleName}'...`);
    execSync("npm install", { cwd: installPath, stdio: "inherit" });

    // Build TypeScript if a build script is defined
    const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
    if (pkg.scripts?.build) {
      console.log(`[modules] Building '${moduleName}'...`);
      execSync("npm run build", { cwd: installPath, stdio: "inherit" });
    }
  }

  // Write module-specific config data (e.g. status hosts list)
  const { config: moduleConfig, ...envConfig } = userConfig;
  if (moduleConfig && typeof moduleConfig === "object") {
    const dataDir = join(installPath, "data");
    mkdirSync(dataDir, { recursive: true });
    writeFileSync(join(dataDir, "config.json"), JSON.stringify(moduleConfig, null, 2));
  }

  // Register in system manifest
  const systemManifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
  systemManifest.modules[moduleName] = {
    name: manifest.name,
    version: manifest.version,
    description: manifest.description,
    installPath,
    installedAt: new Date().toISOString(),
    serviceType: manifest.serviceType,
    ports: manifest.requiredPorts ?? [],
  };
  writeFileSync(MANIFEST_PATH, JSON.stringify(systemManifest, null, 2));

  // Generate PM2 ecosystem config
  if (manifest.serviceType === "pm2") {
    const entryPoint = getEntryPoint(pkgPath);
    const portKey = `${moduleName.toUpperCase().replace(/-/g, "_")}_SERVICE_PORT`;
    const port = manifest.requiredPorts?.[0];

    const env = {
      NODE_ENV: "production",
      ...envConfig,
      ...(port !== undefined ? { [portKey]: String(port) } : {}),
    };

    const pm2Config = {
      apps: [{
        name: moduleName,
        script: entryPoint,
        cwd: installPath,
        env,
        watch: false,
        max_memory_restart: "200M",
      }],
    };

    writeFileSync(
      join(installPath, "ecosystem.config.json"),
      JSON.stringify(pm2Config, null, 2)
    );

    console.log(`[modules] PM2 config written to ${installPath}/ecosystem.config.json`);
    console.log(`[modules] Start with: pm2 start ${installPath}/ecosystem.config.json`);
  }

  console.log(`[modules] '${moduleName}' installed to ${installPath}`);
  return { moduleName, installPath, manifest };
}

/**
 * Run a module's optional install-hook.js after the module has been copied,
 * dependencies installed, and the manifest updated. The hook receives
 * (systemConfig, userConfig, installedPath) and may do post-install wiring
 * (e.g. writing config files, registering MCP entries in the agent's .mcp.json).
 *
 * If the hook returns a Promise, this function awaits it.
 *
 * @param {string} moduleName
 * @param {object} systemConfig
 * @param {object} userConfig
 * @param {string} installedPath  - the directory the module was copied to
 */
export async function runInstallHook(moduleName, systemConfig, userConfig, installedPath) {
  const hookPath = join(installedPath, "install-hook.js");
  if (!existsSync(hookPath)) return;
  try {
    const mod = await import(pathToFileURL(hookPath).href);
    const fn = mod.install ?? mod.default;
    if (typeof fn !== "function") return;
    await fn(systemConfig, userConfig, installedPath);
    console.log(`[modules] install-hook executed for '${moduleName}'`);
  } catch (err) {
    console.warn(`[modules] install-hook for '${moduleName}' failed: ${err.message}`);
  }
}

/**
 * Stop the PM2 process for a module, remove it from config/manifest.json,
 * and optionally delete its files from disk.
 *
 * @param {string}  moduleName
 * @param {object}  systemConfig  - parsed config/system.json
 * @param {boolean} removeFiles   - if true, delete the installed files
 */
export function removeModule(moduleName, systemConfig, removeFiles = false) {
  const systemManifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
  const entry = systemManifest.modules[moduleName];

  if (!entry) {
    throw new Error(`Module '${moduleName}' is not registered in the manifest`);
  }

  // Stop and delete PM2 process (ignore errors if not running)
  if (entry.serviceType === "pm2") {
    try {
      execSync(`pm2 stop ${moduleName}`, { stdio: "pipe" });
      execSync(`pm2 delete ${moduleName}`, { stdio: "pipe" });
      console.log(`[modules] PM2 process '${moduleName}' stopped and removed`);
    } catch {
      // Service may not be running — that's fine
    }
  }

  // Deregister from manifest
  delete systemManifest.modules[moduleName];
  writeFileSync(MANIFEST_PATH, JSON.stringify(systemManifest, null, 2));
  console.log(`[modules] '${moduleName}' removed from manifest`);

  // Optionally remove installed files
  if (removeFiles && entry.installPath && existsSync(entry.installPath)) {
    rmSync(entry.installPath, { recursive: true, force: true });
    console.log(`[modules] Removed files from ${entry.installPath}`);
  }

  return { moduleName };
}
