// @ts-nocheck
/**
 * Helios Upgrade Reviewer Extension v1.0
 *
 * Automatically reviews Pi (npm package: @helios-agent/pi-coding-agent) updates
 * for compatibility BEFORE applying them.
 *
 * Pipeline:
 *  1. Version Check      — compare local vs latest npm version
 *  2. Changelog Delta    — extract and scan entries for breaking-change keywords
 *  3. Type Diff Analysis — compare ExtensionAPI/ExtensionContext signatures
 *  4. Extension Scan     — audit ~/helios-agent/extensions/*.ts for affected API usage
 *  5. Decision & Report  — SAFE / WARN / BREAKING classification
 *  6. Upgrade Execution  — with rollback info, confirmation gates, and verification
 *
 * State file: ~/helios-agent/.upgrade-review-state.json
 * Commands:   /upgrade-review [--force]
 */

import type {
  ExtensionAPI,
  ExtensionContext,
  ExtensionCommandContext,
  SessionStartEvent,
} from "@helios-agent/pi-coding-agent";
import { randomUUID } from "node:crypto";
import { exec as execCb } from "node:child_process";
import {
  existsSync,
  mkdirSync,
  readFileSync,
  readdirSync,
  writeFileSync,
  statSync,
  openSync,
  readSync,
  closeSync,
  unlinkSync,
  rmSync,
  renameSync,
} from "node:fs";
import { homedir, tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { createRequire } from 'module';
const _require = createRequire(import.meta.url);
// BUG-1 fix: use _require (createRequire-backed) instead of bare require()
// bare require() is undefined in ESM — this was crashing on every module load
const { heliosPath } = _require('./lib/helios-root');

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const AGENT_DIR       = heliosPath();
const EXTENSIONS_DIR  = resolve(AGENT_DIR, "extensions");
const STATE_FILE      = resolve(AGENT_DIR, ".upgrade-review-state.json");

/** How long to wait before running session_start auto-check (ms) */
const SESSION_DELAY_MS = 10_000;

/** How often session_start auto-check runs (ms). Default: 24 h */
const COOLDOWN_MS = 24 * 60 * 60 * 1_000;

/** npm package — install path resolved lazily via getPkgInstallDir() */
const PKG_NAME    = "@helios-agent/cli";

/** Resolved lazily via `npm root -g` — works on Homebrew, nvm, Volta, Linux, etc. */
let PKG_INSTALL = "";

/** Remote changelog URL */
const CHANGELOG_URL =
  "https://raw.githubusercontent.com/helios-agi/pi-mono/main/packages/coding-agent/CHANGELOG.md";

/** Breaking-change keywords to scan in changelog entries */
const BREAKING_KEYWORDS = [
  "BREAKING CHANGE",
  "breaking change",
  "breaking:",
  "changed signature",
  "no longer supported",
  "no longer available",
  "replaced by",
  "removed from public API",
  "removed from exports",
  "renamed",
];

/** Key interfaces whose signatures we compare in the type diff */
const KEY_INTERFACES = [
  "ExtensionAPI",
  "ExtensionContext",
  "ExtensionUIContext",
  "ExtensionCommandContext",
  "ExtensionCommandContextActions",
];

/** Timeouts (ms) */
const T_FAST       =  8_000;
const T_MEDIUM     = 30_000;
const T_DOWNLOAD   = 60_000;
const T_UPGRADE    = 120_000;
const T_CONFIRM    = 45_000;

/** Persistent backup directory — survives reboots, available for manual recovery */
const BACKUP_DIR = resolve(AGENT_DIR, ".upgrade-backups");

/** Maximum number of backup tarballs to retain */
const MAX_BACKUPS = 3;

/** Lock file to prevent concurrent upgrades */
const LOCK_FILE = resolve(AGENT_DIR, ".upgrade-lock");

/** Lock file max age before considered stale (10 min) */
const LOCK_MAX_AGE_MS = 600_000;

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface ReviewHistoryEntry {
  timestamp:      number;
  currentVersion: string;
  latestVersion:  string;
  overallStatus:  "SAFE" | "WARN" | "BREAKING" | "UP_TO_DATE" | "ERROR";
}

interface ReviewState {
  lastCheckTimestamp:  number;
  lastReviewedVersion: string | null;
  currentVersion:      string | null;
  reviewHistory:       ReviewHistoryEntry[];
  upgradeLog?:         UpgradeLog[];
}

interface UpgradeLog {
  timestamp:      number;
  phase:          "backup" | "update" | "sync" | "verify" | "rollback" | "cleanup";
  success:        boolean;
  error?:         string;
  versions:       { from: string; to: string; actual?: string };
  backupPath?:    string;
  tier?:          number;
}

interface TypeChange {
  api:      string;   // e.g. "ExtensionAPI.registerCommand"
  change:   string;   // description of what changed
  severity: "info" | "warn" | "breaking";
}

interface ExtResult {
  file:   string;           // basename of the extension file
  status: "SAFE" | "WARN" | "BREAKING";
  issues: string[];         // human-readable descriptions of problems
}

interface CompatReport {
  currentVersion:   string;
  latestVersion:    string;
  changelogDelta:   string;      // relevant changelog entries as text
  breakingChanges:  string[];    // entries/lines that matched breaking keywords
  typeChanges:      TypeChange[];
  extensionResults: ExtResult[];
  overallStatus:    "SAFE" | "WARN" | "BREAKING";
}

// ---------------------------------------------------------------------------
// Async exec helper
// ---------------------------------------------------------------------------

function run(
  cmd: string,
  timeout = T_MEDIUM,
  cwd?: string,
): Promise<{ stdout: string; stderr: string }> {
  return new Promise((res, rej) => {
    execCb(
      cmd,
      { cwd: cwd ?? AGENT_DIR, timeout, killSignal: "SIGKILL" },
      (err, stdout, stderr) => {
        if (err) {
          rej(new Error(`${cmd.slice(0, 80)}: ${stderr?.trim() || err.message}`));
        } else {
          res({ stdout: stdout?.trim() ?? "", stderr: stderr?.trim() ?? "" });
        }
      },
    );
  });
}

// ---------------------------------------------------------------------------
// Validation helpers (H1)
// ---------------------------------------------------------------------------

function validateSemver(version: string): string {
  if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(version)) {
    throw new Error(`Invalid version: ${version.slice(0, 40)}`);
  }
  return version;
}

