/**
 * Optional code-intelligence backends for SF.
 *
 * CODEBASE.md stays the durable baseline. Project RAG is an optional MCP
 * accelerator for local hybrid vector + BM25 code retrieval.
 */

import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { delimiter, join, resolve } from "node:path";

import type { CodebaseMapPreferences } from "./preferences-types.js";

export const PROJECT_RAG_MCP_SERVER_NAME = "project-rag";
const PROJECT_RAG_BINARY_NAME = process.platform === "win32" ? "project-rag.exe" : "project-rag";

const PROJECT_RAG_SOURCE_CANDIDATES = [
  "vendor/project-rag",
  "vendor/brainwires/project-rag",
  "third_party/project-rag",
  "third_party/brainwires/project-rag",
  "tools/project-rag",
  "project-rag",
] as const;

export interface ProjectRagMcpConfig {
  command?: string;
  args?: string[];
  cwd?: string;
  env?: Record<string, string>;
  url?: string;
}

interface McpConfigFile {
  mcpServers?: Record<string, ProjectRagMcpConfig>;
  servers?: Record<string, ProjectRagMcpConfig>;
  [key: string]: unknown;
}

export interface ProjectRagDetection {
  status: "disabled" | "configured" | "missing";
  serverName?: string;
  configPath?: string;
  command?: string;
  binaryPath?: string;
  sourceDir?: string;
  reason: string;
}

export interface EnsureProjectRagMcpConfigResult {
  configPath: string;
  serverName: string;
  status: "created" | "updated" | "unchanged";
}

export interface ProjectRagBuildResult {
  sourceDir: string;
  binaryPath: string;
  buildJobs: string;
  stdout: string;
  stderr: string;
}

function readJsonConfig(configPath: string): McpConfigFile {
  if (!existsSync(configPath)) return {};
  const raw = readFileSync(configPath, "utf-8");
  const parsed = JSON.parse(raw) as McpConfigFile;
  return parsed && typeof parsed === "object" ? parsed : {};
}

function readMcpConfigEntries(projectRoot: string): Array<{ name: string; config: ProjectRagMcpConfig; configPath: string }> {
  const entries: Array<{ name: string; config: ProjectRagMcpConfig; configPath: string }> = [];
  const seen = new Set<string>();
  for (const configPath of [join(projectRoot, ".mcp.json"), join(projectRoot, ".sf", "mcp.json")]) {
    try {
      const data = readJsonConfig(configPath);
      const servers = data.mcpServers ?? data.servers;
      if (!servers || typeof servers !== "object") continue;
      for (const [name, config] of Object.entries(servers)) {
        if (seen.has(name)) continue;
        seen.add(name);
        entries.push({ name, config, configPath });
      }
    } catch {
      // Malformed optional MCP config should not block SF startup.
    }
  }
  return entries;
}

function configLooksLikeProjectRag(name: string, config: ProjectRagMcpConfig): boolean {
  const haystack = [
    name,
    config.command ?? "",
    ...(config.args ?? []),
    config.cwd ?? "",
  ].join(" ").toLowerCase();
  return /project[-_]?rag|brainwires/.test(haystack);
}

function normalizeProjectRoot(projectRoot: string): string {
  return resolve(projectRoot);
}

function commandExists(command: string | undefined, env: NodeJS.ProcessEnv = process.env): boolean {
  if (!command) return false;
  return lookupExecutable(command, env) !== null;
}

