/**
 * `madarx run` — boot the whole stack with one command.
 *
 *   web tier   → spawned child (Next standalone server, or `next start` fallback).
 *                Cron + heartbeat ride along for free: lib/db.js::getDb() auto-starts
 *                them in-process the first time the server opens the DB.
 *   WhatsApp   → spawned + supervised child (scripts/whatsapp-daemon.mjs). Idles
 *                until a number is paired. Opt out with MADARX_DISABLE_WHATSAPP=1.
 *
 * This module imports only node builtins + lib/cli/home.js — it never imports the
 * runtime, so the CLI supervisor process itself never opens the SQLite DB. The
 * web-server child owns the DB and the daemons.
 */

import path from 'node:path';
import fs from 'node:fs';
import { spawn } from 'node:child_process';
import { applyHomeEnv } from './home.js';
import { PKG_ROOT } from './dispatch.js';
import { renderBanner, renderHeroBanner } from './banner.js';
import { readyPanel } from './splash.js';
import { readUpdateCheck } from '../update-check.js';
import { loadProjectEnv } from '../load-env.js';

const HOST = process.env.MADARX_HOST || '127.0.0.1';
const PORT = String(process.env.PORT || process.env.MADARX_PORT || 8787);
const BASE_URL = `http://${HOST}:${PORT}`;

function log(msg) { console.log(`[madarx] ${msg}`); }
function warn(msg) { console.warn(`[madarx] ${msg}`); }

/**
 * The real installed version, read from the package root's package.json. Exported
 * to children via MADARX_VERSION so the bundled web server (where a relative
 * package.json read misses) reports the right version to the update banner.
 */
function pkgVersion() {
  try {
    return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version || null;
  } catch {
    return null;
  }
}

/** Locate how to boot the web tier. Prefer the prebuilt standalone bundle. */
function resolveWebLauncher() {
  const standalone = path.join(PKG_ROOT, '.next', 'standalone', 'server.js');
  if (fs.existsSync(standalone)) {
    return { mode: 'standalone', cmd: process.execPath, args: [standalone] };
  }
  const nextBin = path.join(PKG_ROOT, 'node_modules', 'next', 'dist', 'bin', 'next');
  const built = fs.existsSync(path.join(PKG_ROOT, '.next', 'BUILD_ID'));
  if (fs.existsSync(nextBin) && built) {
    return { mode: 'next-start', cmd: process.execPath, args: [nextBin, 'start', '-p', PORT, '-H', HOST] };
  }
  return null;
}

function spawnWeb(layout) {
  const launcher = resolveWebLauncher();
  if (!launcher) {
    warn('No web build found. Build it first:  npm run build');
    warn(`Looked for ${path.join(PKG_ROOT, '.next', 'standalone', 'server.js')} and a Next start build.`);
    return null;
  }
  log(`web tier: ${launcher.mode} on ${BASE_URL}`);
  const child = spawn(launcher.cmd, launcher.args, {
    cwd: PKG_ROOT,
    stdio: 'inherit',
    env: {
      ...process.env,
      PORT,
      HOSTNAME: HOST, // Next standalone server reads HOSTNAME/PORT
      MADARX_HOME: layout.home,
      // The web bundle's import.meta.url is baked at build time, so lib modules
      // pulled into the bundle compute a wrong package ROOT (the CI builder's
      // path). Pass the real install dir so runtime spawns (hermes job runner,
      // browser harness) can resolve an existing cwd + sibling scripts.
      MADARX_PKG_ROOT: PKG_ROOT,
      ...(pkgVersion() ? { MADARX_VERSION: pkgVersion() } : {}),
    },
  });
  try { fs.writeFileSync(path.join(layout.runDir, 'web.pid'), String(child.pid)); } catch {}
  return child;
}

function spawnWhatsApp(layout) {
  if (process.env.MADARX_DISABLE_WHATSAPP === '1') {
    log('WhatsApp bridge disabled (MADARX_DISABLE_WHATSAPP=1).');
    return null;
  }
  const daemon = path.join(PKG_ROOT, 'scripts', 'whatsapp-daemon.mjs');
  if (!fs.existsSync(daemon)) {
    warn('WhatsApp daemon script not found — skipping.');
    return null;
  }
  log('WhatsApp bridge: starting (idles until a number is paired)…');
  const child = spawn(process.execPath, ['--no-warnings', daemon], {
    cwd: PKG_ROOT,
    stdio: 'inherit',
    // The WhatsApp daemon opens the DB (getDb), which auto-starts the in-process
    // cron/heartbeat poke loop. That loop POSTs the web tier's /api/heartbeat/worker
    // + /api/cron/tick over loopback — it belongs ONLY in the web-server process.
    // If this child wins the machine-wide daemon lock it pokes the web tier from a
    // DIFFERENT process, whose inproc token never matches → 401 on every tick.
    // Disable the in-proc daemon here so only the web child owns the poke loop.
    env: { ...process.env, MADARX_HOME: layout.home, MADARX_DISABLE_INPROC_DAEMON: '1' },
  });
  try { fs.writeFileSync(path.join(layout.runDir, 'whatsapp.pid'), String(child.pid)); } catch {}
  return child;
}

async function waitForHealth(timeoutMs = 45000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try {
      const res = await fetch(`${BASE_URL}/api/state`, { method: 'GET' });
      if (res.ok || res.status === 401 || res.status === 403) return true; // up (auth gate counts)
    } catch { /* not up yet */ }
    await new Promise(r => setTimeout(r, 500));
  }
  return false;
}

