/**
 * ADR-121 Phase 5 (lightweight) — ruvector MCP sidecar availability probe.
 *
 * The `ruvector` CLI ships its own MCP server with a rich tool surface
 * (`hooks_*`, vector memory, trajectory tracking, etc.). The full
 * Phase 5 plan is to wire a sidecar MCP server alongside `@claude-flow/cli`
 * so embedding-heavy MCP tools can delegate to the optimized Rust
 * backend. That's a multi-package integration (touches the CLI's MCP
 * registry, the doctor command, the cost-tracker) and lands in a
 * follow-up.
 *
 * This module ships the *foundation* for that work: a structured
 * availability probe that detects whether `ruvector` is installed,
 * what version, and which MCP tools it exposes. The probe is
 * shellout-based (spawns `ruvector --version` + `ruvector mcp tools`),
 * times out cleanly, and degrades to a `null` report when the CLI
 * isn't on the PATH.
 *
 * Consumers:
 *   - `ruflo doctor` integration (follow-up PR) — reports the sidecar
 *     availability + version in the system health table
 *   - New `embeddings_check_ruvector_sidecar` MCP tool — exposes the
 *     report to LLM agents that want to know whether the optimized
 *     backend is reachable before issuing embedding-heavy calls
 *   - CI guards — assert the probe handles both installed/not-installed
 *     paths cleanly
 */

import { spawn, type ChildProcessByStdio } from 'node:child_process';
import type { Readable } from 'node:stream';

/**
 * Single MCP tool entry as reported by `ruvector mcp tools`.
 * Mirrors the columns of the CLI's text output.
 */
export interface RuvectorMcpTool {
  /** Tool group (e.g. 'hooks-core', 'hooks-trajectory'). */
  readonly group: string;
  /** Tool name (e.g. 'hooks_route'). */
  readonly name: string;
  /** Parameter signature as a free-form string. */
  readonly params: string;
  /** Human-readable description. */
  readonly description: string;
}

export interface RuvectorAvailability {
  /** True iff `ruvector` is on the PATH and `--version` succeeded. */
  readonly available: boolean;
  /** CLI version (e.g. "0.2.13") when available. */
  readonly version?: string;
  /** Resolved binary path when available. */
  readonly binary?: string;
  /** Tools surfaced by `ruvector mcp tools` when available. */
  readonly tools?: readonly RuvectorMcpTool[];
  /** Total tool count (cheap top-level number for dashboards). */
  readonly toolCount?: number;
  /** Tool-group breakdown — { 'hooks-core': 10, 'hooks-trajectory': 3, ... }. */
  readonly groups?: Readonly<Record<string, number>>;
  /** Human-readable reason when `available === false`. */
  readonly reason?: string;
  /** Wall-clock ms the probe took. */
  readonly probeMs: number;
}

export interface RuvectorProbeOptions {
  /** Per-shellout timeout (default 5_000 ms). */
  readonly timeoutMs?: number;
  /** Override the binary name (default 'ruvector'). For tests. */
  readonly binary?: string;
}

/**
 * Run a child process to completion or timeout. Returns null on
 * any failure (spawn error, timeout, non-zero exit). The caller
 * surfaces the failure as a structured `reason` string instead of
 * propagating the raw error — the probe is meant to be friendly,
 * not throw.
 */
async function runShort(bin: string, args: string[], timeoutMs: number): Promise<string | null> {
  return new Promise((resolve) => {
    // Use the literal `['ignore', 'pipe', 'pipe']` stdio shape so
    // TypeScript narrows to ChildProcessByStdio<null, Readable, Readable>
    // (no stdin handle, both stdout + stderr pipeable).
    let proc: ChildProcessByStdio<null, Readable, Readable>;
    try {
      proc = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] }) as ChildProcessByStdio<null, Readable, Readable>;
    } catch {
      resolve(null);
      return;
    }
    let stdout = '';
    let stderr = '';
    let done = false;
    const finish = (val: string | null) => { if (!done) { done = true; resolve(val); } };
    proc.stdout.on('data', (b) => { stdout += b.toString(); });
    proc.stderr.on('data', (b) => { stderr += b.toString(); });
    const timer = setTimeout(() => { try { proc.kill('SIGTERM'); } catch { /* ignore */ } finish(null); }, timeoutMs);
    proc.on('error', () => { clearTimeout(timer); finish(null); });
    proc.on('close', (code) => {
      clearTimeout(timer);
      // For `ruvector --version` and `ruvector mcp tools`, both succeed
      // with exit 0 and print to stdout. A non-zero exit means the CLI
      // surface changed — treat as "not available" for probe purposes.
      finish(code === 0 ? stdout : (stderr ? null : stdout || null));
    });
  });
}

