#!/usr/bin/env node
// cli/index.js — clauth entry point

import { Command } from "commander";
import chalk from "chalk";
import ora from "ora";
import inquirer from "inquirer";
import Conf from "conf";
import { getConfOptions } from "./conf-path.js";
import { getMachineHash, deriveToken, deriveSeedHash } from "./fingerprint.js";
import * as api from "./api.js";
import { writeCredentialWithRecovery } from "./recovery.js";
import os from "os";
import fs from "fs";
import path from "path";

const config = new Conf(getConfOptions());
const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;

function shellSingleQuote(value) {
  return `'${String(value ?? "").replace(/'/g, "''")}'`;
}

function enrollmentScriptName(label) {
  const slug = String(label || "new-computer")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 40) || "new-computer";
  return `clauth-enroll-${slug}.ps1`;
}

function writeEnrollmentScript({ supabaseUrl, anonKey, enrollmentCode, label }) {
  const appDir = process.platform === "win32"
    ? path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "clauth")
    : path.join(os.homedir(), ".config", "clauth");
  fs.mkdirSync(appDir, { recursive: true });
  const scriptPath = path.join(appDir, enrollmentScriptName(label));
  const script = [
    "$ErrorActionPreference = 'Stop'",
    "$label = $env:COMPUTERNAME",
    "if (-not $label) { $label = [System.Net.Dns]::GetHostName() }",
    "Write-Host 'Installing clauth...'",
    "npm install -g @lifeaitools/clauth@latest",
    "Write-Host 'Enrolling this computer with clauth...'",
    [
      "clauth setup",
      `--supabase-url ${shellSingleQuote(supabaseUrl)}`,
      `--anon-key ${shellSingleQuote(anonKey)}`,
      `--enrollment-code ${shellSingleQuote(enrollmentCode)}`,
      "--label \"$label\"",
    ].join(" "),
    "Write-Host 'Installing clauth startup service...'",
    "clauth serve install",
    "Write-Host 'clauth enrollment complete.'",
    "$self = $PSCommandPath",
    "Start-Process powershell.exe -WindowStyle Hidden -ArgumentList @('-NoProfile','-Command',\"Start-Sleep -Seconds 2; Remove-Item -LiteralPath '$self' -Force -ErrorAction SilentlyContinue\")",
  ].join("\r\n");
  fs.writeFileSync(scriptPath, `${script}\r\n`, "utf8");
  return scriptPath;
}

// ============================================================
// Password prompt helper
// ============================================================
async function promptPassword(message = "clauth password") {
  const { pw } = await inquirer.prompt([{
    type: "password",
    name: "pw",
    message,
    mask: "*",
    validate: v => v.length >= 8 || "Password must be at least 8 characters"
  }]);
  return pw;
}

// ============================================================
// Auth helper — get pw + derive token
// ============================================================
async function getAuth(pw) {
  const password = pw || await promptPassword();
  const machineHash = getMachineHash();
  const { token, timestamp } = deriveToken(password, machineHash);
  return { password, machineHash, token, timestamp };
}

const ADDRESS_KEY_TYPES = new Set(["connstring", "fileserver", "oauth"]);
const ADDRESS_FIELDS = new Set(["url", "uri", "host", "hostname", "server", "address", "base_url", "endpoint", "path", "root"]);

function normalizeSearchText(value) {
  return String(value || "").toLowerCase();
}

function redactUrlish(value) {
  const text = String(value || "").trim();
  if (!text) return "";
  try {
    const url = new URL(text);
    if (url.username) url.username = "***";
    if (url.password) url.password = "***";
    return url.toString();
  } catch {
    return text.replace(/:\/\/([^:@/\s]+):([^@/\s]+)@/g, "://***:***@");
  }
}

function collectAddressHints(value, keyType) {
  if (!ADDRESS_KEY_TYPES.has(String(keyType || "").toLowerCase())) return [];
  const hints = new Set();

  function add(candidate) {
    if (candidate === undefined || candidate === null) return;
    const text = redactUrlish(candidate);
    if (text) hints.add(text);
  }

  function walk(node, fieldName = "") {
    if (node === undefined || node === null) return;
    if (typeof node === "string") {
      if (fieldName && ADDRESS_FIELDS.has(fieldName.toLowerCase())) add(node);
      if (/^[a-z][a-z0-9+.-]*:\/\//i.test(node) || /^[A-Za-z]:[\\/]/.test(node) || node.startsWith("\\\\")) add(node);
      return;
    }
    if (Array.isArray(node)) {
      for (const item of node) walk(item, fieldName);
      return;
    }
    if (typeof node === "object") {
      for (const [key, child] of Object.entries(node)) walk(child, key);
    }
  }

  try {
    walk(JSON.parse(value));
  } catch {
    walk(value);
  }

  return [...hints];
}

async function searchServices(auth, query, opts = {}) {
  const q = normalizeSearchText(query);
  if (!q) throw new Error("Search query is required");
  const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
  if (result.error) throw new Error(result.error);

  const services = result.services || [];
  const rows = [];

  for (const s of services) {
    const fields = {
      name: s.name,
      label: s.label,
      project: s.project,
      type: s.key_type,
      description: s.description
    };
    const matched = Object.entries(fields)
      .filter(([, value]) => normalizeSearchText(value).includes(q))
      .map(([field]) => field);

    let addressHints = [];
    if (opts.addresses === true && ADDRESS_KEY_TYPES.has(String(s.key_type || "").toLowerCase()) && s.vault_key) {
      const secret = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, s.name);
      if (!secret.error) {
        addressHints = collectAddressHints(secret.value, s.key_type);
        if (addressHints.some(h => normalizeSearchText(h).includes(q))) matched.push("address");
      }
    }

    if (matched.length) rows.push({ ...s, matched: [...new Set(matched)], addressHints });
  }

  return rows;
}