function validateUrl(url: string): string {
  if (!/^https?:\/\/[^\s"'`$;|&]+$/.test(url)) {
    throw new Error(`Invalid URL: ${url.slice(0, 80)}`);
  }
  return url;
}

// ---------------------------------------------------------------------------
// Package install directory (C1)
// ---------------------------------------------------------------------------

async function getPkgInstallDir(): Promise<string> {
  if (PKG_INSTALL) return PKG_INSTALL;
  try {
    const { stdout } = await run("npm root -g", T_FAST, "/tmp");
    PKG_INSTALL = join(stdout.trim(), PKG_NAME);
    if (!existsSync(PKG_INSTALL)) {
      PKG_INSTALL = join(stdout.trim(), '@helios-agent/pi-coding-agent');
    }
    if (!existsSync(PKG_INSTALL)) {
      PKG_INSTALL = join(stdout.trim(), '@cgh567/cli');
    }
    return PKG_INSTALL;
  } catch (e) {
    process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
    const fallbacks = [
      `/opt/homebrew/lib/node_modules/${PKG_NAME}`,
      `/opt/homebrew/lib/node_modules/@helios-agent/pi-coding-agent`,
      `/opt/homebrew/lib/node_modules/@cgh567/cli`,
      `/usr/local/lib/node_modules/${PKG_NAME}`,
      `/usr/local/lib/node_modules/@helios-agent/pi-coding-agent`,
      `/usr/local/lib/node_modules/@cgh567/cli`,
      `/usr/lib/node_modules/${PKG_NAME}`,
    ];
    for (const p of fallbacks) {
      if (existsSync(p)) { PKG_INSTALL = p; return p; }
    }
    throw new Error("Cannot locate Pi installation directory");
  }
}

// ---------------------------------------------------------------------------
// State helpers
// ---------------------------------------------------------------------------

function readState(): ReviewState {
  try {
    const raw    = readFileSync(STATE_FILE, "utf8");
    const parsed = JSON.parse(raw) as Partial<ReviewState>;
    return {
      lastCheckTimestamp:  parsed.lastCheckTimestamp  ?? 0,
      lastReviewedVersion: parsed.lastReviewedVersion ?? null,
      currentVersion:      parsed.currentVersion      ?? null,
      reviewHistory:       Array.isArray(parsed.reviewHistory) ? parsed.reviewHistory : [],
    };
  } catch (e: any) {
    // ENOENT on first run is expected — state file doesn't exist yet, not an error.
    if (e?.code !== 'ENOENT') {
      process.stderr.write(`[upgrade-reviewer.ts] readState failed: ${String(e)}\n`);
    }
    return {
      lastCheckTimestamp:  0,
      lastReviewedVersion: null,
      currentVersion:      null,
      reviewHistory:       [],
    };
  }
}

function writeState(state: ReviewState): void {
  try {
    writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), "utf8");
  } catch (e) { process.stderr.write(`[extensions] best-effort — never crash Pi: ${String(e)}\n`); }
}

function logUpgradeEvent(entry: UpgradeLog): void {
  try {
    const state = readState();
    const log = state.upgradeLog ?? [];
    log.push(entry);
    // Keep last 50 entries
    if (log.length > 50) log.splice(0, log.length - 50);
    writeState({ ...state, upgradeLog: log });
  } catch (_) { /* fail-open */ }
}

function acquireUpgradeLock(): void {
  if (existsSync(LOCK_FILE)) {
    try {
      const age = Date.now() - statSync(LOCK_FILE).mtimeMs;
      if (age < LOCK_MAX_AGE_MS) {
        throw new Error(`Upgrade already in progress (lock age: ${Math.round(age / 1000)}s)`);
      }
      // Stale lock — remove it
      unlinkSync(LOCK_FILE);
    } catch (e) {
      if ((e as Error).message.includes("already in progress")) throw e;
      // Can't stat — try to proceed
    }
  }
  writeFileSync(LOCK_FILE, JSON.stringify({ pid: process.pid, ts: Date.now() }));
}

function releaseUpgradeLock(): void {
  try { unlinkSync(LOCK_FILE); } catch (_) { /* fail-open */ }
}

function safeCleanupBackup(tarballPath: string | null): void {
  if (!tarballPath) return;
  try {
    // If in persistent backup dir, don't delete (managed by pruneOldBackups)
    if (tarballPath.startsWith(BACKUP_DIR)) return;
    // If in temp dir, delete the parent UUID directory
    const parent = dirname(tarballPath);
    if (parent && parent !== tmpdir() && parent.startsWith(join(tmpdir(), "pi-rollback-"))) {
      rmSync(parent, { recursive: true, force: true });
    }
  } catch (_) { /* fail-open */ }
}

function pruneOldBackups(): void {
  try {
    if (!existsSync(BACKUP_DIR)) return;
    const files = readdirSync(BACKUP_DIR)
      .filter(f => f.endsWith(".tgz"))
      .map(f => ({ name: f, mtime: statSync(join(BACKUP_DIR, f)).mtimeMs }))
      .sort((a, b) => b.mtime - a.mtime);  // newest first
    for (const f of files.slice(MAX_BACKUPS)) {
      try { unlinkSync(join(BACKUP_DIR, f.name)); } catch (_) {}
    }
  } catch (_) { /* fail-open */ }
}

function validateTarball(tarballPath: string): boolean {
  try {
    const stat = statSync(tarballPath);
    if (stat.size < 1024) return false;  // too small
    const fd = openSync(tarballPath, "r");
    const hdr = Buffer.alloc(2);
    readSync(fd, hdr, 0, 2, 0);
    closeSync(fd);
    return hdr[0] === 0x1f && hdr[1] === 0x8b;  // gzip magic bytes
  } catch (_) {
    return false;
  }
}

function saveHistory(
  state:   ReviewState,
  entry:   ReviewHistoryEntry,
): ReviewState {
  return {
    ...state,
    reviewHistory: [...state.reviewHistory, entry].slice(-50),
  };
}

// ---------------------------------------------------------------------------
// Step 1 — Version Check
// ---------------------------------------------------------------------------

/**
 * Resolves a specific version string against the npm registry.
 * If the exact version doesn't exist, falls back to the actual latest.
 * Returns the best available version from the registry.
 */
async function resolveRegistryVersion(targetVersion?: string): Promise<string | null> {
  // Try 1: If a specific version was requested, check if it exists
  if (targetVersion) {
    try {
      const { stdout } = await run(`npm view ${PKG_NAME}@${validateSemver(targetVersion)} version`, T_FAST, "/tmp");
      const resolved = stdout.trim();
      if (resolved) return validateSemver(resolved);
    } catch (_) {
      // Version doesn't exist on registry — fall through to latest
      process.stderr.write(`[upgrade-reviewer.ts] version ${targetVersion} not found on registry, resolving latest…\n`);
    }
  }

  // Try 2: Get the actual latest version from registry
  try {
    const { stdout } = await run(`npm view ${PKG_NAME} version`, T_FAST, "/tmp");
    const latest = stdout.trim();
    if (latest) return validateSemver(latest);
  } catch (_) {}

  // Try 3: Get all versions and pick the highest
  try {
    const { stdout } = await run(`npm view ${PKG_NAME} versions --json`, T_MEDIUM, "/tmp");
    const parsed = JSON.parse(stdout);
    // npm returns a bare string (not array) when a package has exactly one version
    const vArr: string[] = Array.isArray(parsed) ? parsed : (typeof parsed === "string" ? [parsed] : []);
    if (vArr.length > 0) {
      // npm returns versions in ascending order — last is newest
      const newest = vArr[vArr.length - 1];
      return validateSemver(newest);
    }
  } catch (_) {}

  return null;
}

async function getVersions(): Promise<{ current: string; latest: string } | null> {
  try {
    const pkgDir = await getPkgInstallDir();

    // Get current installed version
    // BUG-2 fix: use process.stdout.write instead of process.stderr.write so the
    // version string can be read from stdout. Previously wrote to stderr but read
    // from stdout — always got empty string, causing validateSemver() to throw and
    // getVersions() to return null, permanently disabling the upgrade checker.
    let current: string;
    try {
      const { stdout } = await run(
        `node -e "process.stdout.write(require('${pkgDir}/package.json').version)"`,
        T_FAST,
      );
      current = validateSemver(stdout.trim());
    } catch {
      return null;
    }

    // Resolve latest from registry (resilient — tries multiple strategies)
    const latest = await resolveRegistryVersion();
    if (!latest) {
      process.stderr.write(`[upgrade-reviewer.ts] cannot determine latest version from registry\n`);
      return null;
    }

    return { current, latest };
  } catch (e) {
    process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
    return null;
  }
}

