const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const UI = require('./ui');

/**
 * High-level installer/wrapper for the Graphify code-knowledge-graph layer,
 * used by `baldart configure`, the `/graphify-bootstrap` skill, and `doctor`.
 *
 * Design (since v4.21.0):
 *   - Graphify is a SINGLE language-agnostic tool (tree-sitter, 28 langs,
 *     local/offline). There is NO per-language adapter registry (unlike LSP).
 *   - This class is a THIN WRAPPER over Graphify's own native commands — it
 *     does not reimplement extraction, clustering, hooks, or MCP. It composes:
 *       install        → `pipx install graphifyy` (CLI `graphify` + `graphify-mcp`)
 *       build/update   → `graphify update <root>`  (offline, no API cost)
 *       git auto-rebuild → `graphify hook install` (native post-commit hook)
 *       always-on tool wiring → `graphify <platform> install`
 *       MCP register   → best-effort `claude mcp add … graphify-mcp <graph>`
 *   - All mutating ops return plain `{ ok, … }` objects and never throw, so the
 *     command layers stay thin and can render results / hints.
 *
 * Staleness uses Graphify's native `built_at_commit` field in graph.json
 * (compared to `git rev-parse HEAD`), mirroring `graphify check-update`.
 */
class GraphifyInstaller {
  constructor(cwd = process.cwd()) { this.cwd = cwd; }

  // ── Detection ───────────────────────────────────────────────────────────

  /** True iff the `graphify` CLI is on PATH and answers `--version`. */
  detect() {
    try {
      execSync('graphify --version', { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 });
      return true;
    } catch { return false; }
  }

  /** `{ ok, version? }` — does the CLI run? */
  verify() {
    try {
      const out = execSync('graphify --version', { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 })
        .toString().trim();
      return { ok: true, version: out.replace(/^graphify\s*/i, '') };
    } catch (err) {
      return { ok: false, error: (err.message || '').split('\n')[0] };
    }
  }

  /** Absolute path to the graph artifact for a graphed root. */
  graphPath(root = this.cwd, outputDir = 'graphify-out') {
    return path.join(root, outputDir, 'graph.json');
  }

  /** True iff a graph has been built for this root. */
  graphExists(root = this.cwd, outputDir = 'graphify-out') {
    return fs.existsSync(this.graphPath(root, outputDir));
  }

  /**
   * Staleness via Graphify's native `built_at_commit` vs current HEAD.
   * Returns `{ built: bool, stale: bool, builtAtCommit?, head? }`. Never throws.
   * `stale` is false when we cannot determine (no git, unreadable graph) so we
   * never nag without evidence.
   */
  isStale(root = this.cwd, outputDir = 'graphify-out') {
    const gp = this.graphPath(root, outputDir);
    if (!fs.existsSync(gp)) return { built: false, stale: false };
    let builtAtCommit = null;
    try { builtAtCommit = (JSON.parse(fs.readFileSync(gp, 'utf8')) || {}).built_at_commit || null; }
    catch { return { built: true, stale: false }; }
    let head = null;
    try { head = execSync('git rev-parse HEAD', { cwd: root, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }).toString().trim(); }
    catch { return { built: true, stale: false, builtAtCommit }; }
    if (!builtAtCommit) return { built: true, stale: false, head };
    // graph.json stores the short or full sha; compare by prefix either way.
    const stale = !(head.startsWith(builtAtCommit) || builtAtCommit.startsWith(head));
    return { built: true, stale, builtAtCommit, head };
  }

  // ── Install (the tool itself) ─────────────────────────────────────────────

  /**
   * Install the Graphify CLI. Prefers `pipx` (handles PATH), falls back to
   * `pip install --user`. Returns `{ ok, method?, error? }`. Never throws.
   * NOT run automatically in CI / `--non-interactive` by the command layer —
   * installing a Python tool is invasive; doctor + the bootstrap skill own that.
   */
  install({ spinner = true } = {}) {
    if (this.detect()) return { ok: true, method: 'already-present' };
    const attempts = [];
    if (this._hasCmd('pipx')) attempts.push({ method: 'pipx', cmd: 'pipx install graphifyy' });
    if (this._hasCmd('pip3')) attempts.push({ method: 'pip3 --user', cmd: 'pip3 install --user graphifyy' });
    if (this._hasCmd('pip')) attempts.push({ method: 'pip --user', cmd: 'pip install --user graphifyy' });
    if (!attempts.length) {
      return { ok: false, error: 'no pipx/pip found on PATH (install Python 3.10+ first)' };
    }
    const sp = spinner ? UI.spinner('Installing Graphify (graphifyy)…').start() : null;
    for (const a of attempts) {
      try {
        execSync(a.cmd, { stdio: 'pipe', timeout: 300000 });
        if (this.detect()) { if (sp) sp.succeed(`Installed Graphify via ${a.method}`); return { ok: true, method: a.method }; }
      } catch (_) { /* try next */ }
    }
    if (sp) sp.fail('Graphify install failed');
    return { ok: false, error: `install failed (tried: ${attempts.map(a => a.method).join(', ')}). Run \`pipx install graphifyy\` manually.` };
  }

  // ── Native composition (build / hooks / always-on / MCP) ─────────────────