// ============================================================
// Program
// ============================================================
const program = new Command();

program
  .name("clauth")
  .version(VERSION)
  .description(chalk.cyan("🔐 clauth") + " — Hardware-bound credential vault for LIFEAI infrastructure");

// ──────────────────────────────────────────────
// clauth install  (Supabase provisioning + skill install + test)
// ──────────────────────────────────────────────
import { runInstall } from './commands/install.js';
import { runUninstall } from './commands/uninstall.js';
import { runScrub } from './commands/scrub.js';
import { runServe } from './commands/serve.js';
import { runCodevelop } from './commands/codevelop.js';
import { runNpm, runPublish } from './commands/npm.js';

program
  .command('install')
  .description('Provision Supabase, deploy Edge Function, install Claude skill')
  .option('--ref <ref>', 'Supabase project ref')
  .option('--pat <pat>', 'Supabase Personal Access Token')
  .action(async (opts) => {
    await runInstall(opts);
  });

program
  .command('uninstall')
  .description('Full teardown — drop DB objects, Edge Function, secrets, skill, config')
  .option('--ref <ref>', 'Supabase project ref')
  .option('--pat <pat>', 'Supabase Personal Access Token (required)')
  .option('--yes', 'Skip confirmation prompt')
  .action(async (opts) => {
    if (!opts.yes) {
      const inquirerMod = await import('inquirer');
      const { confirm } = await inquirerMod.default.prompt([{
        type: 'input',
        name: 'confirm',
        message: chalk.red('Type "CONFIRM UNINSTALL" to proceed:'),
      }]);
      if (confirm !== 'CONFIRM UNINSTALL') {
        console.log(chalk.yellow('\n  Uninstall cancelled.\n'));
        process.exit(0);
      }
    }
    await runUninstall(opts);
  });

program
  .command("codevelop")
  .description("Install and launch Claude/Codex co-development terminal sessions")
  .argument("[action]", "install-terminal | start | join | say | read | watch | who | ask | reply | inbox | listen | request-partner | launch-peer | check-partner | sync | help", "help")
  .option("--repo <path>", "Repo root", "C:\\Dev\\regen-root")
  .option("--port <port>", "Isolated clauth port", "53137")
  .option("--base-url <url>", "Override clauth base URL")
  .option("--name <name>", "Session name")
  .option("--session <idOrManifestPath>", "Co-develop session id or manifest path")
  .option("--task <task>", "Initial partner request task")
  .option("--context <path>", "Context, plan, or architecture file the partner should read first")
  .option("--channel <name>", "Ad hoc channel name for join/say/read/watch")
  .option("--message <text>", "Ad hoc message for say")
  .option("--to <peer>", "Target peer for ask/reply, or optional direct ad hoc recipient for say")
  .option("--from <peer>", "Sender peer for ask/reply")
  .option("--turn <turn_id>", "Turn ID for reply")
  .option("--role <role>", "Turn role, e.g. reviewer or builder")
  .option("--skill <skill>", "Requested skill name, e.g. rdc:review")
  .option("--verdict <verdict>", "Reply verdict: pass, fail, or blocked")
  .option("--summary <summary>", "Reply summary")
  .option("--evidence <items>", "Reply evidence; separate multiple items with semicolons")
  .option("--files-changed <items>", "Reply changed files; separate multiple items with semicolons")
  .option("--commits <items>", "Reply commits; separate multiple items with semicolons")
  .option("--blockers <items>", "Reply blockers; separate multiple items with semicolons")
  .option("--next <items>", "Reply next actions; separate multiple items with semicolons")
  .option("--wait", "For ask: wait for a matching reply")
  .option("--once", "For listen: exit after the first message event")
  .option("--json", "For read: print raw JSON")
  .option("--timeout-ms <ms>", "Wait timeout in milliseconds", "300000")
  .option("--interval-ms <ms>", "Wait polling interval in milliseconds", "2000")
  .option("--start-isolated-clauth", "Start isolated clauth if the selected port is not running")
  .option("--peer <peer>", "Peer name for launch-peer/check-partner/sync, or ad hoc name such as codex-1")
  .option("--dry-run", "Print and write session manifest without opening Windows Terminal")
  .option("--no-open", "Create session/config but do not open Windows Terminal")
  .option("--print-only", "For launch-peer: resolve command without starting the CLI")
  .action(async (action, opts) => {
    await runCodevelop({ ...opts, action });
  });

program
  .command("npm")
  .description("Operate npm auth safely through the clauth npm service")
  .argument("[action]", "whoami | tokens | set-local | sync-github-secret | rerun | help", "help")
  .argument("[args...]", "Action arguments")
  .option("--repo <repo>", "GitHub repo, e.g. LIFEAI/rdc-skills")
  .action(async (action, args, opts) => {
    await runNpm(action, { ...opts, args });
  });