// ---------------------------------------------------------------------------
// Step 2 — Changelog Delta
// ---------------------------------------------------------------------------

/**
 * Fetches CHANGELOG.md from GitHub and slices the entries between `fromVer`
 * (exclusive) and `toVer` (inclusive).
 *
 * CHANGELOG format: ## [0.x.y] - YYYY-MM-DD
 */
async function fetchChangelogDelta(
  fromVer: string,
  toVer:   string,
): Promise<{ delta: string; breakingChanges: string[] }> {
  let rawChangelog = "";

  // Prefer the remote version (most up-to-date)
  try {
    const { stdout } = await run(
      `curl -fsSL --max-time 10 "${CHANGELOG_URL}"`,
      T_MEDIUM,
      "/tmp",
    );
    rawChangelog = stdout;
  } catch (e) {
    process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
    // Fall back to local
    try {
      const pkgDir = await getPkgInstallDir();
      rawChangelog = readFileSync(
        `${pkgDir}/CHANGELOG.md`,
        "utf8",
      );
    } catch (e) {
      process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
      return {
        delta:           "(could not fetch changelog)",
        breakingChanges: [],
      };
    }
  }

  // Split by version headings: ## [x.y.z]
  const lines = rawChangelog.split("\n");

  let capturing     = false;
  let reachedFrom   = false;
  const deltaLines: string[] = [];

  for (const line of lines) {
    const match = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
    if (match) {
      const ver = match[1]!;
      if (ver === toVer) {
        capturing = true;
      } else if (ver === fromVer) {
        reachedFrom = true;
        break; // stop — we don't want entries at or below fromVer
      } else if (capturing) {
        // Still between toVer and fromVer, keep capturing
      } else {
        // Above toVer — skip
        continue;
      }
    }
    if (capturing && !reachedFrom) {
      deltaLines.push(line);
    }
  }

  if (deltaLines.length === 0) {
    const toVerFound = rawChangelog.includes(`## [${toVer}]`);
    if (!toVerFound) {
      return {
        delta: `(version ${toVer} not found in changelog — may not be published yet)`,
        breakingChanges: [],
      };
    }
  }

  const delta = deltaLines.join("\n").trim() || "(no changelog entries found in range)";

  // Scan for breaking-change keywords
  const lowerDelta     = delta.toLowerCase();
  const breakingChanges: string[] = [];

  for (const keyword of BREAKING_KEYWORDS) {
    if (lowerDelta.includes(keyword.toLowerCase())) {
      // Collect the lines that contain the keyword
      for (const line of deltaLines) {
        if (line.toLowerCase().includes(keyword.toLowerCase())) {
          breakingChanges.push(line.trim());
        }
      }
    }
  }

  // Deduplicate
  const unique = Array.from(new Set(breakingChanges));
  return { delta, breakingChanges: unique };
}

// ---------------------------------------------------------------------------
// Step 3 — Type Diff Analysis
// ---------------------------------------------------------------------------

/**
 * Downloads the new version's tarball, extracts d.ts files, and compares
 * key interface signatures against the currently installed types.
 */
async function analyzeTypeDiff(
  latestVersion: string,
): Promise<TypeChange[]> {
  const changes: TypeChange[] = [];

  // Read current installed types
  const pkgDir = await getPkgInstallDir();
  const currentTypesPath = `${pkgDir}/dist/index.d.ts`;
  let currentTypes = "";
  try {
    currentTypes = readFileSync(currentTypesPath, "utf8");
  } catch (e) {
    process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
    return [{ api: "__diag__local_types", change: "Cannot read current types file", severity: "warn" }];
  }

  // Use a randomized temp directory to avoid collisions (H3)
  const unpackDir = join(tmpdir(), `pi-upgrade-review-${randomUUID()}`);

  try {
    try { mkdirSync(unpackDir, { recursive: true }); } catch (e) { process.stderr.write(`[extensions] fail-open: non-critical: ${String(e)}\n`); }

    const tarball = join(unpackDir, `pi-${latestVersion}.tgz`);

    // Get tarball URL
    let tarballUrl = "";
    try {
      const { stdout } = await run(
        `npm view ${PKG_NAME}@${latestVersion} dist.tarball`,
        T_FAST,
        "/tmp",
      );
      tarballUrl = validateUrl(stdout.trim()); // H1
    } catch (e) {
      process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
      return [{ api: "__diag__npm", change: "Cannot fetch tarball URL for new version", severity: "warn" }];
    }

    // Download
    try {
      await run(
        `curl -fsSL --max-time 30 -o "${tarball}" "${tarballUrl}"`,
        T_DOWNLOAD,
        "/tmp",
      );
    } catch (e) {
      process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
      return [{ api: "__diag__download", change: "Cannot download new version tarball", severity: "warn" }];
    }

    // H2 — Verify integrity via npm's built-in shasum
    try {
      const { stdout: pkgInfo } = await run(
        `npm view ${PKG_NAME}@${latestVersion} dist.integrity`,
        T_FAST,
        "/tmp",
      );
      const expectedIntegrity = pkgInfo.trim();
      if (expectedIntegrity) {
        // npm uses sha512 integrity
        const algo = expectedIntegrity.startsWith("sha512-") ? "sha512" : "sha256";
        const { stdout: actualHash } = await run(
          `openssl dgst -${algo} -binary "${tarball}" | openssl base64 -A`,
          T_FAST,
          "/tmp",
        );
        const actualIntegrity = `${algo}-${actualHash.trim()}`;
        if (actualIntegrity !== expectedIntegrity) {
          changes.push({ api: "__diag__integrity", change: "Tarball checksum mismatch — download may be corrupted", severity: "breaking" });
          return changes;
        }
      }
    } catch (e) {
      process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
      // Can't verify — proceed with warning
      changes.push({ api: "__diag__integrity", change: "Could not verify tarball integrity", severity: "warn" });
    }

    // Extract
    const extractDir = join(unpackDir, `package-${latestVersion}`);
    try { mkdirSync(extractDir, { recursive: true }); } catch (e) { process.stderr.write(`[extensions] fail-open: non-critical: ${String(e)}\n`); }
    try {
      await run(
        `tar -xzf "${tarball}" -C "${extractDir}" --strip-components=1`,
        T_MEDIUM,
        "/tmp",
      );
    } catch (e) {
      process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
      return [{ api: "__diag__extract", change: "Cannot extract tarball", severity: "warn" }];
    }

    // Read new types
    const newTypesPath = join(extractDir, "dist/index.d.ts");
    let newTypes = "";
    try {
      newTypes = readFileSync(newTypesPath, "utf8");
    } catch (e) {
      process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
      return [{ api: "__diag__new_types", change: "Cannot read new version's types file", severity: "warn" }];
    }

    // Compare key interfaces by extracting exported type names from the
    // `export type { ... }` blocks in index.d.ts
    const currentExports = extractExportedTypes(currentTypes);
    const newExports      = extractExportedTypes(newTypes);

    // Find removed exports
    for (const name of currentExports) {
      if (!newExports.has(name)) {
        changes.push({
          api:      name,
          change:   `Removed from public API exports`,
          severity: "breaking",
        });
      }
    }

    // Find added exports (informational)
    for (const name of newExports) {
      if (!currentExports.has(name)) {
        changes.push({
          api:      name,
          change:   `New export added`,
          severity: "info",
        });
      }
    }

    // Deep compare: find actual .d.ts files with interface bodies (C2)
    const MAX_DTS_FILES = 200;
    const findDtsFiles = (dir: string, depth = 0): string[] => {
      if (depth > 3) return [];
      const results: string[] = [];
      try {
        const entries = readdirSync(dir, { withFileTypes: true });
        for (const e of entries) {
          if (e.name === "node_modules" || e.name.startsWith(".")) continue;
          const full = join(dir, e.name);
          if (e.isDirectory()) results.push(...findDtsFiles(full, depth + 1));
          else if (e.name.endsWith(".d.ts")) results.push(full);
          if (results.length >= MAX_DTS_FILES) break;
        }
      } catch (e) { process.stderr.write(`[extensions] fail-open: non-critical: ${String(e)}\n`); }
      return results.slice(0, MAX_DTS_FILES);
    };

    const currentDtsFiles = findDtsFiles(await getPkgInstallDir());
    const newDtsFiles = findDtsFiles(extractDir);

    const currentAllDts = currentDtsFiles.map(f => { try { return readFileSync(f, "utf8"); } catch (e) { process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`); return ""; } }).join("\n");
    const newAllDts = newDtsFiles.map(f => { try { return readFileSync(f, "utf8"); } catch (e) { process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`); return ""; } }).join("\n");

    for (const iface of KEY_INTERFACES) {
      const currentMethods = extractInterfaceMethods(currentAllDts, iface);
      const newMethods = extractInterfaceMethods(newAllDts, iface);

      // Find removed methods
      for (const method of currentMethods) {
        if (!newMethods.has(method)) {
          changes.push({
            api:      `${iface}.${method}`,
            change:   `Method/property removed from ${iface}`,
            severity: "breaking",
          });
        }
      }

      // Find added methods (informational)
      for (const method of newMethods) {
        if (!currentMethods.has(method)) {
          changes.push({
            api:      `${iface}.${method}`,
            change:   `New method/property added to ${iface}`,
            severity: "info",
          });
        }
      }
    }

    // H5 — Full cleanup of temp dir handled by finally block

  } catch (e) {
    process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
    changes.push({ api: "__diag__type-diff", change: "Type diff analysis failed unexpectedly", severity: "warn" });
  } finally {
    try { await run(`rm -rf "${unpackDir}"`, T_FAST, "/tmp"); } catch (e) { process.stderr.write(`[extensions] fail-open: non-critical: ${String(e)}\n`); }
  }

  return changes;
}