export function detectProjectRag(
  projectRoot: string,
  prefs?: CodebaseMapPreferences,
): ProjectRagDetection {
  const mode = prefs?.project_rag ?? "auto";
  if (mode === "off") {
    return { status: "disabled", reason: "codebase.project_rag is off" };
  }

  const configuredServer = prefs?.project_rag_server?.trim();
  const normalizedRoot = normalizeProjectRoot(projectRoot);
  const binaryPath = resolveProjectRagBinaryForProject(normalizedRoot, process.env) ?? undefined;
  const sourceDir = findProjectRagSourceDir(normalizedRoot, process.env) ?? undefined;
  const entries = readMcpConfigEntries(normalizedRoot);
  const match = entries.find(({ name, config }) =>
    configuredServer ? name === configuredServer : configLooksLikeProjectRag(name, config)
  );

  if (match) {
    const configuredCommandExists = commandExists(match.config.command);
    return {
      status: "configured",
      serverName: match.name,
      configPath: match.configPath,
      command: match.config.command,
      binaryPath,
      sourceDir,
      reason: configuredCommandExists
        ? "project-rag MCP server configured"
        : "project-rag MCP server configured but command is not currently executable",
    };
  }

  return {
    status: "missing",
    binaryPath,
    sourceDir,
    reason: mode === "required"
      ? "codebase.project_rag is required but no project-rag MCP server is configured"
      : "no project-rag MCP server configured",
  };
}

function lookupExecutable(command: string, env: NodeJS.ProcessEnv = process.env): string | null {
  if (command.includes("/") && existsSync(command)) return command;
  const pathValue = env.PATH ?? "";
  for (const dir of pathValue.split(delimiter).filter(Boolean)) {
    const candidate = join(dir, command);
    if (existsSync(candidate)) return candidate;
  }
  return null;
}

export function resolveProjectRagBinary(env: NodeJS.ProcessEnv = process.env): string | null {
  const explicit = env.SF_PROJECT_RAG_BIN?.trim() || env.PROJECT_RAG_BIN?.trim();
  if (explicit) return explicit;
  return lookupExecutable("project-rag", env);
}

function projectRagBinaryFromSource(sourceDir: string): string | null {
  const candidate = join(sourceDir, "target", "release", PROJECT_RAG_BINARY_NAME);
  return existsSync(candidate) ? candidate : null;
}

export function resolveProjectRagBuildJobs(env: NodeJS.ProcessEnv = process.env): string {
  const configured = env.SF_PROJECT_RAG_BUILD_JOBS?.trim() || env.CARGO_BUILD_JOBS?.trim();
  if (!configured) return "2";

  const parsed = Number.parseInt(configured, 10);
  return Number.isFinite(parsed) && parsed > 0 ? String(parsed) : "2";
}

export function findProjectRagSourceDir(
  projectRoot: string,
  env: NodeJS.ProcessEnv = process.env,
): string | null {
  const explicit = env.SF_PROJECT_RAG_SOURCE?.trim() || env.PROJECT_RAG_SOURCE?.trim();
  const candidates = [
    ...(explicit ? [explicit] : []),
    ...PROJECT_RAG_SOURCE_CANDIDATES.map(relativePath => join(normalizeProjectRoot(projectRoot), relativePath)),
  ];
  for (const candidate of candidates) {
    const manifestPath = join(candidate, "Cargo.toml");
    if (!existsSync(manifestPath)) continue;
    try {
      const manifest = readFileSync(manifestPath, "utf-8");
      if (/name\s*=\s*"project-rag"/.test(manifest) || /project-rag/i.test(candidate)) {
        return resolve(candidate);
      }
    } catch {
      // Optional vendored source discovery should never block SF startup.
    }
  }
  return null;
}

export function resolveProjectRagBinaryForProject(
  projectRoot: string,
  env: NodeJS.ProcessEnv = process.env,
): string | null {
  const explicitOrPath = resolveProjectRagBinary(env);
  if (explicitOrPath) return explicitOrPath;

  const sourceDir = findProjectRagSourceDir(projectRoot, env);
  if (sourceDir) {
    const builtBinary = projectRagBinaryFromSource(sourceDir);
    if (builtBinary) return builtBinary;
  }

  for (const relativePath of [
    join("target", "release", PROJECT_RAG_BINARY_NAME),
    join(".bin", PROJECT_RAG_BINARY_NAME),
    join("bin", PROJECT_RAG_BINARY_NAME),
  ]) {
    const candidate = join(normalizeProjectRoot(projectRoot), relativePath);
    if (existsSync(candidate)) return candidate;
  }

  return null;
}