// ──────────────────────────────────────────────
// clauth publish [target]
// Guarded npm publish for ANY package — refuses to ship code that isn't
// committed AND pushed to GitHub (prevents npm/repo divergence from dev builds).
// ──────────────────────────────────────────────
program
  .command("publish [target]")
  .description("Safely publish an npm package (default: cwd). Refuses unless committed + pushed to GitHub.")
  .option("--dry-run", "Run all guards and pack, but do not publish")
  .option("--access <access>", "npm access: public | restricted")
  .option("--allow-dirty", "Override the uncommitted-changes guard (NOT recommended)")
  .option("--allow-unpushed", "Override the not-pushed-to-remote guard (NOT recommended)")
  .action(async (target, opts) => {
    try {
      await runPublish(target, opts);
    } catch (err) {
      console.error(chalk.red(err.message));
      process.exitCode = 1;
    }
  });

// ──────────────────────────────────────────────
// clauth setup
// ──────────────────────────────────────────────
program
  .command("setup")
  .description("Register this machine with the vault (run after clauth install)")
  .option("--admin-token <token>", "Bootstrap token (from clauth install output)")
  .option("--enrollment-code <code>", "One-time enrollment code from clauth enroll")
  .option("--supabase-url <url>", "Vault Supabase URL, for enrolling a new computer without running clauth install")
  .option("--anon-key <key>", "Vault Supabase anon key, for enrolling a new computer without running clauth install")
  .option("--install-id <id>", "Logical install/owner group for admin-token setup", "default")
  .option("--label <label>", "Human label for this machine")
  .option("-p, --pw <password>", "Password (skip interactive prompt)")
  .action(async (opts) => {
    console.log(chalk.cyan("\n🔐 clauth setup\n"));

    if (opts.supabaseUrl) config.set("supabase_url", opts.supabaseUrl);
    if (opts.anonKey) config.set("supabase_anon_key", opts.anonKey);

    // URL + anon key may already be saved by clauth install, or provided by
    // an old-machine enrollment command.
    let savedUrl  = config.get("supabase_url");
    let savedAnon = config.get("supabase_anon_key");
    if (!savedUrl || !savedAnon) {
      const configAnswers = await inquirer.prompt([
        { type: "input", name: "supabaseUrl", message: "Vault Supabase URL:", default: savedUrl || opts.supabaseUrl || "" },
        { type: "password", name: "anonKey", message: "Vault anon key:", mask: "*", default: savedAnon || opts.anonKey || "" },
      ]);
      if (!configAnswers.supabaseUrl || !configAnswers.anonKey) {
        console.log(chalk.yellow("  Supabase config not found. Run clauth install first, or provide --supabase-url and --anon-key.\n"));
        process.exit(1);
      }
      config.set("supabase_url", configAnswers.supabaseUrl);
      config.set("supabase_anon_key", configAnswers.anonKey);
      savedUrl = configAnswers.supabaseUrl;
      savedAnon = configAnswers.anonKey;
    }
    console.log(chalk.gray(`  Project: ${savedUrl}\n`));

    let answers;
    if (opts.pw && (opts.adminToken || opts.enrollmentCode)) {
      // Non-interactive mode — all flags provided
      answers = {
        label: opts.label || os.hostname(),
        pw: opts.pw,
        adminTk: opts.adminToken,
        enrollmentCode: opts.enrollmentCode,
      };
    } else if (opts.enrollmentCode) {
      const pw = opts.pw || await promptPassword("Set clauth password for this computer");
      answers = {
        label: opts.label || os.hostname(),
        pw,
        adminTk: opts.adminToken,
        enrollmentCode: opts.enrollmentCode,
      };
    } else {
      answers = await inquirer.prompt([
        { type: "input",    name: "label",   message: "Machine label:",   default: opts.label || os.hostname() },
        { type: "password", name: "pw",      message: "Set password:",    mask: "*", default: opts.pw || "" },
        { type: "password", name: "enrollmentCode", message: "Enrollment code (preferred for new computer; leave blank if using bootstrap token):", mask: "*",
          default: opts.enrollmentCode || "" },
        { type: "password", name: "adminTk", message: "Bootstrap token (admin fallback):", mask: "*",
          default: opts.adminToken || "" },
      ]);
    }

    const spinner = ora("Registering machine with vault...").start();
    try {
      const machineHash = getMachineHash();
      const seedHash = deriveSeedHash(machineHash, answers.pw);
      const result = answers.enrollmentCode
        ? await api.redeemEnrollment(machineHash, seedHash, answers.label, answers.enrollmentCode)
        : await api.registerMachine(machineHash, seedHash, answers.label, answers.adminTk, { install_id: opts.installId || "default" });
      if (result.error) throw new Error(result.error);
      spinner.succeed(chalk.green(`Machine registered: ${machineHash.slice(0,12)}... install_id=${result.install_id || opts.installId || "default"}`));

      console.log(chalk.green("\n✓ clauth is ready.\n"));
      console.log(chalk.cyan("  clauth test     — verify connection"));
      console.log(chalk.cyan("  clauth status   — see all services\n"));
    } catch (err) {
      spinner.fail(chalk.red(`Setup failed: ${err.message}`));
      process.exit(1);
    }
  });

