import { checkPrereqs, installPrereq } from "./prereqs.js";
import { rl, askText, askYesNo, askChoice, askMultiSelect } from "./prompts.js";
import { installModule, runInstallHook } from "./modules.js";
import { processTemplate } from "./templates.js";
import { generatePm2EcosystemConfig, setupPm2Startup, runHealthChecks, printSummary } from "./post-install.js";
import { execSync } from "node:child_process";
import { writeFileSync, mkdirSync, readdirSync, existsSync, readFileSync, statSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { homedir } from "node:os";
import { randomBytes } from "node:crypto";

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

const BANNER = `
╔══════════════════════════════════════════════════════════════╗
║         T E L E T R A A N   F R A M E W O R K              ║
║                    Installer  v1.0                          ║
╚══════════════════════════════════════════════════════════════╝
`;

function run(cmd, opts = {}) {
  try {
    return execSync(cmd, { stdio: "pipe", ...opts }).toString().trim();
  } catch {
    return null;
  }
}

function getModules() {
  const result = [];
  let entries;
  try {
    entries = readdirSync(MODULES_DIR, { withFileTypes: true });
  } catch {
    return result;
  }
  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const manifestPath = join(MODULES_DIR, entry.name, "manifest.json");
    if (!existsSync(manifestPath)) continue;
    try {
      const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
      result.push({ id: entry.name, ...manifest });
    } catch {
      // Skip unparseable manifests
    }
  }
  return result;
}

// Files that should be written with a leading dot (e.g. env → .env, mcp.json → .mcp.json).
const DOT_PREFIX_FILES = new Set(["env", "mcp.json"]);

// Process a template directory, stripping ".template" from output filenames.
function processTemplateDir(srcDir, destDir, vars) {
  mkdirSync(destDir, { recursive: true });
  const entries = readdirSync(srcDir, { withFileTypes: true });
  for (const entry of entries) {
    const src = join(srcDir, entry.name);
    let destName = entry.name.endsWith(".template")
      ? entry.name.slice(0, -".template".length)
      : entry.name;
    if (DOT_PREFIX_FILES.has(destName)) destName = `.${destName}`;
    const dest = join(destDir, destName);
    if (entry.isDirectory()) {
      processTemplateDir(src, dest, vars);
    } else {
      processTemplate(src, dest, vars);
    }
  }
}

// ── Config file mode ─────────────────────────────────────────────────────────
const args = process.argv.slice(2);
let config = null;
const configIdx = args.indexOf("--config");
if (configIdx !== -1 && args[configIdx + 1]) {
  const configPath = args[configIdx + 1];
  try {
    config = JSON.parse(readFileSync(configPath, "utf8"));
    console.log(`Using config file: ${configPath}`);
  } catch (err) {
    console.error(`Failed to read config file ${configPath}: ${err.message}`);
    process.exit(1);
  }
}