  /**
   * Build or refresh the graph for `root` via Graphify's native extractor.
   * `graphify update <root>` is offline (no API cost). `--wiki` (LLM-named
   * communities / wiki pages) is opt-in and requires an LLM API key.
   * Returns `{ ok, error? }`. Never throws.
   */
  buildGraph(root = this.cwd, { wiki = false, spinner = true } = {}) {
    const sp = spinner ? UI.spinner(`Building code graph for ${path.basename(root) || '.'}…`).start() : null;
    try {
      execSync(`graphify update ${JSON.stringify(root)}`, { cwd: this.cwd, stdio: 'pipe', timeout: 600000 });
      if (wiki) {
        try { execSync(`graphify label ${JSON.stringify(root)}`, { cwd: this.cwd, stdio: 'pipe', timeout: 600000 }); }
        catch (_) { /* labelling needs an LLM key; non-fatal */ }
      }
      if (sp) sp.succeed('Code graph built');
      return { ok: true };
    } catch (err) {
      if (sp) sp.fail('Code graph build failed');
      return { ok: false, error: (err.message || '').split('\n')[0] };
    }
  }

  /**
   * Install Graphify's NATIVE post-commit git hook (rebuilds the graph after
   * each commit, no API cost) + the graph.json union merge-driver.
   * Returns `{ ok, error? }`. Never throws.
   */
  installGitHook(root = this.cwd) {
    try {
      execSync('graphify hook install', { cwd: root, stdio: 'pipe', timeout: 30000 });
      return { ok: true };
    } catch (err) {
      return { ok: false, error: (err.message || '').split('\n')[0] };
    }
  }

  /** `{ installed: bool }` — is the native git hook present? Never throws. */
  hookStatus(root = this.cwd) {
    try {
      const out = execSync('graphify hook status', { cwd: root, stdio: ['ignore', 'pipe', 'pipe'], timeout: 10000 }).toString();
      return { installed: /install|present|ok|✓/i.test(out) && !/not install/i.test(out) };
    } catch { return { installed: false }; }
  }

  /**
   * Wire Graphify's always-on mode into each enabled AI tool. Graphify ships
   * native installers that write a graph-first section to the tool's instruction
   * file (CLAUDE.md / AGENTS.md / …) + a pre-tool hook so the assistant queries
   * the graph before grepping. `tools` are BALDART `tools.enabled` ids
   * (claude, codex, …) mapped to Graphify's `graphify <platform> install`.
   * Returns `{ wired: [...], skipped: [...] }`. Never throws.
   */
  installAlwaysOn(tools = ['claude'], root = this.cwd) {
    const SUPPORTED = new Set(['claude', 'codex', 'cursor', 'gemini', 'opencode', 'aider']);
    const wired = [];
    const skipped = [];
    for (const t of tools) {
      if (!SUPPORTED.has(t)) { skipped.push({ tool: t, reason: 'no graphify native installer' }); continue; }
      try {
        execSync(`graphify ${t} install`, { cwd: root, stdio: 'pipe', timeout: 30000 });
        wired.push(t);
      } catch (err) {
        skipped.push({ tool: t, reason: (err.message || '').split('\n')[0] });
      }
    }
    return { wired, skipped };
  }

  /**
   * Best-effort registration of Graphify's MCP server with the Claude CLI.
   * Mirrors how the LSP layer leans on `claude plugin install` rather than
   * building a registrar. Opt-in (`graph.register_mcp`). Returns
   * `{ ok, reason? }`. Never throws.
   */
  registerMcp(root = this.cwd, outputDir = 'graphify-out') {
    if (!this._hasCmd('claude')) return { ok: false, reason: 'claude CLI not on PATH' };
    const gp = this.graphPath(root, outputDir);
    try {
      execSync(`claude mcp add graphify -- graphify-mcp ${JSON.stringify(gp)}`, { cwd: root, stdio: 'pipe', timeout: 20000 });
      return { ok: true };
    } catch (err) {
      return { ok: false, reason: (err.message || '').split('\n')[0] };
    }
  }

  // ── Certification ─────────────────────────────────────────────────────────

  /**
   * One-shot certification of the whole layer, used by `configure`, the
   * `/graphify-bootstrap` skill, and (equivalently) `doctor`. Verifies, in one
   * call, that the install + first run actually succeeded:
   *   - `binary`     — `graphify` CLI is on PATH and answers `--version`
   *   - `graphBuilt` — the FIRST RUN completed (graph.json exists)
   *   - `fresh`      — graph.json was built from the current HEAD (not stale)
   *   - `hook`       — Graphify's native auto-rebuild git hook is installed
   *                    (only checked when `expectHook`)
   * Returns a plain object; never throws. `ok` is the minimum for the layer to
   * function (binary present AND first run done); `fresh`/`hook` are reported but
   * do not fail `ok` (a stale graph is rebuildable, a missing hook is restorable).
   */
  certify(root = this.cwd, { outputDir = 'graphify-out', expectHook = true } = {}) {
    const binary = this.detect();
    if (!binary) {
      return { ok: false, binary: false, version: null, graphBuilt: false, fresh: false, hook: false, details: 'graphify CLI not on PATH' };
    }
    const version = this.verify().version || null;
    const st = this.isStale(root, outputDir);
    const graphBuilt = st.built === true;
    const fresh = graphBuilt && st.stale !== true;
    const hook = expectHook ? this.hookStatus(root).installed === true : true;
    return {
      ok: binary && graphBuilt,
      binary, version, graphBuilt, fresh, hook,
      builtAtCommit: st.builtAtCommit || null, head: st.head || null,
      details: graphBuilt
        ? (fresh ? 'graph fresh' : 'graph stale (behind HEAD)')
        : 'graph not built (first run pending)',
    };
  }

  // ── internals ─────────────────────────────────────────────────────────────

  _hasCmd(cmd) {
    try { execSync(`command -v ${cmd}`, { stdio: 'ignore', shell: '/bin/bash', timeout: 5000 }); return true; }
    catch { return false; }
  }
}

module.exports = GraphifyInstaller;