// ──────────────────────────────────────────────
// clauth enroll
// ──────────────────────────────────────────────
program
  .command("enroll")
  .description("Create a one-time enrollment code for adding another computer")
  .option("--label <label>", "Suggested label for the new computer")
  .option("--ttl-minutes <minutes>", "Enrollment lifetime, 5 to 1440 minutes", "60")
  .option("--install-id <id>", "Override install id; default is current machine's install id")
  .option("-p, --pw <password>", "Password (or will prompt)")
  .action(async (opts) => {
    console.log(chalk.cyan("\n🔐 clauth enroll\n"));
    const auth = await getAuth(opts.pw);
    const spinner = ora("Creating one-time machine enrollment...").start();
    try {
      const result = await api.createEnrollment(
        auth.password,
        auth.machineHash,
        auth.token,
        auth.timestamp,
        opts.label,
        Number(opts.ttlMinutes || 60),
        opts.installId
      );
      if (result.error) throw new Error(result.error);
      const supabaseUrl = config.get("supabase_url");
      const anonKey = config.get("supabase_anon_key");
      const scriptPath = writeEnrollmentScript({
        supabaseUrl,
        anonKey,
        enrollmentCode: result.enrollment_code,
        label: opts.label,
      });
      spinner.succeed(chalk.green(`Enrollment created for install_id=${result.install_id}`));
      console.log("");
      console.log(chalk.bold("  Enrollment code:"));
      console.log(chalk.white(`  ${result.enrollment_code}`));
      console.log("");
      console.log(chalk.bold("  On the new computer:"));
      console.log(chalk.gray(`  Run this one-time script: ${scriptPath}`));
      console.log(chalk.gray("  It installs clauth, enrolls with this code, installs startup, then deletes itself."));
      console.log("");
      console.log(chalk.gray(`  Expires: ${result.expires_at}`));
    } catch (err) {
      spinner.fail(chalk.red(`Enroll failed: ${err.message}`));
      process.exitCode = 1;
    }
  });

// ──────────────────────────────────────────────
// clauth status
// ──────────────────────────────────────────────
program
  .command("status")
  .description("Show all services and their state")
  .option("-p, --pw <password>", "Password (or will prompt)")
  .option("--project <name>", "Filter by project scope")
  .action(async (opts) => {
    const auth = await getAuth(opts.pw);
    const spinner = ora("Fetching service status...").start();
    try {
      const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
      spinner.stop();
      if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }

      const heading = opts.project ? `clauth service status (project: ${opts.project})` : "clauth service status";
      console.log(chalk.cyan(`\n🔐 ${heading}\n`));
      console.log(
        chalk.bold(
          "  " + "SERVICE".padEnd(24) + "TYPE".padEnd(12) + "PROJECT".padEnd(22) + "STATUS".padEnd(12) +
          "KEY STORED".padEnd(12) + "LAST RETRIEVED"
        )
      );
      console.log("  " + "─".repeat(90));

      for (const s of result.services || []) {
        const status = s.enabled
          ? chalk.green("ACTIVE".padEnd(12))
          : s.vault_key
            ? chalk.yellow("SUSPENDED".padEnd(12))
            : chalk.gray("NO KEY".padEnd(12));
        const hasKey = s.vault_key ? chalk.green("✓".padEnd(12)) : chalk.gray("—".padEnd(12));
        const lastGet = s.last_retrieved
          ? new Date(s.last_retrieved).toLocaleDateString()
          : chalk.gray("never");
        const proj = s.project ? chalk.blue(s.project.padEnd(22)) : chalk.gray("—".padEnd(22));

        console.log(`  ${s.name.padEnd(24)}${s.key_type.padEnd(12)}${proj}${status}${hasKey}${lastGet}`);
      }
      console.log();
    } catch (err) {
      spinner.fail(chalk.red(err.message));
    }
  });

// ──────────────────────────────────────────────
// clauth write pw <new_password>
// clauth write params
// clauth write key <service> <value>
// ──────────────────────────────────────────────
const writeCmd = program.command("write").description("Write credentials or update auth parameters");

writeCmd
  .command("pw [newpw]")
  .description("Set or update clauth master password")
  .action(async (newpw) => {
    console.log(chalk.cyan("\n🔐 clauth write pw\n"));
    const current = await promptPassword("Current password (to verify)");
    const pw = newpw || (await inquirer.prompt([
      { type: "password", name: "p", message: "New password:", mask: "*" },
      { type: "password", name: "c", message: "Confirm new password:", mask: "*" }
    ]).then(a => { if (a.p !== a.c) { console.log(chalk.red("Passwords don't match")); process.exit(1); } return a.p; }));

    // Re-register machine with new seed hash
    const machineHash = getMachineHash();
    const newSeedHash = deriveSeedHash(machineHash, pw);
    const { token, timestamp } = deriveToken(current, machineHash);
    const adminToken = await inquirer.prompt([{
      type: "password", name: "t", message: "Admin bootstrap token (required for re-registration):", mask: "*"
    }]).then(a => a.t);

    const spinner = ora("Updating password and re-registering machine...").start();
    try {
      const result = await api.registerMachine(machineHash, newSeedHash, null, adminToken);
      if (result.error) throw new Error(result.error);
      spinner.succeed(chalk.green("Password updated and machine re-registered."));
    } catch (err) {
      spinner.fail(chalk.red(err.message));
    }
  });

writeCmd
  .command("params")
  .description("Re-read hardware fingerprint (use after hardware change)")
  .action(async () => {
    const spinner = ora("Reading hardware fingerprint...").start();
    try {
      const hash = getMachineHash();
      spinner.succeed(chalk.green(`Machine hash: ${hash.slice(0,16)}...`));
      console.log(chalk.gray("Full hash: " + hash));
    } catch (err) {
      spinner.fail(chalk.red(err.message));
    }
  });