async function main() {
  // ── Step 1: Welcome ──────────────────────────────────────────────────────────
  console.log(BANNER);
  console.log("Welcome! This installer sets up your Teletraan AI home system.\n");
  const systemName = await askText("System name", "Teletraan", config?.systemName);
  const systemNameLower = systemName.toLowerCase().replace(/\s+/g, "-");

  // ── Step 2: Prerequisites ────────────────────────────────────────────────────
  console.log("\nChecking prerequisites...\n");
  const prereqs = await checkPrereqs();
  for (const p of prereqs) {
    const status = p.installed
      ? `✓  ${p.version ?? ""}`.trimEnd()
      : "✗  MISSING";
    console.log(`  ${p.name.padEnd(22)} ${status}`);
  }

  const missing = prereqs.filter(p => !p.installed);
  if (missing.length > 0) {
    console.log(`\n${missing.length} prerequisite(s) not met.`);
    for (const p of missing) {
      if (!p.installCommand) {
        console.log(`  ${p.name}: must be installed manually.`);
        continue;
      }
      const doInstall = await askYesNo(`  Install ${p.name}?`, true);
      if (doInstall) await installPrereq(p.name);
    }
  }

  // ── Step 3: Install path ─────────────────────────────────────────────────────
  const defaultPath = join(homedir(), systemNameLower);
  const installPath = await askText("\nInstall path", defaultPath, config?.installPath);

  // ── Step 5: Google OAuth ─────────────────────────────────────────────────────
  console.log("\nGoogle OAuth (used by the Calendar module)");
  const oauthConfigValue = config?.oauth !== undefined ? !config.oauth.skip : undefined;
  const setupOAuth = await askYesNo("Configure Google OAuth now?", false, oauthConfigValue);
  let oauthClientId = "";
  let oauthClientSecret = "";
  if (setupOAuth) {
    oauthClientId = await askText("  Client ID", "", config?.oauth?.clientId);
    oauthClientSecret = await askText("  Client Secret", "", config?.oauth?.clientSecret);
  }

  // ── Step 6: Proxy config ─────────────────────────────────────────────────────
  console.log("\nProxy Configuration");
  const proxyChoices = [
    "Local nginx (configure manually later)",
    "External proxy — provide host now",
    "Skip proxy setup",
  ];
  const proxyTypeMap = { local: proxyChoices[0], external: proxyChoices[1], skip: proxyChoices[2] };
  const proxyConfigValue = config?.proxy?.type !== undefined ? proxyTypeMap[config.proxy.type] : undefined;
  const proxyChoice = await askChoice("Proxy setup:", proxyChoices, proxyConfigValue);
  let proxyHost = "";
  let charonPort = 8501;
  let dnsPort = 8085;
  if (proxyChoice.startsWith("External")) {
    proxyHost = await askText("  Proxy host IP", "", config?.proxy?.host);
    charonPort = parseInt(await askText("  Charon port", "8501"), 10);
    dnsPort = parseInt(await askText("  DNS port", "8085"), 10);
  }

  // ── Step 7: Module selection ─────────────────────────────────────────────────
  console.log("\nModule Selection");
  const availableModules = getModules();
  let selectedModules = [];
  if (availableModules.length === 0) {
    console.log("No modules found in modules/.");
  } else if (config?.modules) {
    selectedModules = availableModules.filter(m => config.modules.includes(m.id));
    console.log(`  Modules (from config): ${selectedModules.map(m => m.name).join(", ") || "(none)"}`);
  } else {
    const labels = availableModules.map(m =>
      m.description ? `${m.name} — ${m.description}` : m.name
    );
    const selectedLabels = await askMultiSelect("Select modules to install:", labels);
    selectedModules = availableModules.filter((_, i) => selectedLabels.includes(labels[i]));
  }

  // ── Assemble system config ───────────────────────────────────────────────────
  const conductorSecret = randomBytes(32).toString("hex");
  const systemConfig = {
    name: systemName,
    installPath,
    host: run("ipconfig getifaddr en0") ?? "127.0.0.1",
    domain: "localhost",
    ports: { conductor: 7100, main: 3006 },
    auth: { adminEmail: "", oauthClientId, oauthClientSecret },
    proxy: { host: proxyHost, charonPort, dnsPort },
    anthropic: { model: "claude-sonnet-4-6", maxTokens: 8192 },
    claudeBinaryPath: process.env.CLAUDE_BINARY_PATH || "",
    installedAt: new Date().toISOString(),
    version: "1.0.0",
  };

  // ── Step 8: Module-specific setup ────────────────────────────────────────────
  const moduleUserConfigs = {};
  for (const mod of selectedModules) {
    console.log(`\n── ${mod.name} Setup ──`);
    const setupPath = join(FRAMEWORK_ROOT, "modules", mod.id, "setup.js");
    if (config !== null) {
      // Non-interactive: use provided config values or empty defaults — never prompt
      moduleUserConfigs[mod.id] = config.moduleConfigs?.[mod.id] ?? {};
      console.log(`  Using config values for ${mod.name}.`);
    } else if (existsSync(setupPath)) {
      try {
        const { setup } = await import(pathToFileURL(setupPath).href);
        if (typeof setup === "function") {
          moduleUserConfigs[mod.id] = await setup(systemConfig, rl);
        }
      } catch (err) {
        console.log(`  Setup failed for ${mod.name}: ${err.message}`);
      }
    } else {
      console.log("  (No additional setup required.)");
    }
  }

  // ── Step 9: Generate config files from templates ──────────────────────────────
  console.log("\nGenerating configuration files...");
  mkdirSync(join(installPath, "config"), { recursive: true });

  writeFileSync(
    join(installPath, "config", "system.json"),
    JSON.stringify(systemConfig, null, 2)
  );
  console.log(`  system.json → ${installPath}/config/system.json`);

  // Conductor agent templates
  const conductorSrc = join(FRAMEWORK_ROOT, "core", "agents", "conductor");
  if (existsSync(conductorSrc)) {
    const conductorDest = join(installPath, "agents", systemNameLower);
    const templateVars = {
      SYSTEM_NAME: systemName,
      SYSTEM_NAME_LOWER: systemNameLower,
      INSTALL_PATH: installPath,
      DOMAIN: systemConfig.domain,
      HOST_IP: systemConfig.host,
      PROXY_IP: proxyHost || "127.0.0.1",
      CONDUCTOR_SECRET: conductorSecret,
      CONDUCTOR_PORT: String(systemConfig.ports.conductor),
      CLAUDE_BINARY_PATH: systemConfig.claudeBinaryPath,
    };
    processTemplateDir(conductorSrc, conductorDest, templateVars);
    console.log(`  Conductor agent → ${conductorDest}`);

    // Make start.sh executable
    const startSh = join(conductorDest, "start.sh");
    if (existsSync(startSh)) run(`chmod +x "${startSh}"`);

    // hooks/
    const hooksDir = join(conductorDest, "hooks");
    if (existsSync(hooksDir)) {
      for (const f of readdirSync(hooksDir)) {
        if (f.endsWith(".sh")) run(`chmod +x "${join(hooksDir, f)}"`);
      }
    }

    // Install channel server. channel-server depends on the sibling channel-lib
    // via `file:../channel-lib`, so copy channel-lib to <installPath>/tools/
    // before the channel-server's `npm install` runs.
    const channelServerSrc = join(FRAMEWORK_ROOT, "core", "tools", "channel-server");
    if (existsSync(channelServerSrc)) {
      const toolsDir = join(installPath, "tools");
      mkdirSync(toolsDir, { recursive: true });

      const channelLibSrc = join(FRAMEWORK_ROOT, "core", "tools", "channel-lib");
      if (existsSync(channelLibSrc)) {
        const channelLibDest = join(toolsDir, "channel-lib");
        run(`cp -r "${channelLibSrc}" "${channelLibDest}"`);
        console.log(`  Channel lib   → ${channelLibDest}`);
      }

      const channelServerDest = join(toolsDir, `${systemNameLower}-channel`);
      run(`cp -r "${channelServerSrc}" "${channelServerDest}"`);
      console.log(`  Channel server → ${channelServerDest}`);
      console.log("  Installing channel server dependencies...");
      try {
        execSync("npm install", { cwd: channelServerDest, stdio: "inherit" });
        console.log("  Channel server ready.");
      } catch (err) {
        console.warn(`  Warning: channel server npm install failed: ${err.message}`);
      }
    }

    // Generate conductor PM2 ecosystem config and start service
    const conductorServiceName = `${systemNameLower}-agent`;
    const conductorEco = join(conductorDest, "ecosystem.config.cjs");
    writeFileSync(conductorEco, `module.exports = {
  apps: [{
    name: "${conductorServiceName}",
    script: "${startSh}",
    interpreter: "bash",
    cwd: "${conductorDest}",
    restart_delay: 5000,
    max_restarts: 50,
  }]
};
`);
    try {
      execSync(`pm2 start "${conductorEco}"`, { stdio: "inherit" });
      run("pm2 save");
      console.log(`  Conductor PM2 service: ${conductorServiceName}`);
    } catch (err) {
      console.warn(`  Warning: could not start conductor agent: ${err.message}`);
    }
    console.log(`  Note: Run 'claude' to authenticate Claude Code for the agent to come online.`);
  }

  // agents.json template
  const agentsJsonTpl = join(FRAMEWORK_ROOT, "core", "agents", "agents.json.template");
  if (existsSync(agentsJsonTpl)) {
    processTemplate(agentsJsonTpl, join(installPath, "agents", "agents.json"), {
      SYSTEM_NAME: systemName,
      SYSTEM_NAME_LOWER: systemNameLower,
      INSTALL_PATH: installPath,
      CONDUCTOR_PORT: String(systemConfig.ports.conductor),
    });
    console.log(`  agents.json → ${installPath}/agents/agents.json`);
  }

  // ── Step 10: Install modules ──────────────────────────────────────────────────
  const installResults = [];
  if (selectedModules.length > 0) {
    console.log("\nInstalling modules...");
    for (const mod of selectedModules) {
      try {
        const userConfig = moduleUserConfigs[mod.id] ?? {};
        const result = installModule(mod.id, systemConfig, userConfig);
        installResults.push(result);
        // Modules that need post-copy wiring (e.g. registering MCP entries in
        // the agent's .mcp.json, writing access.json) can ship an
        // install-hook.js alongside their other files. It runs once, here.
        await runInstallHook(mod.id, systemConfig, userConfig, result.installPath);
        console.log(`  ✓ ${mod.name}`);
      } catch (err) {
        console.error(`  ✗ ${mod.name}: ${err.message}`);
      }
    }
  }

  // ── Step 10.5: Install web dashboard ─────────────────────────────────────────
  console.log("\nInstalling web dashboard...");
  const dashboardSrc = join(FRAMEWORK_ROOT, "core", "main");
  const dashboardDest = join(installPath, "dashboard");

  if (existsSync(dashboardSrc)) {
    // Use rsync so we can skip node_modules + .next + .turbo. cp -r would
    // bring those over and the binary symlinks inside dashboard/node_modules/.bin/
    // get their relative paths broken under the new location, making
    // `npm install` (and `npm run build`) fail with "Cannot find module
    // '../server/require-hook'" or similar.
    run(`rsync -a --delete --exclude=node_modules --exclude=.next --exclude=.turbo "${dashboardSrc}/" "${dashboardDest}/"`);
    console.log(`  Copied dashboard → ${dashboardDest}`);

    console.log("  Running npm install (this may take a moment)...");
    try {
      execSync("npm install", { cwd: dashboardDest, stdio: "inherit" });
    } catch (err) {
      console.error(`  Warning: npm install failed: ${err.message}`);
    }

    // Generate .env.local with actual config values
    const channelUrl = `http://127.0.0.1:${systemConfig.ports.conductor}`;
    const envLocal = [
      "# Generated by Teletraan installer",
      `ONE_CHANNEL_URL=${channelUrl}`,
      `ONE_CHANNEL_SECRET=${conductorSecret}`,
      "CALENDAR_SERVICE_URL=http://127.0.0.1:7200",
      "IMESSAGE_BRIDGE_URL=http://127.0.0.1:7300",
      "MUSIC_SERVICE_URL=http://127.0.0.1:7400",
      "STATUS_SERVICE_URL=http://127.0.0.1:7500",
      "BACKUP_SERVICE_URL=http://127.0.0.1:7600",
      `AGENTS_DIR=${installPath}/agents`,
      `SKILLS_DIR=${installPath}/skills`,
      `APPS_DIR=${installPath}/apps`,
      `TOOLS_MENU_DIR=${installPath}/tools-menu`,
      `NEXT_PUBLIC_SYSTEM_NAME=${systemName}`,
      `NEXT_PUBLIC_PRIMARY_AGENT=${systemNameLower}`,
    ].join("\n") + "\n";
    writeFileSync(join(dashboardDest, ".env.local"), envLocal);
    console.log("  Generated .env.local");

    console.log("  Building dashboard (npm run build)...");
    try {
      execSync("npm run build", { cwd: dashboardDest, stdio: "inherit" });
      console.log("  Dashboard build complete.");
    } catch (err) {
      console.error(`  Warning: Dashboard build failed: ${err.message}`);
    }

    const dashboardPort = systemConfig.ports.main;
    const dashboardServiceName = `${systemNameLower}-dashboard`;
    const dashboardEcoPath = join(dashboardDest, "ecosystem.config.cjs");
    writeFileSync(dashboardEcoPath, `module.exports = {
  apps: [{
    name: "${dashboardServiceName}",
    script: "npm",
    args: "run start",
    cwd: "${dashboardDest}",
    env: {
      PORT: "${dashboardPort}",
      NODE_ENV: "production",
    }
  }]
};
`);
    try {
      execSync(`pm2 start "${dashboardEcoPath}"`, { stdio: "inherit" });
      run("pm2 save");
      console.log(`  ✓ Dashboard running on port ${dashboardPort} (${dashboardServiceName})`);
    } catch (err) {
      console.error(`  Warning: Could not start dashboard: ${err.message}`);
    }
  } else {
    console.log("  Warning: Dashboard source not found at core/main, skipping.");
  }

  // ── Step 11: PM2 services ─────────────────────────────────────────────────────
  if (installResults.length > 0) {
    console.log("\nStarting PM2 services...");
    let anyStarted = false;
    for (const result of installResults) {
      const ecoCfg = join(result.installPath, "ecosystem.config.json");
      if (!existsSync(ecoCfg)) continue;
      try {
        execSync(`pm2 start "${ecoCfg}"`, { stdio: "inherit" });
        anyStarted = true;
        console.log(`  Started: ${result.moduleName}`);
      } catch (err) {
        console.error(`  Failed to start ${result.moduleName}: ${err.message}`);
      }
    }
    if (anyStarted) run("pm2 save");

    // Combined ecosystem config
    const manifest = JSON.parse(readFileSync(join(FRAMEWORK_ROOT, "config", "manifest.json"), "utf8"));
    generatePm2EcosystemConfig(systemConfig, manifest.modules ?? {});
  }

  // Configure PM2 startup — always, since dashboard and conductor are always installed
  setupPm2Startup();

  // ── Step 12: Success ──────────────────────────────────────────────────────────
  rl.close();

  const modList = installResults.length > 0
    ? installResults.map(r => `    • ${r.moduleName}`).join("\n")
    : "    (none selected)";

  const authNote = config
    ? ""
    : `
  Dashboard is live. When ready, run 'claude' to authenticate,
  then restart the agent with: pm2 restart ${systemNameLower}
`;

  console.log(`
╔══════════════════════════════════════════════════════════════╗
║               Installation Complete!                        ║
╚══════════════════════════════════════════════════════════════╝

  System:       ${systemName}
  Install path: ${installPath}
  Config:       ${installPath}/config/system.json

Installed modules:
${modList}
${authNote}
Next steps:
  1. Review config:    cat ${installPath}/config/system.json
  2. Service status:   pm2 status
  3. Service logs:     pm2 logs
`);
}

main().catch(err => {
  console.error("\nInstallation failed:", err.message);
  rl.close();
  process.exit(1);
});