/**
 * Parse the columnar text output of `ruvector mcp tools` into a
 * structured tool list. The CLI's output shape (as of ruvector
 * 0.2.x):
 *
 *   RuVector MCP Tools
 *
 *     <group> (<count>):
 *       <name>      <params>     <description>
 *       ...
 *
 * Parser is intentionally tolerant of column-alignment variation
 * (the CLI right-pads to a column width that depends on the
 * longest tool name) — we split on runs of 2+ spaces, not exact
 * column positions.
 */
function parseMcpTools(stdout: string): RuvectorMcpTool[] {
  const tools: RuvectorMcpTool[] = [];
  let currentGroup: string | null = null;
  for (const rawLine of stdout.split('\n')) {
    const line = rawLine.replace(/\[[0-9;]*m/g, ''); // strip ANSI color
    const trimmed = line.trim();
    if (!trimmed) continue;
    // Group header looks like "  hooks-core (10):"
    const groupMatch = /^\s+([a-z0-9_-]+)\s*\((\d+)\):\s*$/i.exec(line);
    if (groupMatch) {
      currentGroup = groupMatch[1]!;
      continue;
    }
    // Tool row: indented further, has at least 3 columns.
    if (currentGroup && /^\s{4,}\S/.test(line)) {
      // Split on runs of 2+ spaces to get [name, params, ...description].
      const parts = trimmed.split(/\s{2,}/);
      if (parts.length < 2) continue;
      const name = parts[0]!.trim();
      const params = (parts[1] ?? '').trim();
      const description = parts.slice(2).join(' ').trim() || params;
      // Re-derive params if description ate them (description was
      // missing → parts had 2 entries, second was actually the desc).
      const finalParams = parts.length >= 3 ? params : '(none)';
      const finalDesc = parts.length >= 3 ? description : params;
      // Skip header-shaped rows that slip past the regex (e.g. an
      // extra divider line in a future CLI release).
      if (!name || name.startsWith('-') || name === 'RuVector') continue;
      tools.push({ group: currentGroup, name, params: finalParams, description: finalDesc });
    }
  }
  return tools;
}

/**
 * Probe ruvector CLI availability + MCP tool surface. Never throws;
 * returns a `RuvectorAvailability` report with `available: false` and
 * a `reason` string when the CLI isn't reachable.
 */
export async function probeRuvectorSidecar(
  options: RuvectorProbeOptions = {},
): Promise<RuvectorAvailability> {
  const t0 = Date.now();
  const bin = options.binary ?? 'ruvector';
  const timeoutMs = options.timeoutMs ?? 5_000;

  // Step 1 — version probe. If this fails the CLI isn't there.
  const versionOut = await runShort(bin, ['--version'], timeoutMs);
  if (!versionOut) {
    return {
      available: false,
      reason: `'${bin} --version' did not return cleanly within ${timeoutMs}ms; CLI likely not installed`,
      probeMs: Date.now() - t0,
    };
  }
  const version = versionOut.trim().split('\n')[0]?.trim();

  // Step 2 — locate the binary via `which` (best-effort, non-fatal).
  let binary: string | undefined;
  const whichOut = await runShort('which', [bin], 2_000);
  if (whichOut) binary = whichOut.trim().split('\n')[0]?.trim();

  // Step 3 — tool list. `ruvector mcp tools` is the documented surface.
  const toolsOut = await runShort(bin, ['mcp', 'tools'], timeoutMs);
  if (!toolsOut) {
    return {
      available: true,
      version,
      binary,
      reason: `'${bin} mcp tools' did not return cleanly within ${timeoutMs}ms; tool surface unknown`,
      probeMs: Date.now() - t0,
    };
  }
  const tools = parseMcpTools(toolsOut);
  const groups: Record<string, number> = {};
  for (const t of tools) groups[t.group] = (groups[t.group] ?? 0) + 1;

  return {
    available: true,
    version,
    binary,
    tools,
    toolCount: tools.length,
    groups,
    probeMs: Date.now() - t0,
  };
}

/**
 * Compact summary for dashboards / `ruflo doctor` output. Returns
 * a single line suitable for a table row.
 */
export function formatRuvectorAvailability(r: RuvectorAvailability): string {
  if (!r.available) {
    return `ruvector: not available (${r.reason ?? 'unknown reason'}) [${r.probeMs}ms]`;
  }
  const groupSummary = r.groups
    ? Object.entries(r.groups).map(([g, n]) => `${g}=${n}`).join(', ')
    : 'tools unknown';
  return `ruvector: ${r.version ?? '?'} (${r.toolCount ?? 0} MCP tools — ${groupSummary}) [${r.probeMs}ms]`;
}