writeCmd
  .command("key <service> [value]")
  .description("Write a credential into vault for a service")
  .option("-p, --pw <password>", "Password")
  .action(async (service, value, opts) => {
    const auth = await getAuth(opts.pw);
    let val = value;
    if (!val) {
      const { v } = await inquirer.prompt([{ type: "password", name: "v", message: `Value for ${service}:`, mask: "*" }]);
      val = v;
    }
    const spinner = ora(`Writing key for ${service}...`).start();
    try {
      const { result, snapshot, normalized } = await writeCredentialWithRecovery({
        password: auth.password,
        machineHash: auth.machineHash,
        service,
        value: val,
      });
      if (result.error) throw new Error(result.error);
      const details = [
        snapshot?.ok ? "recovery snapshot written" : null,
        normalized ? "value normalized" : null,
      ].filter(Boolean);
      spinner.succeed(chalk.green(`Key stored in vault: auth.${service}${details.length ? ` (${details.join(", ")})` : ""}`));
    } catch (err) {
      spinner.fail(chalk.red(err.message));
    }
  });

// ──────────────────────────────────────────────
// clauth enable <service|all>
// clauth disable <service|all>
// ──────────────────────────────────────────────
program
  .command("enable <service>")
  .description("Enable a service (or 'all')")
  .option("-p, --pw <password>")
  .action(async (service, opts) => {
    const auth = await getAuth(opts.pw);
    const spinner = ora(`Enabling ${service}...`).start();
    try {
      const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, true);
      if (result.error) throw new Error(result.error);
      spinner.succeed(chalk.green(`Enabled: ${service}`));
    } catch (err) { spinner.fail(chalk.red(err.message)); }
  });

program
  .command("disable <service>")
  .description("Disable a service (or 'all')")
  .option("-p, --pw <password>")
  .action(async (service, opts) => {
    const auth = await getAuth(opts.pw);
    const spinner = ora(`Disabling ${service}...`).start();
    try {
      const result = await api.enable(auth.password, auth.machineHash, auth.token, auth.timestamp, service, false);
      if (result.error) throw new Error(result.error);
      spinner.succeed(chalk.yellow(`Disabled: ${service}`));
    } catch (err) { spinner.fail(chalk.red(err.message)); }
  });

// ──────────────────────────────────────────────
// clauth add service <name>
// clauth remove service <name>
// clauth list services
// ──────────────────────────────────────────────
const addCmd = program.command("add").description("Add resources to the registry");

addCmd
  .command("service <name>")
  .description("Register a new service slot")
  .option("--type <type>", "Key type: token | keypair | connstring | oauth")
  .option("--label <label>", "Human-readable label")
  .option("--description <desc>", "Description")
  .option("--project <project>", "Project scope (groups related services)")
  .option("-p, --pw <password>")
  .action(async (name, opts) => {
    const auth = await getAuth(opts.pw);
    let answers;
    if (opts.type && opts.label) {
      // Non-interactive — all flags provided
      answers = { label: opts.label, key_type: opts.type, desc: opts.description || "" };
    } else {
      answers = await inquirer.prompt([
        { type: "input",  name: "label", message: "Label:", default: opts.label || name },
        { type: "list",   name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
        { type: "input",  name: "desc",  message: "Description (optional):", default: opts.description || "" }
      ]);
    }
    const spinner = ora(`Adding service: ${name}${opts.project ? ` (project: ${opts.project})` : ""}...`).start();
    try {
      const result = await api.addService(
        auth.password, auth.machineHash, auth.token, auth.timestamp,
        name, answers.label, answers.key_type, answers.desc, opts.project
      );
      if (result.error) throw new Error(result.error);
      spinner.succeed(chalk.green(`Service added: ${name} (${answers.key_type})${opts.project ? chalk.blue(` [${opts.project}]`) : ""}`));
      console.log(chalk.gray(`  Next: clauth write key ${name}`));
    } catch (err) { spinner.fail(chalk.red(err.message)); }
  });

const removeCmd = program.command("remove").description("Remove resources from the registry");

removeCmd
  .command("service <name>")
  .description("Remove a service and its key from vault")
  .option("-p, --pw <password>")
  .action(async (name, opts) => {
    const { confirm } = await inquirer.prompt([{
      type: "input", name: "confirm",
      message: chalk.red(`Type "CONFIRM REMOVE ${name.toUpperCase()}" to proceed:`)
    }]);
    const auth = await getAuth(opts.pw);
    const spinner = ora(`Removing ${name}...`).start();
    try {
      const result = await api.removeService(auth.password, auth.machineHash, auth.token, auth.timestamp, name, confirm);
      if (result.error) throw new Error(result.error);
      spinner.succeed(chalk.yellow(`Removed: ${name}`));
    } catch (err) { spinner.fail(chalk.red(err.message)); }
  });

program
  .command("list")
  .description("List all registered services")
  .option("-p, --pw <password>")
  .option("--project <name>", "Filter by project scope")
  .action(async (opts) => {
    const auth = await getAuth(opts.pw);
    const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
    if (result.error) { console.log(chalk.red(result.error)); return; }
    const heading = opts.project ? `Registered services (project: ${opts.project})` : "Registered services";
    console.log(chalk.cyan(`\n ${heading}:\n`));
    let lastProject = undefined;
    for (const s of result.services || []) {
      const proj = s.project || null;
      if (proj !== lastProject) {
        if (lastProject !== undefined) console.log();
        console.log(chalk.gray(`  [${proj || "global"}]`));
        lastProject = proj;
      }
      console.log(`  ${chalk.bold(s.name.padEnd(24))} ${chalk.gray(s.key_type.padEnd(12))} ${chalk.gray(s.label || "")}`);
    }
    console.log();
  });

program
  .command("search <query>")
  .description("Search services by name, label, project, description, or type")
  .option("-p, --pw <password>")
  .option("--project <name>", "Filter by project scope")
  .option("--addresses", "Also search redacted address hints from address-bearing secrets (may retrieve multiple secrets)")
  .action(async (query, opts) => {
    const auth = await getAuth(opts.pw);
    const spinner = ora("Searching services...").start();
    try {
      const rows = await searchServices(auth, query, { project: opts.project, addresses: opts.addresses === true });
      spinner.stop();
      console.log(chalk.cyan(`\n Search results for "${query}":\n`));
      if (!rows.length) {
        console.log(chalk.gray("  No matching services found.\n"));
        return;
      }
      console.log(chalk.bold("  " + "SERVICE".padEnd(24) + "TYPE".padEnd(12) + "PROJECT".padEnd(20) + "MATCHED"));
      console.log("  " + "─".repeat(78));
      for (const s of rows) {
        const project = s.project || "global";
        console.log(`  ${chalk.bold(s.name.padEnd(24))}${String(s.key_type || "").padEnd(12)}${project.padEnd(20)}${s.matched.join(", ")}`);
        if (s.label) console.log(chalk.gray(`    label: ${s.label}`));
        if (s.description) console.log(chalk.gray(`    description: ${s.description}`));
        for (const hint of s.addressHints || []) console.log(chalk.gray(`    address: ${hint}`));
      }
      console.log();
    } catch (err) {
      spinner.fail(chalk.red(err.message));
    }
  });

// ──────────────────────────────────────────────
// clauth test <service|all>
// ──────────────────────────────────────────────
program
  .command("test [service]")
  .description("Test HMAC handshake — no key returned")
  .option("-p, --pw <password>")
  .action(async (service, opts) => {
    const auth = await getAuth(opts.pw);
    const spinner = ora("Testing auth handshake...").start();
    try {
      const result = await api.test(auth.password, auth.machineHash, auth.token, auth.timestamp);
      if (result.error) throw new Error(`${result.error}: ${result.reason}`);
      spinner.succeed(chalk.green("PASS — HMAC validated"));
      console.log(chalk.gray(`  Machine: ${auth.machineHash.slice(0,16)}...`));
      console.log(chalk.gray(`  Window:  ${new Date(result.timestamp).toISOString()}`));
    } catch (err) {
      spinner.fail(chalk.red("FAIL — " + err.message));
    }
  });

// ──────────────────────────────────────────────
// clauth get <service>
// ──────────────────────────────────────────────
program
  .command("get <service>")
  .description("Retrieve a key from vault")
  .option("-p, --pw <password>")
  .option("--json", "Output raw JSON")
  .action(async (service, opts) => {
    const auth = await getAuth(opts.pw);
    const spinner = ora(`Retrieving ${service}...`).start();
    try {
      const result = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, service);
      spinner.stop();
      if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
      if (opts.json) {
        console.log(JSON.stringify(result, null, 2));
      } else {
        console.log(chalk.cyan(`\n🔑 ${service} (${result.key_type})\n`));
        const val = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
        console.log(val);
        console.log();
      }
    } catch (err) {
      spinner.fail(chalk.red(err.message));
    }
  });