/**
 * Extracts the set of exported type/interface/class/function names from a .d.ts file.
 * Covers both direct declarations and barrel re-exports.
 */
function extractExportedTypes(dtsContent: string): Set<string> {
  const names = new Set<string>();
  const declRegex = /export\s+(?:declare\s+)?(?:interface|type|function|class|const|let|var|enum)\s+(\w+)/g;
  let match: RegExpExecArray | null;
  while ((match = declRegex.exec(dtsContent)) !== null) {
    if (match[1] && /^[A-Za-z]/.test(match[1])) names.add(match[1]);
  }
  const barrelRegex = /export\s+(?:type\s+)?\{([^}]+)\}/g;
  while ((match = barrelRegex.exec(dtsContent)) !== null) {
    const inner = match[1] ?? "";
    for (const part of inner.split(",")) {
      const aliasParts = part.trim().split(/\s+as\s+/);
      const rawName = (aliasParts.length > 1 ? aliasParts[aliasParts.length - 1] : aliasParts[0])?.trim();
      const name = rawName?.split(/\s+/).pop();
      if (name && /^[A-Za-z]/.test(name)) names.add(name);
    }
  }
  return names;
}

/**
 * Extracts method/property names from a TypeScript interface definition.
 * Works on .d.ts files where interfaces are fully declared (not just re-exported).
 * Falls back to export-name comparison if interfaces aren't found in index.d.ts.
 *
 * NOTE: Only finds the FIRST declaration. Declaration merging across files is missed.
 */