export function buildProjectRagMcpServerConfig(
  projectRoot: string = process.cwd(),
  env: NodeJS.ProcessEnv = process.env,
): ProjectRagMcpConfig {
  const command = resolveProjectRagBinaryForProject(projectRoot, env);
  if (!command) {
    const sourceDir = findProjectRagSourceDir(projectRoot, env);
    throw new Error(
      sourceDir
        ? `project-rag source found at ${sourceDir}, but no release binary exists. Run /sf codebase rag build first.`
        : "project-rag binary not found. Set SF_PROJECT_RAG_BIN, install project-rag on PATH, or vendor Brainwires/project-rag under vendor/project-rag.",
    );
  }
  return {
    command,
    env: {
      RUST_LOG: env.RUST_LOG ?? "info",
    },
  };
}

export function buildProjectRagBinary(
  projectRoot: string,
  env: NodeJS.ProcessEnv = process.env,
): ProjectRagBuildResult {
  const sourceDir = findProjectRagSourceDir(projectRoot, env);
  if (!sourceDir) {
    throw new Error(
      "project-rag source not found. Vendor Brainwires/project-rag under vendor/project-rag or set SF_PROJECT_RAG_SOURCE.",
    );
  }
  const cargo = lookupExecutable("cargo", env);
  if (!cargo) {
    throw new Error("cargo not found in PATH; cannot build vendored project-rag.");
  }

  const buildJobs = resolveProjectRagBuildJobs(env);
  const result = spawnSync(cargo, ["build", "--release"], {
    cwd: sourceDir,
    env: { ...process.env, ...env, CARGO_BUILD_JOBS: buildJobs },
    encoding: "utf-8",
    maxBuffer: 20 * 1024 * 1024,
  });
  const stdout = result.stdout ?? "";
  const stderr = result.stderr ?? "";
  if (result.error) {
    throw new Error(`cargo build failed to start: ${result.error.message}`);
  }
  if (result.status !== 0) {
    throw new Error(
      `cargo build --release failed with exit ${result.status ?? "unknown"}:\n${stderr || stdout}`.trim(),
    );
  }

  const binaryPath = projectRagBinaryFromSource(sourceDir);
  if (!binaryPath) {
    throw new Error(`cargo build completed, but ${join(sourceDir, "target", "release", PROJECT_RAG_BINARY_NAME)} was not found.`);
  }

  return { sourceDir, binaryPath, buildJobs, stdout, stderr };
}

export function ensureProjectRagMcpConfig(
  projectRoot: string,
  env: NodeJS.ProcessEnv = process.env,
): EnsureProjectRagMcpConfigResult {
  const resolvedProjectRoot = normalizeProjectRoot(projectRoot);
  const configPath = join(resolvedProjectRoot, ".mcp.json");
  const alreadyPresent = existsSync(configPath);
  const existing = readJsonConfig(configPath);
  const desiredServer = buildProjectRagMcpServerConfig(resolvedProjectRoot, env);
  const previousServers = existing.mcpServers ?? {};
  const current = previousServers[PROJECT_RAG_MCP_SERVER_NAME];
  const unchanged =
    JSON.stringify(current ?? null) === JSON.stringify(desiredServer)
    && existing.mcpServers !== undefined;

  if (unchanged) {
    return { configPath, serverName: PROJECT_RAG_MCP_SERVER_NAME, status: "unchanged" };
  }

  const nextConfig: McpConfigFile = {
    ...existing,
    mcpServers: {
      ...previousServers,
      [PROJECT_RAG_MCP_SERVER_NAME]: desiredServer,
    },
  };
  writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf-8");
  return {
    configPath,
    serverName: PROJECT_RAG_MCP_SERVER_NAME,
    status: alreadyPresent ? "updated" : "created",
  };
}