// ──────────────────────────────────────────────
// clauth revoke <service|all>
// ──────────────────────────────────────────────
program
  .command("revoke <service>")
  .description("Delete key from vault (destructive)")
  .option("-p, --pw <password>")
  .action(async (service, opts) => {
    const phrase = service === "all" ? "CONFIRM REVOKE ALL" : `CONFIRM REVOKE ${service.toUpperCase()}`;
    const { confirm } = await inquirer.prompt([{
      type: "input", name: "confirm",
      message: chalk.red(`Type "${phrase}" to proceed:`)
    }]);
    const auth = await getAuth(opts.pw);
    const spinner = ora(`Revoking ${service}...`).start();
    try {
      const result = await api.revoke(auth.password, auth.machineHash, auth.token, auth.timestamp, service, confirm);
      if (result.error) throw new Error(result.error);
      spinner.succeed(chalk.yellow(`Revoked: ${service}`));
    } catch (err) { spinner.fail(chalk.red(err.message)); }
  });

// ──────────────────────────────────────────────
// clauth scrub [target]
// ──────────────────────────────────────────────
program
  .command("scrub [target]")
  .description("Scrub credentials from Claude Code transcript logs (no auth required)")
  .option("--force", "Rescrub files even if already marked clean")
  .addHelpText("after", `
Examples:
  clauth scrub              Scrub the most recent (active) transcript
  clauth scrub <file>       Scrub a specific file
  clauth scrub all          Scrub every transcript + tool-result sidecar (.jsonl + .txt)
  clauth scrub all --force  Rescrub all files (ignore markers)
  clauth scrub session      Scrub ONLY the ending session (transcript + sidecars); reads SessionEnd hook JSON on stdin

  Redacts: built-in token patterns, your ~/.clauth/scrub-patterns.json,
  and this machine's live vault values (best-effort via the daemon).
`)
  .action(async (target, opts) => {
    await runScrub(target, opts);
  });

// ──────────────────────────────────────────────
// clauth watchdog
// ──────────────────────────────────────────────
program
  .command("watchdog [action] [args...]")
  .description("Manage auto-restart watchdog (install|uninstall|status|start|register|list|events|restart)")
  .option("--manifest <path>", "Watchdog service manifest for register")
  .option("--service <id>", "Watchdog service id for restart")
  .option("--limit <n>", "Event count for events", "100")
  .action(async (action, args, opts) => {
    const { runWatchdog } = await import("./commands/watchdog.js");
    await runWatchdog(action, { ...opts, args });
  });

