#!/usr/bin/env node
// Standalone installer for the iMessage MCP module. Works without the
// Teletraan framework. Run from inside the unpacked module directory:
//
//   cd imessage-mcp && node install-standalone.js
//
// Prompts for:
//   • Install path (where the MCP files should live; default: this dir)
//   • Target .mcp.json (the file Claude Code reads; default: ~/.claude/.mcp.json
//     or a passed --mcp-config path)
//   • Skills dir (where the /imessage:access skill should be installed;
//     default: <dirname of .mcp.json>/skills/)
//   • FIFO path (only matters if Claude Code is running in --print mode and
//     feeding stdin from a named pipe; leave blank to use MCP-notification
//     mode instead)
//   • Allowlist (comma-separated phones / emails)
//
// Then it:
//   1. Optionally copies module files to <install path> (skipped if install
//      path matches the current dir — common case)
//   2. Runs npm install at the install path
//   3. Writes <install path>/data/access.json with the allowlist
//   4. Merges an imessage entry into target .mcp.json (preserving everything
//      already in there); creates the file if it doesn't exist
//   5. Copies the skill to <skills dir>/imessage-access/

import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, statSync, realpathSync } from "node:fs";
import { join, dirname, resolve } from "node:path";
import { homedir } from "node:os";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { createInterface } from "node:readline";

const __dirname = dirname(fileURLToPath(import.meta.url));
const HOME = homedir();

function expandTilde(p) {
  return p.startsWith("~/") || p === "~" ? join(HOME, p.slice(1)) : p;
}

function header(s) {
  console.log("\n" + s);
  console.log("─".repeat(s.length));
}

// readline.question() hangs after the first read when stdin is a pipe (Node
// 22 behavior — interactive mode is fine, piped mode never gets line 2+).
// Detect the mode at startup: TTY → use rl.question; pipe → read all lines
// upfront via for-await and dispatch from the array. Same interface either way.
const isTTY = process.stdin.isTTY === true;
const rl = createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: isTTY,
});

let pipedLines = null;
let pipedIdx = 0;
async function preloadIfPiped() {
  if (isTTY) return;
  pipedLines = [];
  for await (const line of rl) pipedLines.push(line);
}

function ask(q, def = "") {
  const prompt = def ? `${q} [${def}]: ` : `${q}: `;
  if (!isTTY) {
    const raw = (pipedIdx < pipedLines.length ? pipedLines[pipedIdx++] : "").trim();
    // Echo to stdout so the transcript matches what the user would see.
    process.stdout.write(prompt + raw + "\n");
    return Promise.resolve((raw || def).trim());
  }
  return new Promise((resolve) => {
    rl.question(prompt, (a) => resolve((a.trim() || def).trim()));
  });
}
const askYesNo = async (q, def = false) => {
  const ans = (await ask(`${q} (y/n)`, def ? "y" : "n")).toLowerCase();
  return ans.startsWith("y");
};