function openBrowser(url, flags = {}) {
  if (flags['no-open'] || process.env.MADARX_NO_OPEN === '1') return;
  const opener = process.platform === 'darwin' ? 'open'
    : process.platform === 'win32' ? 'cmd'
    : 'xdg-open';
  const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
  try {
    const child = spawn(opener, args, { stdio: 'ignore', detached: true });
    child.on('error', () => {});
    child.unref();
  } catch { /* best-effort */ }
}

/**
 * Boot and supervise. Resolves with an exit code. On a clean boot it stays
 * resident until SIGINT/SIGTERM or until the web child exits.
 */
export async function run({ flags = {} } = {}) {
  const layout = applyHomeEnv();

  // Load $MADARX_HOME/.env.local (MADARX_ENV_FILE, set by applyHomeEnv) into the
  // supervisor's env BEFORE spawning children. The Next standalone web server
  // only auto-loads .env.local from the package dir, not from MADARX_HOME, so
  // without this the secrets-vault master key (MADARX_MASTER_KEY) and other
  // vault keys never reach the web tier — and any vault write fails with
  // "MADARX_MASTER_KEY is not set but the secrets vault has stored credentials".
  // Children inherit via { ...process.env } in spawnWeb / spawnWhatsApp.
  loadProjectEnv();

  // Branded boot banner (suppress with --quiet / MADARX_NO_BANNER=1).
  if (!flags.quiet && process.env.MADARX_NO_BANNER !== '1') {
    let ledger = null;
    try { ledger = readUpdateCheck(layout.updateCheckFile); } catch { /* best-effort */ }
    // Catalog counts: best-effort dynamic import so a count never blocks or
    // breaks boot. These modules build static catalogs at eval time and do NOT
    // open the database, so the supervisor stays DB-free.
    let toolCats = [];
    let toolCount;
    let skillGroups = [];
    let skillCount;
    try {
      const [{ MARKETPLACE_TOOL_COUNT, listToolCategories }, seeds] = await Promise.all([
        import('../tools/registry.js'),
        import('../marketplace-seeds.js'),
      ]);
      toolCount = MARKETPLACE_TOOL_COUNT;
      toolCats = listToolCategories();
      // Showcase the business-default skill pack (research, comms, documents,
      // data, web) grouped by category — what most teams need out of the box.
      skillGroups = seeds.listDefaultSkillGroups ? seeds.listDefaultSkillGroups() : [];
      skillCount = (seeds.DEFAULT_SKILL_SLUGS || []).length;
    } catch { /* catalog is optional — banner still renders without it */ }
    const version = pkgVersion() || '0.0.0';
    const hero = process.env.MADARX_BANNER_LEGACY !== '1';
    const render = hero ? renderHeroBanner : renderBanner;
    console.log(render({
      version,
      home: layout.home,
      url: BASE_URL,
      engine: process.env.MADARX_DEFAULT_ENGINE || 'claude-code',
      commit: process.env.MADARX_COMMIT || undefined,
      latest: ledger?.latest || null,
      behind: Boolean(ledger?.latest && ledger.latest !== version && ledger?.behind),
      toolCats,
      toolCount,
      skillGroups,
      skillCount,
      // legacy renderBanner still understands these:
      tools: toolCount,
    }));
  } else {
    log(`data home: ${layout.home}`);
  }

  const web = spawnWeb(layout);
  if (!web) return 1;

  let stopping = false;
  const children = [web];

  const wa = spawnWhatsApp(layout);
  if (wa) children.push(wa);

  const shutdown = (signal) => {
    if (stopping) return;
    stopping = true;
    log(`shutting down (${signal})…`);
    for (const c of children) { try { c.kill('SIGTERM'); } catch {} }
    // Hard-exit backstop if a child ignores SIGTERM.
    setTimeout(() => process.exit(0), 5000).unref();
  };
  process.on('SIGINT', () => shutdown('SIGINT'));
  process.on('SIGTERM', () => shutdown('SIGTERM'));

  // The web child is the lifeline. If it dies, tear everything down.
  const exitCode = await new Promise((resolve) => {
    web.on('exit', (code, sig) => {
      if (!stopping) warn(`web tier exited (code=${code} signal=${sig}) — stopping.`);
      for (const c of children) { if (c !== web) { try { c.kill('SIGTERM'); } catch {} } }
      resolve(code == null ? 0 : code);
    });
    web.on('error', (err) => { warn(`web tier failed to start: ${err.message}`); resolve(1); });

    // Health gate + browser open run alongside; never block shutdown on them.
    waitForHealth().then((ok) => {
      if (stopping) return;
      if (ok) {
        if (!flags.quiet && process.env.MADARX_NO_BANNER !== '1') {
          console.log('\n' + readyPanel({ version: pkgVersion() || undefined, url: BASE_URL }) + '\n');
        } else {
          log(`ready → ${BASE_URL}`);
        }
        openBrowser(BASE_URL, flags);
      }
      else warn(`web tier did not answer on ${BASE_URL} within the timeout — it may still be starting.`);
    });
  });

  // Clean up pidfiles.
  for (const f of ['web.pid', 'whatsapp.pid']) {
    try { fs.rmSync(path.join(layout.runDir, f), { force: true }); } catch {}
  }
  return exitCode;
}