// clauth doctor
// ──────────────────────────────────────────────
program
  .command("doctor")
  .description("Check all prerequisites and diagnose issues")
  .action(async () => {
    const { runDoctor } = await import("./commands/doctor.js");
    await runDoctor();
  });

// ──────────────────────────────────────────────
// clauth invite generate|list|revoke
// ──────────────────────────────────────────────
const invite = program.command("invite").description("Manage vault invites");

invite
  .command("generate")
  .description("Generate an invite code for a friend")
  .option("--uses <n>", "Max redemptions", "1")
  .option("--expires <hours>", "Expiry in hours", "168")
  .action(async (opts) => {
    const { runInvite } = await import("./commands/invite.js");
    await runInvite("generate", opts);
  });

invite
  .command("list")
  .description("List active invites")
  .action(async () => {
    const { runInvite } = await import("./commands/invite.js");
    await runInvite("list", {});
  });

invite
  .command("revoke <code>")
  .description("Revoke an invite code")
  .action(async (code) => {
    const { runInvite } = await import("./commands/invite.js");
    await runInvite("revoke", { code });
  });

// ──────────────────────────────────────────────
// clauth join <invite-code>
// ──────────────────────────────────────────────
program
  .command("join <invite-code>")
  .description("Join a vault using an invite code from a friend")
  .action(async (code) => {
    const { runJoin } = await import("./commands/join.js");
    await runJoin(code);
  });

// ──────────────────────────────────────────────
// clauth update
// ──────────────────────────────────────────────
program
  .command("update")
  .description("Update clauth to the latest version")
  .action(async () => {
    const { execSync } = await import("child_process");
    console.log(chalk.cyan("\n  Updating clauth...\n"));
    try {
      execSync("npm install -g @lifeaitools/clauth@latest", { stdio: "inherit" });
      console.log(chalk.green("\n  Updated successfully.\n"));
    } catch (err) {
      console.log(chalk.red(`\n  Update failed: ${err.message}\n`));
    }
  });

// ──────────────────────────────────────────────
// clauth tunnel start|stop|status
// (setup moved to in-browser wizard at http://127.0.0.1:52437)
// ──────────────────────────────────────────────
const tunnelCmd = program.command("tunnel").description("Manage Cloudflare tunnel for claude.ai web integration");

tunnelCmd
  .command("setup")
  .description("Open the tunnel setup wizard in your browser")
  .action(async () => {
    console.log(chalk.cyan("\n  Tunnel setup is now handled in the browser.\n"));
    console.log(chalk.white("  1. Start the daemon:  clauth serve start"));
    console.log(chalk.white("  2. Open:              http://127.0.0.1:52437"));
    console.log(chalk.white("  3. Unlock the vault and click \"Setup Tunnel\"\n"));
  });

tunnelCmd
  .command("start")
  .description("Tell daemon to start the tunnel")
  .action(async () => {
    try {
      const r = await fetch("http://127.0.0.1:52437/tunnel/start", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        signal: AbortSignal.timeout(5000),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) {
        console.error(`  ✗ ${data.error || r.statusText}`);
        if (r.status === 401) console.error("  Unlock the daemon first: http://127.0.0.1:52437");
        process.exit(1);
      }
      console.log(`  ✓ ${data.message || "Tunnel starting — check status with: clauth tunnel status"}`);
    } catch (e) {
      console.error("  ✗ Daemon not running. Start it with: clauth serve");
      process.exit(1);
    }
  });

tunnelCmd
  .command("stop")
  .description("Tell daemon to stop the tunnel")
  .action(async () => {
    try {
      const r = await fetch("http://127.0.0.1:52437/tunnel/stop", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        signal: AbortSignal.timeout(5000),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) {
        console.error(`  ✗ ${data.error || r.statusText}`);
        process.exit(1);
      }
      console.log("  ✓ Tunnel stopped.");
    } catch (e) {
      console.error("  ✗ Daemon not running.");
      process.exit(1);
    }
  });

tunnelCmd
  .command("status")
  .description("Show current tunnel status")
  .action(async () => {
    try {
      const r = await fetch("http://127.0.0.1:52437/tunnel", {
        signal: AbortSignal.timeout(5000),
      });
      const data = await r.json().catch(() => ({}));
      const icons = {
        live: "✓", starting: "◌", not_configured: "⚠",
        not_started: "○", error: "✗", missing_cloudflared: "✗",
      };
      const labels = {
        live: `Live — ${data.url || ""}`,
        starting: "Starting...",
        not_configured: "Not configured — open http://127.0.0.1:52437 and click Setup Tunnel",
        not_started: "Not started — run: clauth tunnel start",
        error: `Error${data.error ? ": " + data.error : ""} — check cloudflared config`,
        missing_cloudflared: "cloudflared not installed — https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/",
      };
      const status = data.status || "unknown";
      console.log(`\n  ${icons[status] || "?"} Tunnel: ${labels[status] || status}\n`);
    } catch (e) {
      console.error("  ✗ Daemon not running. Start it with: clauth serve");
      process.exit(1);
    }
  });

