import { hasBinary } from "../agents/skills.js";
import { formatCliCommand } from "../cli/command-format.js";
import { runCommandWithTimeout } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js";
import { isRich, theme } from "../terminal/theme.js";

const SEARCH_TOOL = "https://docs.openclaw.ai/mcp.search_open_claw";
const SEARCH_TIMEOUT_MS = 30_000;
const DEFAULT_SNIPPET_MAX = 220;
const MCP_ERROR_PATTERN = /MCP error\s+-?\d+/i;

type DocResult = {
  title: string;
  link: string;
  snippet?: string;
};

type NodeRunner = {
  cmd: string;
  args: string[];
};

type ToolRunOptions = {
  input?: string;
  timeoutMs?: number;
};

function resolveNodeRunner(): NodeRunner {
  if (hasBinary("pnpm")) {
    return { cmd: "pnpm", args: ["dlx"] };
  }
  if (hasBinary("npx")) {
    return { cmd: "npx", args: ["-y"] };
  }
  throw new Error(
    `Docs search needs pnpm or npx to run the docs search helper. Install pnpm, or run ${formatCliCommand("npm install -g pnpm")}.`,
  );
}

async function runNodeTool(tool: string, toolArgs: string[], options: ToolRunOptions = {}) {
  const runner = resolveNodeRunner();
  const argv = [runner.cmd, ...runner.args, tool, ...toolArgs];
  return await runCommandWithTimeout(argv, {
    timeoutMs: options.timeoutMs ?? SEARCH_TIMEOUT_MS,
    input: options.input,
  });
}

async function runTool(tool: string, toolArgs: string[], options: ToolRunOptions = {}) {
  if (hasBinary(tool)) {
    return await runCommandWithTimeout([tool, ...toolArgs], {
      timeoutMs: options.timeoutMs ?? SEARCH_TIMEOUT_MS,
      input: options.input,
    });
  }
  return await runNodeTool(tool, toolArgs, options);
}

function extractLine(lines: string[], prefix: string): string | undefined {
  const line = lines.find((value) => value.startsWith(prefix));
  if (!line) {
    return undefined;
  }
  return line.slice(prefix.length).trim();
}

function normalizeSnippet(raw: string | undefined, fallback: string): string {
  const base = raw && raw.trim().length > 0 ? raw : fallback;
  const cleaned = base.replace(/\s+/g, " ").trim();
  if (!cleaned) {
    return "";
  }
  if (cleaned.length <= DEFAULT_SNIPPET_MAX) {
    return cleaned;
  }
  return `${cleaned.slice(0, DEFAULT_SNIPPET_MAX - 3)}...`;
}

function firstParagraph(text: string): string {
  return (
    text
      .split(/\n\s*\n/)
      .map((chunk) => chunk.trim())
      .find(Boolean) ?? ""
  );
}

function parseSearchOutput(raw: string): DocResult[] {
  const normalized = raw.replace(/\r/g, "");
  const blocks = normalized
    .split(/\n(?=Title: )/g)
    .map((chunk) => chunk.trim())
    .filter(Boolean);

  const results: DocResult[] = [];
  for (const block of blocks) {
    const lines = block.split("\n");
    const title = extractLine(lines, "Title:");
    const link = extractLine(lines, "Link:");
    if (!title || !link) {
      continue;
    }
    const content = extractLine(lines, "Content:");
    const contentIndex = lines.findIndex((line) => line.startsWith("Content:"));
    const body =
      contentIndex >= 0
        ? lines
            .slice(contentIndex + 1)
            .join("\n")
            .trim()
        : "";
    const snippet = normalizeSnippet(content, firstParagraph(body));
    results.push({ title, link, snippet: snippet || undefined });
  }
  return results;
}

function escapeMarkdown(text: string): string {
  return text.replace(/[()[\]]/g, "\\$&");
}

function buildMarkdown(query: string, results: DocResult[]): string {
  const lines: string[] = [`# Docs search: ${escapeMarkdown(query)}`, ""];
  if (results.length === 0) {
    lines.push("_No results._");
    return lines.join("\n");
  }
  for (const item of results) {
    const title = escapeMarkdown(item.title);
    const snippet = item.snippet ? escapeMarkdown(item.snippet) : "";
    const suffix = snippet ? ` - ${snippet}` : "";
    lines.push(`- [${title}](${item.link})${suffix}`);
  }
  return lines.join("\n");
}

function formatLinkLabel(link: string): string {
  return link.replace(/^https?:\/\//i, "");
}

function renderRichResults(query: string, results: DocResult[], runtime: RuntimeEnv) {
  runtime.log(`${theme.heading("Docs search:")} ${theme.info(query)}`);
  if (results.length === 0) {
    runtime.log(theme.muted("No results."));
    return;
  }
  for (const item of results) {
    const linkLabel = formatLinkLabel(item.link);
    const link = formatDocsLink(item.link, linkLabel);
    runtime.log(
      `${theme.muted("-")} ${theme.command(item.title)} ${theme.muted("(")}${link}${theme.muted(")")}`,
    );
    if (item.snippet) {
      runtime.log(`  ${theme.muted(item.snippet)}`);
    }
  }
}

async function renderMarkdown(markdown: string, runtime: RuntimeEnv) {
  runtime.log(markdown.trimEnd());
}

export async function docsSearchCommand(queryParts: string[], runtime: RuntimeEnv) {
  const query = queryParts.join(" ").trim();
  if (!query) {
    const docs = formatDocsLink("/", "docs.openclaw.ai");
    if (isRich()) {
      runtime.log(`${theme.muted("Docs:")} ${docs}`);
      runtime.log(`${theme.muted("Search:")} ${formatCliCommand('openclaw docs "your query"')}`);
    } else {
      runtime.log("Docs: https://docs.openclaw.ai/");
      runtime.log(`Search: ${formatCliCommand('openclaw docs "your query"')}`);
    }
    return;
  }

  const payload = JSON.stringify({ query });
  const res = await runTool(
    "mcporter",
    ["call", SEARCH_TOOL, "--args", payload, "--output", "text"],
    { timeoutMs: SEARCH_TIMEOUT_MS },
  );

  if (res.code !== 0) {
    const err = res.stderr.trim() || res.stdout.trim() || `exit ${res.code}`;
    runtime.error(`Docs search failed: ${err}`);
    runtime.exit(1);
    return;
  }

  const combined = `${res.stdout}\n${res.stderr}`;
  if (MCP_ERROR_PATTERN.test(combined)) {
    const err = (res.stderr.trim() || res.stdout.trim())
      .split("\n")
      .find((line) => MCP_ERROR_PATTERN.test(line));
    runtime.error(`Docs search failed: ${err ?? "MCP error reported by docs search tool"}`);
    runtime.exit(1);
    return;
  }

  const results = parseSearchOutput(res.stdout);
  if (isRich()) {
    renderRichResults(query, results, runtime);
    return;
  }
  const markdown = buildMarkdown(query, results);
  await renderMarkdown(markdown, runtime);
}