function formatToolPrefix(serverName: string): string {
  return `mcp__${serverName.replace(/[^A-Za-z0-9_]/g, "_")}__`;
}

export function buildCodeIntelligenceContextBlock(
  projectRoot: string,
  prefs?: CodebaseMapPreferences,
): string {
  const detection = detectProjectRag(projectRoot, prefs);
  const lines = [
    "[PROJECT CODE INTELLIGENCE]",
    "",
    "- Durable baseline: use `.sf/CODEBASE.md` for structural orientation and persistent project knowledge.",
  ];

  if (detection.status === "disabled") {
    lines.push("- Project RAG: disabled by `codebase.project_rag: off`.");
  } else if (detection.status === "configured" && detection.serverName) {
    const prefix = formatToolPrefix(detection.serverName);
    lines.push(`- Project RAG: configured as MCP server \`${detection.serverName}\`.`);
    lines.push(
      "- Use Project RAG for broad code retrieval before manual file-by-file reading, " +
      "especially conceptual queries, exact identifiers, schema fields, and git-history questions.",
    );
    lines.push(
      `- Expected MCP tool prefix: \`${prefix}\` ` +
      `(for example \`${prefix}index_codebase\`, \`${prefix}query_codebase\`, ` +
      `\`${prefix}search_by_filters\`, \`${prefix}find_definition\`, ` +
      `\`${prefix}find_references\`, \`${prefix}get_call_graph\`).`,
    );
    lines.push(prefs?.project_rag_auto_index === false
      ? "- Do not auto-index unless explicitly needed; query existing indexes first. " +
        "If any Project RAG tool is missing or fails, continue with `.sf/CODEBASE.md`, `rg`, `lsp`, and scout."
      : "- Index first if the backend is stale or empty; use incremental indexing when available. " +
        "If any Project RAG tool is missing or fails, continue with `.sf/CODEBASE.md`, `rg`, `lsp`, and scout.");
  } else {
    lines.push("- Project RAG: not configured. This is optional; continue with `.sf/CODEBASE.md`, `rg`, `lsp`, and scout.");
    lines.push("- To enable later: build/install Brainwires/project-rag, then run `/sf codebase rag init` or set `SF_PROJECT_RAG_BIN` before initializing MCP config.");
  }

  return `\n\n${lines.join("\n")}`;
}

export function formatProjectRagStatus(projectRoot: string, prefs?: CodebaseMapPreferences): string {
  const detection = detectProjectRag(projectRoot, prefs);
  const lines = ["Project RAG Status", ""];
  lines.push(`Status: ${detection.status}`);
  lines.push(`Reason: ${detection.reason}`);
  if (detection.serverName) lines.push(`Server: ${detection.serverName}`);
  if (detection.configPath) lines.push(`Config: ${detection.configPath}`);
  if (detection.command) lines.push(`Command: ${detection.command}`);
  if (detection.binaryPath) lines.push(`Binary: ${detection.binaryPath}`);
  if (detection.sourceDir) lines.push(`Source: ${detection.sourceDir}`);
  if (detection.status === "configured" && detection.command) {
    lines.push(`Operational: ${commandExists(detection.command) ? "yes" : "no - configured command is missing"}`);
  } else if (detection.binaryPath) {
    lines.push("Operational: no - binary exists but MCP config is missing; run /sf codebase rag init.");
  } else if (detection.sourceDir) {
    lines.push("Operational: no - source exists but release binary is missing; run /sf codebase rag build.");
  } else {
    lines.push("Operational: no - binary/source not found.");
  }
  lines.push("");
  lines.push("Project RAG is optional. SF falls back to CODEBASE.md, rg, lsp, and scout when it is unavailable.");
  lines.push("When configured, agents should use index_codebase, query_codebase, search_by_filters, find_definition, find_references, and get_call_graph before manual file-by-file reading.");
  return lines.join("\n");
}