async function main() {
  console.log("iMessage MCP — standalone installer");
  console.log("");
  console.log("This sets up the iMessage MCP server for use with Claude Code.");
  console.log("Requires macOS, Node 22+, Messages.app signed in, and Full Disk Access");
  console.log("for whichever process will run the MCP (terminal, sshd-keygen-wrapper, etc.).");
  console.log("");
  console.log("Answer the prompts first; then I'll run npm install and write everything.");

  await preloadIfPiped();

  // Collect ALL answers upfront. npm install can take a minute; piped stdin
  // closes after the last answer is sent, so readline would hit EOF if we
  // prompted after the install. Asking everything first keeps it robust.
  header("Install location");
  const defaultInstall = realpathSync(__dirname);
  const installPath = resolve(expandTilde(
    await ask("Where should the MCP live?", defaultInstall)
  ));

  let overwriteOK = true;
  if (installPath !== defaultInstall && existsSync(installPath)) {
    overwriteOK = await askYesNo(`  ${installPath} already exists. Overwrite?`, false);
    if (!overwriteOK) {
      console.error("Aborted.");
      process.exit(1);
    }
  }

  header("Allowlist");
  console.log("Only these contacts will be able to message the agent — anyone else");
  console.log("is silently dropped. Phones (+15551234567) or email addresses, comma-separated.");
  const raw = await ask("Allowlist", "");
  const allowFrom = raw.split(",").map((s) => s.trim()).filter(Boolean);

  header("MCP registration");
  console.log("Pick the .mcp.json that Claude Code will read. Common locations:");
  console.log(`  • ~/.claude/.mcp.json         (global)`);
  console.log(`  • <project>/.mcp.json         (project-local)`);
  console.log(`  • <agent>/.mcp.json           (per-agent)`);
  const defaultMcp = join(HOME, ".claude", ".mcp.json");
  const mcpFile = resolve(expandTilde(
    await ask("Target .mcp.json", defaultMcp)
  ));

  const fifoPath = (await ask(
    "FIFO path for --print mode inbound delivery (blank = use MCP notifications instead)",
    ""
  )).trim();

  header("Skill installation");
  const defaultSkillsDir = join(dirname(mcpFile), "skills");
  const skillsDir = resolve(expandTilde(
    await ask("Where should the /imessage:access skill go?", defaultSkillsDir)
  ));

  // Close readline now — no more prompts. Anything below this is non-interactive.
  rl.close();

  // ── Execute ────────────────────────────────────────────────────────────────
  if (installPath !== defaultInstall) {
    mkdirSync(installPath, { recursive: true });
    // Copy module sources (exclude node_modules + data — they're regenerated).
    for (const f of ["server.ts", "package.json", "manifest.json", "README.md", "skill"]) {
      const src = join(__dirname, f);
      if (existsSync(src)) cpSync(src, join(installPath, f), { recursive: true });
    }
    console.log(`\n  Copied module sources to ${installPath}`);
  } else {
    console.log(`\n  Using ${installPath} in place.`);
  }

  console.log("  Running npm install (better-sqlite3 native compile — about a minute)...");
  execSync("npm install --silent", {
    cwd: installPath,
    stdio: ["ignore", "inherit", "inherit"],
  });

  if (allowFrom.length === 0) {
    console.log("  Warning: empty allowlist — no inbound iMessages will reach the agent.");
    console.log("  You can edit data/access.json later to add contacts.");
  }

  const stateDir = join(installPath, "data");
  mkdirSync(stateDir, { recursive: true, mode: 0o700 });
  const access = {
    dmPolicy: "allowlist",
    allowFrom,
    groups: {},
    pending: {},
  };
  writeFileSync(join(stateDir, "access.json"), JSON.stringify(access, null, 2) + "\n", { mode: 0o600 });
  console.log(`  Wrote ${stateDir}/access.json (${allowFrom.length} contacts).`);

  let mcp = { mcpServers: {} };
  if (existsSync(mcpFile)) {
    try {
      mcp = JSON.parse(readFileSync(mcpFile, "utf8"));
      if (!mcp.mcpServers || typeof mcp.mcpServers !== "object") mcp.mcpServers = {};
    } catch (err) {
      console.log(`  Warning: ${mcpFile} was unparseable, will be replaced (${err.message})`);
      mcp = { mcpServers: {} };
    }
  } else {
    mkdirSync(dirname(mcpFile), { recursive: true });
  }

  mcp.mcpServers.imessage = {
    command: join(installPath, "node_modules", ".bin", "tsx"),
    args: [join(installPath, "server.ts")],
    env: {
      IMESSAGE_STATE_DIR: stateDir,
      ...(fifoPath ? { ONE_INPUT_FIFO: fifoPath } : {}),
    },
  };

  writeFileSync(mcpFile, JSON.stringify(mcp, null, 2) + "\n");
  console.log(`  Registered \`imessage\` entry in ${mcpFile}`);

  const skillSrc = join(installPath, "skill");
  if (existsSync(skillSrc)) {
    const skillDest = join(skillsDir, "imessage-access");
    mkdirSync(skillDest, { recursive: true });
    cpSync(skillSrc, skillDest, { recursive: true });
    console.log(`  Installed skill at ${skillDest}/SKILL.md`);
  } else {
    console.log("  Skill source not found at " + skillSrc + " — skipping.");
  }

  header("Done");
  console.log("Two macOS permissions still needed before the MCP can do anything:");
  console.log("");
  console.log("  1. Full Disk Access — System Settings → Privacy & Security → Full Disk Access");
  console.log("     Add the process that runs the MCP. For local Terminal use, add Terminal.app.");
  console.log("     For SSH-spawned agents, add /usr/libexec/sshd-keygen-wrapper.");
  console.log("");
  console.log("  2. Automation → Messages — auto-prompts on first send. Click OK when it does.");
  console.log("");
  console.log("Smoke test (from this shell):");
  console.log(`  cd ${installPath}`);
  console.log("  ./node_modules/.bin/tsx server.ts < /dev/null");
  console.log("  # Should log: 'imessage channel: watching chat.db (watermark=N)' then exit cleanly.");
}

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