function extractInterfaceMethods(dtsContent: string, ifaceName: string): Set<string> {
  const methods = new Set<string>();

  // Try to find the interface block: interface IFaceName { ... }
  // Handle both direct and extended interfaces
  const ifaceRegex = new RegExp(
    `(?:export\\s+)?interface\\s+${ifaceName}\\s*(?:extends\\s+[^{]+)?\\{`,
    "m"
  );
  const match = ifaceRegex.exec(dtsContent);
  if (!match) return methods;

  // Find the matching closing brace by counting depth
  let depth = 1;
  let pos = match.index + match[0].length;

  while (pos < dtsContent.length && depth > 0) {
    if (dtsContent[pos] === "{") depth++;
    else if (dtsContent[pos] === "}") depth--;
    pos++;
  }

  const body = dtsContent.slice(match.index + match[0].length, pos - 1);

  // Extract method and property names from the interface body
  // Matches patterns like: methodName(, propertyName:, propertyName?, readonly propertyName
  const memberRegex = /(?:readonly\s+)?(\w+)\s*[\(?:<:]/g;
  let memberMatch: RegExpExecArray | null;
  while ((memberMatch = memberRegex.exec(body)) !== null) {
    const name = memberMatch[1];
    if (name && !["undefined", "null", "void", "never", "any", "string", "number", "boolean"].includes(name)) {
      methods.add(name);
    }
  }

  return methods;
}

// ---------------------------------------------------------------------------
// Step 4 — Extension Compatibility Scan
// ---------------------------------------------------------------------------

/**
 * Recursively finds all .ts extension files, excluding test files,
 * node_modules, lib, and disabled files.
 */
function findExtensionFiles(dir: string, depth = 0): string[] {
  if (depth > 2) return []; // don't recurse too deep
  const results: string[] = [];
  try {
    const entries = readdirSync(dir, { withFileTypes: true });
    for (const entry of entries) {
      const fullPath = join(dir, entry.name);
      if (entry.isDirectory()) {
        // Skip node_modules, lib, and hidden dirs
        if (entry.name === "node_modules" || entry.name === "lib" || entry.name.startsWith(".")) continue;
        results.push(...findExtensionFiles(fullPath, depth + 1));
      } else if (
        entry.name.endsWith(".ts") &&
        !entry.name.endsWith(".test.ts") &&
        !entry.name.endsWith(".d.ts") &&
        !entry.name.endsWith(".disabled")
      ) {
        results.push(fullPath);
      }
    }
  } catch (e) { process.stderr.write(`[extensions] skip unreadable dirs: ${String(e)}\n`); }
  return results;
}

/**
 * Scans all .ts extension files for pi.* and ctx.* API calls.
 * Cross-references against removed/changed APIs from the type diff.
 */
function scanExtensions(typeChanges: TypeChange[]): ExtResult[] {
  const results: ExtResult[] = [];

  // Build method-name lookup from type changes (C3)
  const breakingMethodNames = new Set<string>();
  const warnMethodNames = new Set<string>();
  for (const tc of typeChanges) {
    const parts = tc.api.split(".");
    const methodName = parts.length > 1 ? parts[parts.length - 1] : tc.api;
    if (tc.severity === "breaking") breakingMethodNames.add(methodName);
    else if (tc.severity === "warn") warnMethodNames.add(methodName);
  }

  // Also keep full API names for the heuristic import check
  const breakingApis = new Set(
    typeChanges
      .filter(tc => tc.severity === "breaking")
      .map(tc => tc.api),
  );
  const warnApis = new Set(
    typeChanges
      .filter(tc => tc.severity === "warn")
      .map(tc => tc.api),
  );

  let files: string[] = [];
  try {
    files = findExtensionFiles(EXTENSIONS_DIR);
  } catch (e) {
    process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
    return [];
  }

  for (const filepath of files) {
    const basename = filepath.replace(EXTENSIONS_DIR + "/", "");
    let source = "";
    try {
      source = readFileSync(filepath, "utf8");
    } catch (e) {
      process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
      results.push({ file: basename, status: "WARN", issues: ["Cannot read file"] });
      continue;
    }

    const issues: string[] = [];
    let worstStatus: "SAFE" | "WARN" | "BREAKING" = "SAFE";

    // Extract pi.* and ctx.* call patterns
    const apiCallRegex = /(?:pi|ctx)\.([\w]+)/g;
    const usedApis     = new Set<string>();
    let apiMatch: RegExpExecArray | null;
    while ((apiMatch = apiCallRegex.exec(source)) !== null) {
      usedApis.add(apiMatch[1] ?? "");
    }

    // Cross-reference extracted method names against breaking/warn method-level names (C3)
    const alreadyFlagged = new Set<string>();
    for (const usedApi of usedApis) {
      if (breakingMethodNames.has(usedApi)) {
        issues.push(`BREAKING: uses \`${usedApi}\` — API removed/renamed in new version`);
        worstStatus = "BREAKING";
        alreadyFlagged.add(usedApi);
      } else if (warnMethodNames.has(usedApi)) {
        if (worstStatus !== "BREAKING") worstStatus = "WARN";
        issues.push(`WARN: uses \`${usedApi}\` — API signature changed`);
        alreadyFlagged.add(usedApi);
      } else {
        // Fallback: check against full API names (type-level names for removed exports etc.)
        for (const breaking of breakingApis) {
          if (
            breaking === usedApi ||
            breaking.endsWith(`.${usedApi}`) ||
            breaking.startsWith(`${usedApi}.`)
          ) {
            issues.push(`BREAKING: uses \`${usedApi}\` — API removed/renamed in new version`);
            worstStatus = "BREAKING";
          }
        }
        for (const warn of warnApis) {
          if (
            warn === usedApi ||
            warn.endsWith(`.${usedApi}`) ||
            warn.startsWith(`${usedApi}.`)
          ) {
            if (worstStatus !== "BREAKING") worstStatus = "WARN";
            issues.push(`WARN: uses \`${usedApi}\` — API signature changed`);
          }
        }
      }
    }

    // Additional heuristic: look for direct imports of types that changed
    for (const tc of typeChanges.filter(t => t.severity !== "info")) {
      const methodName = tc.api.split(".").pop() ?? tc.api;
      if (alreadyFlagged.has(methodName)) continue;
      const name = tc.api.split(".").pop() ?? tc.api;
      if (
        source.includes(`"${name}"`) ||
        source.includes(`'${name}'`) ||
        source.includes(`: ${name}`) ||
        source.includes(`<${name}>`) ||
        source.includes(`${name},`) ||
        source.includes(`, ${name}`)
      ) {
        const severity: "SAFE" | "WARN" | "BREAKING" =
          tc.severity === "breaking" ? "BREAKING" : "WARN";
        if (
          (severity === "BREAKING" && worstStatus !== "BREAKING") ||
          (severity === "WARN" && worstStatus === "SAFE")
        ) {
          worstStatus = severity;
        }
        const tag = severity === "BREAKING" ? "BREAKING" : "WARN";
        const msg = `${tag}: references \`${name}\` — ${tc.change}`;
        if (!issues.includes(msg)) issues.push(msg);
      }
    }

    results.push({ file: basename, status: worstStatus, issues });
  }

  return results;
}

// ---------------------------------------------------------------------------
// Step 5 — Build Report
// ---------------------------------------------------------------------------

function buildReport(
  versions:        { current: string; latest: string },
  changelogResult: { delta: string; breakingChanges: string[] },
  typeChanges:     TypeChange[],
  extResults:      ExtResult[],
): CompatReport {
  // Aggregate overall status
  let overallStatus: "SAFE" | "WARN" | "BREAKING" = "SAFE";

  if (changelogResult.breakingChanges.length > 0) overallStatus = "WARN";

  const hasTypeBreaking = typeChanges.some(tc => tc.severity === "breaking");
  const hasTypeWarn     = typeChanges.some(tc => tc.severity === "warn");
  if (hasTypeBreaking) overallStatus = "BREAKING";
  else if (hasTypeWarn && overallStatus === "SAFE") overallStatus = "WARN";

  const hasExtBreaking = extResults.some(e => e.status === "BREAKING");
  const hasExtWarn     = extResults.some(e => e.status === "WARN");
  if (hasExtBreaking) overallStatus = "BREAKING";
  else if (hasExtWarn && overallStatus === "SAFE") overallStatus = "WARN";

  return {
    currentVersion:   versions.current,
    latestVersion:    versions.latest,
    changelogDelta:   changelogResult.delta,
    breakingChanges:  changelogResult.breakingChanges,
    typeChanges:      typeChanges.filter(tc => tc.severity !== "info" && !tc.api.startsWith("__diag__")),
    extensionResults: extResults,
    overallStatus,
  };
}

// ---------------------------------------------------------------------------
// Step 6 — Format Report for Display
// ---------------------------------------------------------------------------

function formatReport(report: CompatReport): string {
  const safeCount     = report.extensionResults.filter(e => e.status === "SAFE").length;
  const warnCount     = report.extensionResults.filter(e => e.status === "WARN").length;
  const breakCount    = report.extensionResults.filter(e => e.status === "BREAKING").length;
  const typeWarnCount = report.typeChanges.filter(tc => tc.severity === "warn").length;
  const typeBreakCount= report.typeChanges.filter(tc => tc.severity === "breaking").length;

  const statusIcon =
    report.overallStatus === "SAFE"     ? "✅" :
    report.overallStatus === "WARN"     ? "⚠️" : "🚨";

  const lines: string[] = [
    `📋 Pi Upgrade Review: v${report.currentVersion} → v${report.latestVersion}`,
    "",
    `Changelog: ${report.breakingChanges.length > 0
      ? `${report.breakingChanges.length} potential breaking note(s) detected`
      : "no breaking change keywords found"}`,
    `Type changes: ${typeBreakCount > 0
      ? `${typeBreakCount} breaking, ${typeWarnCount} warnings`
      : typeWarnCount > 0
        ? `${typeWarnCount} warnings`
        : "none detected"}`,
    `Extensions: ${safeCount} SAFE, ${warnCount} WARN, ${breakCount} BREAKING`,
    "",
    `Status: ${statusIcon} ${report.overallStatus}${
      report.overallStatus === "SAFE" ? " — auto-upgrading..." :
      report.overallStatus === "WARN" ? " — confirmation required" :
      " — review required before upgrading"
    }`,
  ];

  // If WARN or BREAKING, append details
  if (report.overallStatus !== "SAFE") {
    if (report.breakingChanges.length > 0) {
      lines.push("", "── Changelog warnings ──");
      for (const bc of report.breakingChanges.slice(0, 10)) {
        lines.push(`  • ${bc}`);
      }
    }

    const significantTypeChanges = report.typeChanges.filter(tc => tc.severity !== "info");
    if (significantTypeChanges.length > 0) {
      lines.push("", "── Type API changes ──");
      for (const tc of significantTypeChanges.slice(0, 10)) {
        const icon = tc.severity === "breaking" ? "🚨" : "⚠️";
        lines.push(`  ${icon} ${tc.api}: ${tc.change}`);
      }
    }

    const affectedExts = report.extensionResults.filter(e => e.status !== "SAFE");
    if (affectedExts.length > 0) {
      lines.push("", "── Affected extensions ──");
      for (const ext of affectedExts) {
        const icon = ext.status === "BREAKING" ? "🚨" : "⚠️";
        lines.push(`  ${icon} ${ext.file}`);
        for (const issue of ext.issues.slice(0, 3)) {
          lines.push(`       ${issue}`);
        }
      }
    }
  }

  return lines.join("\n");
}

// ---------------------------------------------------------------------------
// Rollback Backup (tarball-safe, persistent)
// ---------------------------------------------------------------------------

/**
 * Creates a tarball backup of the current installation.
 * Stored in ~/helios-agent/.upgrade-backups/ so it survives reboots.
 * Falls back to tmpdir() if persistent dir can't be created.
 */
async function createRollbackBackup(pkgDir: string, version: string): Promise<string | null> {
  let backupLocation: string;
  let isPersistent = false;

  try {
    mkdirSync(BACKUP_DIR, { recursive: true });
    backupLocation = BACKUP_DIR;
    isPersistent = true;
  } catch (_) {
    // Fallback to temp dir
    backupLocation = join(tmpdir(), `pi-rollback-${randomUUID()}`);
    mkdirSync(backupLocation, { recursive: true });
  }

  try {
    const { stdout } = await run(`npm pack "${pkgDir}"`, T_MEDIUM, backupLocation);
    const tarballName = stdout.trim().split("\n").pop() ?? "";
    if (!tarballName || tarballName.includes("..") || !tarballName.endsWith(".tgz")) {
      process.stderr.write(`[upgrade-reviewer.ts] npm pack returned unexpected output: "${tarballName}" (non-fatal)\n`);
      logUpgradeEvent({ timestamp: Date.now(), phase: "backup", success: false, error: `bad tarball name: ${tarballName}`, versions: { from: version, to: "" } });
      if (!isPersistent) { try { rmSync(backupLocation, { recursive: true, force: true }); } catch (_) {} }
      return null;
    }

    const tarballPath = join(backupLocation, tarballName);

    // If persistent, rename to include version for clarity
    if (isPersistent) {
      const versionedName = `pi-${version}.tgz`;
      const versionedPath = join(backupLocation, versionedName);
      try {
        renameSync(tarballPath, versionedPath);
        if (validateTarball(versionedPath)) {
          pruneOldBackups();
          logUpgradeEvent({ timestamp: Date.now(), phase: "backup", success: true, versions: { from: version, to: "" }, backupPath: versionedPath });
          return versionedPath;
        } else {
          // Rename succeeded but validation failed — clean up orphan
          try { unlinkSync(versionedPath); } catch (_) {}
        }
      } catch (_) {
        // Rename failed, use original path
        if (existsSync(tarballPath) && validateTarball(tarballPath)) {
          pruneOldBackups();
          logUpgradeEvent({ timestamp: Date.now(), phase: "backup", success: true, versions: { from: version, to: "" }, backupPath: tarballPath });
          return tarballPath;
        }
      }
    }

    // Non-persistent or persistent rename failed
    if (existsSync(tarballPath) && validateTarball(tarballPath)) {
      logUpgradeEvent({ timestamp: Date.now(), phase: "backup", success: true, versions: { from: version, to: "" }, backupPath: tarballPath });
      return tarballPath;
    }

    logUpgradeEvent({ timestamp: Date.now(), phase: "backup", success: false, error: "tarball validation failed", versions: { from: version, to: "" } });
    if (!isPersistent) { try { rmSync(backupLocation, { recursive: true, force: true }); } catch (_) {} }
    return null;
  } catch (e) {
    process.stderr.write(`[upgrade-reviewer.ts] backup tarball failed (non-fatal): ${String(e)}\n`);
    logUpgradeEvent({ timestamp: Date.now(), phase: "backup", success: false, error: String(e), versions: { from: version, to: "" } });
    if (!isPersistent) { try { rmSync(backupLocation, { recursive: true, force: true }); } catch (_) {} }
    return null;
  }
}

/**
 * Multi-tier rollback: tarball → registry exact version → last known-good → latest.
 * Returns the version that was restored, or null if all tiers failed.
 */
async function rollbackWithRetry(
  backupTarball: string | null,
  rollbackVersion: string,
  state: ReviewState,
  ui: ExtensionContext["ui"],
  versions: { from: string; to: string },
): Promise<{ restoredVersion: string | null; tier: number }> {
  const tiers: Array<{ label: string; install: () => Promise<void> }> = [];

  // Tier 1: Local tarball backup
  if (backupTarball && existsSync(backupTarball) && validateTarball(backupTarball)) {
    tiers.push({
      label: `local backup tarball`,
      install: () => run(`npm install -g "${backupTarball}"`, T_UPGRADE, "/tmp").then(() => {}),
    });
  }

  // Tier 2: npm registry exact version
  tiers.push({
    label: `npm registry @${rollbackVersion}`,
    install: () => run(`npm install -g ${PKG_NAME}@${rollbackVersion}`, T_UPGRADE, "/tmp").then(() => {}),
  });

  // Tier 3: Last known-good version from state (semver-validated to prevent injection)
  if (state.lastReviewedVersion && state.lastReviewedVersion !== rollbackVersion) {
    try {
      const safeVersion = validateSemver(state.lastReviewedVersion);
      tiers.push({
        label: `last known-good @${safeVersion}`,
        install: () => run(`npm install -g ${PKG_NAME}@${safeVersion}`, T_UPGRADE, "/tmp").then(() => {}),
      });
    } catch (_) {
      process.stderr.write(`[upgrade-reviewer.ts] skipping tier 3: invalid lastReviewedVersion in state: ${String(state.lastReviewedVersion).slice(0, 40)}\n`);
    }
  }

  // Tier 4: Latest from registry
  tiers.push({
    label: `npm registry @latest`,
    install: () => run(`npm install -g ${PKG_NAME}@latest`, T_UPGRADE, "/tmp").then(() => {}),
  });

  for (let i = 0; i < tiers.length; i++) {
    const tier = tiers[i];
    try {
      ui.setWorkingMessage(`Rolling back (tier ${i + 1}/${tiers.length}: ${tier.label})…`);
      await tier.install();
      // Verify the rollback actually installed something
      const pkgDir = await getPkgInstallDir();
      const { stdout } = await run(
        `node -e "process.stderr.write(require('${pkgDir}/package.json').version)"`,
        T_FAST,
      );
      const restored = stdout.trim();
      logUpgradeEvent({ timestamp: Date.now(), phase: "rollback", success: true, versions, tier: i + 1 });
      return { restoredVersion: restored, tier: i + 1 };
    } catch (e) {
      process.stderr.write(`[upgrade-reviewer.ts] rollback tier ${i + 1} (${tier.label}) failed: ${String(e)}\n`);
      logUpgradeEvent({ timestamp: Date.now(), phase: "rollback", success: false, error: String(e), versions, tier: i + 1 });
    }
  }

  return { restoredVersion: null, tier: -1 };
}

// ---------------------------------------------------------------------------
// Upgrade Execution
// ---------------------------------------------------------------------------

async function executeUpgrade(
  versions: { current: string; latest: string },
  ui:       ExtensionContext["ui"],
): Promise<boolean> {
  const state = readState();
  const rollbackVersion = versions.current;
  let versionInfo = { from: versions.current, to: versions.latest };

  // ── Lock: prevent concurrent upgrades ────────────────────────────────────
  try {
    acquireUpgradeLock();
  } catch (e) {
    ui.notify(`⚠️ ${(e as Error).message}`, "warning");
    return false;
  }

  try {
    // ── Registry pre-check: verify target exists, auto-resolve if not ──────
    ui.setWorkingMessage("Verifying target version on registry…");
    let resolvedTarget = versions.latest;
    try {
      const resolved = await resolveRegistryVersion(versions.latest);
      if (!resolved) {
        ui.setWorkingMessage(undefined);
        ui.notify(
          `⚠️ Cannot reach npm registry to verify target version. Skipping upgrade.`,
          "warning",
        );
        logUpgradeEvent({ timestamp: Date.now(), phase: "verify", success: false, error: "registry unreachable", versions: versionInfo });
        return false;
      }
      if (resolved !== versions.latest) {
        // Target version didn't exist — auto-resolved to the actual latest
        process.stderr.write(`[upgrade-reviewer.ts] target v${versions.latest} not on registry, resolved to v${resolved}\n`);
        resolvedTarget = resolved;
        versionInfo = { from: versions.current, to: resolvedTarget };
        ui.notify(
          `ℹ️ v${versions.latest} not found on registry. Upgrading to v${resolvedTarget} instead.`,
          "info",
        );
      }
    } catch (e) {
      ui.setWorkingMessage(undefined);
      ui.notify(
        `⚠️ Cannot verify target version on registry: ${(e as Error).message}. Skipping upgrade.`,
        "warning",
      );
      logUpgradeEvent({ timestamp: Date.now(), phase: "verify", success: false, error: String(e), versions: versionInfo });
      return false;
    }

    // ── Backup: create tarball of current installation ─────────────────────
    ui.setWorkingMessage("Creating backup of current installation…");
    const backupTarball = await createRollbackBackup(await getPkgInstallDir(), rollbackVersion);

    // Save rollback point — do NOT set lastReviewedVersion until upgrade succeeds (H4)
    writeState({
      ...state,
      currentVersion:      versions.current,
      lastCheckTimestamp:  state.lastCheckTimestamp,
      reviewHistory:       state.reviewHistory,
    });

    // ── Step 1: npm install target version ───────────────────────────────
    ui.setWorkingMessage(`Upgrading ${PKG_NAME} to v${resolvedTarget}…`);
    try {
      await run(`npm install -g ${PKG_NAME}@${resolvedTarget}`, T_UPGRADE, "/tmp");
      logUpgradeEvent({ timestamp: Date.now(), phase: "update", success: true, versions: versionInfo, backupPath: backupTarball ?? undefined });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      logUpgradeEvent({ timestamp: Date.now(), phase: "update", success: false, error: msg, versions: versionInfo });

      // Check if partial update occurred
      let postFailVersion = rollbackVersion;
      try {
        const pkgDir = await getPkgInstallDir();
        const { stdout } = await run(
          `node -e "process.stderr.write(require('${pkgDir}/package.json').version)"`,
          T_FAST,
        );
        postFailVersion = stdout.trim();
      } catch (_) {}

      if (postFailVersion !== rollbackVersion) {
        // Partial update detected — rollback
        ui.setWorkingMessage("Partial update detected, rolling back…");
        const result = await rollbackWithRetry(backupTarball, rollbackVersion, state, ui, versionInfo);
        safeCleanupBackup(backupTarball);
        ui.setWorkingMessage(undefined);
        if (result.restoredVersion) {
          ui.notify(
            `⚠️ npm update failed and partial update was detected.\n⬅️ Rolled back to v${result.restoredVersion} (tier ${result.tier}).`,
            "warning",
          );
        } else {
          ui.notify(
            `❌ npm update failed and rollback failed.\nManual fix: npm install -g ${PKG_NAME}@${rollbackVersion}`,
            "error",
          );
        }
      } else {
        ui.setWorkingMessage(undefined);
        safeCleanupBackup(backupTarball);
        ui.notify(
          `❌ npm update failed: ${msg}\n\nManual recovery:\n  npm install -g ${PKG_NAME}@${rollbackVersion}`,
          "error",
        );
      }
      return false;
    }

    // ── Step 2: pi update (package sync) ───────────────────────────────────
    ui.setWorkingMessage("Running pi update for package sync…");
    try {
      await run("pi update", T_UPGRADE, AGENT_DIR);
      logUpgradeEvent({ timestamp: Date.now(), phase: "sync", success: true, versions: versionInfo });
    } catch (e) {
      process.stderr.write(`[upgrade-reviewer.ts] pi update failed (non-fatal): ${String(e)}\n`);
      logUpgradeEvent({ timestamp: Date.now(), phase: "sync", success: false, error: String(e), versions: versionInfo });
    }

    // ── Step 3: Verify ───────────────────────────────────────────────────────
    ui.setWorkingMessage("Verifying upgrade…");
    let newVersion = "";
    try {
      const pkgDir = await getPkgInstallDir();
      const { stdout } = await run(
        `node -e "process.stderr.write(require('${pkgDir}/package.json').version)"`,
        T_FAST,
      );
      newVersion = stdout.trim();
    } catch (e) {
      process.stderr.write(`[upgrade-reviewer.ts] version check failed: ${String(e)}\n`);
      logUpgradeEvent({ timestamp: Date.now(), phase: "verify", success: false, error: String(e), versions: versionInfo });
      ui.setWorkingMessage(undefined);
      ui.notify(
        `⚠️ Upgrade ran but could not verify new version. Expected v${versions.latest}.`,
        "warning",
      );
      return true;
    }

    ui.setWorkingMessage(undefined);

    if (newVersion !== resolvedTarget) {
      // Version mismatch — multi-tier rollback
      ui.notify(`⚠️ Version mismatch (got ${newVersion}, expected ${resolvedTarget}). Rolling back…`, "warning");
      const result = await rollbackWithRetry(backupTarball, rollbackVersion, state, ui, versionInfo);
      safeCleanupBackup(backupTarball);
      ui.setWorkingMessage(undefined);

      if (result.restoredVersion) {
        ui.notify(`⬅️ Rolled back to v${result.restoredVersion} (tier ${result.tier}).`, "info");
      } else {
        ui.notify(
          `❌ All rollback tiers failed. Manual fix:\n  npm install -g ${PKG_NAME}@${rollbackVersion}`,
          "error",
        );
      }
      return false;
    }

    // ── Step 4: Success — update state ─────────────────────────────────────
    writeState({
      ...readState(),
      currentVersion:      newVersion,
      lastCheckTimestamp:  Date.now(),
      lastReviewedVersion: newVersion,
    });

    logUpgradeEvent({ timestamp: Date.now(), phase: "verify", success: true, versions: { ...versionInfo, actual: newVersion } });

    // Clean up backup (persistent backups stay; only temp ones are cleaned)
    safeCleanupBackup(backupTarball);
    logUpgradeEvent({ timestamp: Date.now(), phase: "cleanup", success: true, versions: versionInfo });

    ui.notify(
      `✅ Upgraded to v${newVersion}. Restart session for changes to take effect.`,
      "info",
    );
    return true;
  } finally {
    releaseUpgradeLock();
  }
}

// ---------------------------------------------------------------------------
// Core Review Pipeline
// ---------------------------------------------------------------------------

async function runReviewPipeline(
  ui:    ExtensionContext["ui"],
  force: boolean,
): Promise<void> {
  // ── Step 1: Version Check ──────────────────────────────────────────────────
  ui.setWorkingMessage("Checking Pi version…");
  let versions: { current: string; latest: string } | null = null;
  try {
    versions = await getVersions();
  } finally {
    ui.setWorkingMessage(undefined);
  }

  if (!versions) {
    if (force) ui.notify("⚠️ Could not determine Pi versions. Check npm connectivity.", "warning");
    return;
  }

  const state = readState();
  writeState({ ...state, currentVersion: versions.current, lastCheckTimestamp: Date.now() });

  // Resolve the actual latest version from registry (handles wrong/phantom versions)
  const resolvedLatest = await resolveRegistryVersion(versions.latest);
  if (resolvedLatest && resolvedLatest !== versions.latest) {
    process.stderr.write(`[upgrade-reviewer.ts] resolved v${versions.latest} → v${resolvedLatest} from registry\n`);
    versions = { current: versions.current, latest: resolvedLatest };
  }

  if (versions.current === versions.latest) {
    if (force) ui.notify(`✅ Already up to date (v${versions.current})`, "info");
    const updated = saveHistory(readState(), {
      timestamp:      Date.now(),
      currentVersion: versions.current,
      latestVersion:  versions.latest,
      overallStatus:  "UP_TO_DATE",
    });
    writeState(updated);
    return;
  }

  ui.notify(
    `🔍 Pi update available: v${versions.current} → v${versions.latest}. Analyzing…`,
    "info",
  );

  // ── Step 2: Changelog Delta ─────────────────────────────────────────────────
  ui.setWorkingMessage("Fetching changelog…");
  let changelogResult: { delta: string; breakingChanges: string[] } = { delta: "", breakingChanges: [] };
  try {
    changelogResult = await fetchChangelogDelta(versions.current, versions.latest);
  } finally {
    ui.setWorkingMessage(undefined);
  }

  // ── Step 3: Type Diff ───────────────────────────────────────────────────────
  ui.setWorkingMessage("Analyzing type changes…");
  let typeChanges: TypeChange[] = [];
  try {
    typeChanges = await analyzeTypeDiff(versions.latest);
  } finally {
    ui.setWorkingMessage(undefined);
  }

    // Hard-abort: integrity/download failures mean analysis is unreliable
    const integrityFailure = typeChanges.find(
      tc => tc.api === "__diag__integrity" && tc.severity === "breaking"
    );
    if (integrityFailure) {
      ui.notify(
        `🚨 Tarball integrity check failed: ${integrityFailure.change}\n` +
        `Cannot verify API compatibility. Upgrade aborted.\n` +
        `Try again later or run: npm install -g ${PKG_NAME}@${versions.latest}`,
        "error",
      );
      const withHistory = saveHistory(readState(), {
        timestamp: Date.now(),
        currentVersion: versions.current,
        latestVersion: versions.latest,
        overallStatus: "ERROR",
      });
      writeState(withHistory);
      return;
    }

  // ── Step 4: Extension Scan ──────────────────────────────────────────────────
  ui.setWorkingMessage("Scanning extensions for compatibility…");
  let extResults: ExtResult[] = [];
  try {
    extResults = scanExtensions(typeChanges.filter(tc => !tc.api.startsWith("__diag__")));
  } finally {
    ui.setWorkingMessage(undefined);
  }

  // ── Step 5: Build & Display Report ─────────────────────────────────────────
  const report = buildReport(versions, changelogResult, typeChanges, extResults);
  const reportText = formatReport(report);

  // Save to history
  const withHistory = saveHistory(readState(), {
    timestamp:      Date.now(),
    currentVersion: versions.current,
    latestVersion:  versions.latest,
    overallStatus:  report.overallStatus,
  });
  writeState(withHistory);

  // ── Step 6: Action ──────────────────────────────────────────────────────────
  if (report.overallStatus === "SAFE") {
    // Auto-upgrade with brief summary
    ui.notify(reportText, "info");
    await executeUpgrade(versions, ui);
    return;
  }

  // WARN or BREAKING: show report and ask for confirmation
  const confirmMsg = [
    reportText,
    "",
    report.overallStatus === "BREAKING"
      ? "⚠️ Breaking changes detected. Review the above and confirm to upgrade."
      : "ℹ️ Some warnings detected. Review the above and confirm to upgrade.",
    "",
    `Upgrade from v${versions.current} to v${versions.latest}?`,
  ].join("\n");

  let accepted = false;
  try {
    accepted = await ui.confirm("Pi Upgrade Review", confirmMsg, { timeout: T_CONFIRM });
  } catch (e) {
    process.stderr.write(`[upgrade-reviewer.ts] operation failed: ${String(e)}\n`);
    // Non-interactive — skip
    return;
  }

  if (!accepted) {
    ui.notify("Upgrade cancelled. Run /upgrade-review --force anytime to retry.", "info");
    return;
  }

  await executeUpgrade(versions, ui);
}

// ---------------------------------------------------------------------------
// Extension Entry Point
// ---------------------------------------------------------------------------

export default function upgradeReviewerExtension(pi: ExtensionAPI): void {

  // ── session_start: cooldown-gated auto-check ──────────────────────────────
  pi.on("session_start", async (_event: SessionStartEvent, ctx: ExtensionContext) => {
    setTimeout(async () => {
      try {
        const state = readState();
        const elapsed = Date.now() - state.lastCheckTimestamp;
        if (elapsed < COOLDOWN_MS) return; // within cooldown — skip silently
        await runReviewPipeline(ctx.ui, /* force */ false);
      } catch (e) { process.stderr.write(`[extensions] never crash Pi: ${String(e)}\n`); }
    }, SESSION_DELAY_MS);
  });

  // ── /upgrade-review [--force] ─────────────────────────────────────────────
  pi.registerCommand("upgrade-review", {
    description:
      "Review Pi upgrade compatibility before installing. " +
      "Use --force to skip the 24-hour cooldown.",
    async handler(args: string, ctx: ExtensionCommandContext) {
      const force = args.trim().toLowerCase() === "--force" ||
                    args.trim().toLowerCase() === "-f";
      try {
        await runReviewPipeline(ctx.ui as ExtensionContext["ui"], /* force */ true);
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        ctx.ui.notify(`/upgrade-review error: ${msg}`, "error");
      }
    },
  });
}