// ──────────────────────────────────────────────
// clauth chitchat --session <id>
// ──────────────────────────────────────────────
program
  .command("chitchat")
  .description("Join a chitchat collab session — auto-starts /rdc:collab via claude -p")
  .requiredOption("--session <id>", "Session ID from claude.ai")
  .action(async (opts) => {
    const id = opts.session;
    // Verify session exists in the daemon
    try {
      const r = await fetch(`http://127.0.0.1:52437/chitchat/${id}`, { signal: AbortSignal.timeout(3000) });
      if (!r.ok) {
        console.error(`\n  ✗ Session ${id} not found (daemon returned ${r.status})\n`);
        process.exit(1);
      }
    } catch (e) {
      console.error(`\n  ✗ Daemon not reachable at http://127.0.0.1:52437 — is clauth running?\n`);
      process.exit(1);
    }
    console.log(chalk.cyan(`\n  [collab] session ${id} — starting /rdc:collab...\n`));

    // Find claude binary (same candidates as daemon)
    const { execSync: es, spawn } = await import("child_process");
    let claudeBin = null;
    for (const c of [
      process.env.CLAUDE_BIN,
      path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'),
      path.join(process.env.APPDATA || '', 'npm', 'claude'),
      'claude',
    ].filter(Boolean)) {
      try { es(`"${c}" --version`, { stdio: 'ignore', timeout: 3000 }); claudeBin = c; break; } catch {}
    }
    if (!claudeBin) {
      console.error('  ✗ claude CLI not found — is @anthropic-ai/claude-code installed globally?');
      process.exit(1);
    }

    // Auto-invoke /rdc:collab skill with streaming output to this terminal
    const proc = spawn(claudeBin, [
      '-p', `/rdc:collab --session ${id}`,
      '--dangerously-skip-permissions',
    ], {
      stdio: 'inherit',
      cwd: 'C:/Dev/regen-root',
      shell: true,
    });
    proc.on('error', e => { console.error(`  ✗ spawn error: ${e.message}`); process.exit(1); });
    proc.on('exit', code => process.exit(code ?? 0));
  });

// ──────────────────────────────────────────────
// clauth --help override banner
// ──────────────────────────────────────────────
program.addHelpText("beforeAll", chalk.cyan(`
  ██████╗██╗      █████╗ ██╗   ██╗████████╗██╗  ██╗
 ██╔════╝██║     ██╔══██╗██║   ██║╚══██╔══╝██║  ██║
 ██║     ██║     ███████║██║   ██║   ██║   ███████║
 ██║     ██║     ██╔══██║██║   ██║   ██║   ██╔══██║
 ╚██████╗███████╗██║  ██║╚██████╔╝   ██║   ██║  ██║
  ╚═════╝╚══════╝╚═╝  ╚═╝ ╚═════╝    ╚═╝   ╚═╝  ╚═╝
  v${VERSION} — LIFEAI Credential Vault
`));

// ──────────────────────────────────────────────
// clauth serve [action]
// ──────────────────────────────────────────────
program
  .command("serve [action]")
  .description("Manage localhost HTTP vault daemon (start|stop|restart|ping|install|uninstall)")
  .option("--port <n>", "Port (default: 52437)")
  .option("-p, --pw <password>", "clauth password (optional — omit to start locked, unlock in browser)")
  .option("--services <list>", "Comma-separated service whitelist (default: all)")
  .option("--tunnel <hostname>", "Fixed tunnel hostname (e.g. clauth.prtrust.fund) — uses named Cloudflare Tunnel instead of random URL")
  .option("--staged", "Start on staging port (52438) for blue-green verification before make-live")
  .option("--isolated", "Run on a non-live port without touching live PID files, browser, or boot-key credentials")
  .option("--from-boot-key", "Internal: password came from boot.key auto-unlock (degrade gracefully on verify failure)")
  .option("--action <action>", "Internal: action override for daemon child")
  .addHelpText("after", `
Actions:
  start       Start the server as a background daemon
  stop        Stop the running daemon
  restart     Stop + start
  ping        Check if the daemon is running
  foreground  Run in foreground (Ctrl+C to stop) — default if no action given
  mcp         Run as MCP stdio server for Claude Code (JSON-RPC over stdin/stdout)
  install     Store password securely + register auto-start service (cross-platform)
              Windows: DPAPI + HKCU\\Run | macOS: Keychain + LaunchAgent | Linux: libsecret/openssl + systemd
  uninstall   Remove auto-start service + delete stored password
  upgrade     Blue-green upgrade: start new version on staging port, verify, then make live

MCP SSE (built into start/foreground):
  The HTTP daemon also serves MCP SSE transport at GET /sse + POST /message.
  Connect claude.ai via Cloudflare Tunnel pointing to http://127.0.0.1:52437/sse

Examples:
  clauth serve start                     Start locked — unlock at http://127.0.0.1:52437
  clauth serve start -p mypass           Start pre-unlocked (password in memory only)
  clauth serve stop                      Stop the daemon
  clauth serve ping                      Check status
  clauth serve restart                   Restart (stays locked until browser unlock)
  clauth serve start --services github,vercel
  clauth serve mcp                       Start MCP server for Claude Code
  clauth serve mcp -p mypass             Start MCP server pre-unlocked
  clauth serve foreground --port 53137 --isolated
                                          Start isolated passwordless server for route tests
  clauth serve install                   Set up auto-start on login (DPAPI/Keychain/libsecret)
  clauth serve install --tunnel host     Auto-start with Cloudflare Tunnel
  clauth serve uninstall                 Remove auto-start
`)
  .action(async (action, opts) => {
    const resolvedAction = opts.action || action || "foreground";
    await runServe({ ...opts, action: resolvedAction });
  });

program.parse(process.argv);
