import { stat } from "node:fs/promises";
import path from "node:path";
import type { PilotDeckToolDefinition } from "../protocol/types.js";
import { PilotDeckToolRuntimeError } from "../protocol/errors.js";
import { resolvePilotDeckWorkspacePath } from "./filesystem/pathSafety.js";
import {
  isIgnoredPath,
  normalizeRelativePath,
  runRipgrep,
  splitRipgrepLines,
} from "./filesystem/ripgrep.js";

export type GrepInput = {
  pattern: string;
  path?: string;
  glob?: string;
  output_mode?: "content" | "files_with_matches" | "count";
  "-B"?: number;
  "-A"?: number;
  "-C"?: number;
  context?: number;
  "-n"?: boolean;
  "-i"?: boolean;
  type?: string;
  head_limit?: number;
  offset?: number;
  multiline?: boolean;
};

const DEFAULT_HEAD_LIMIT = 250;
const MAX_COLUMNS = 500;
const EXCLUDED_DIRECTORY_GLOBS = [
  "!.git",
  "!.svn",
  "!.hg",
  "!.bzr",
  "!.jj",
  "!.sl",
  "!node_modules",
  "!dist",
] as const;

export function createGrepTool(): PilotDeckToolDefinition<GrepInput> {
  return {
    name: "grep",
    aliases: ["Grep"],
    description:
      "A ripgrep-powered search tool for workspace file contents.\n\nUsage:\n- ALWAYS use `grep` for content search tasks. Do NOT invoke `grep` or `rg` through `bash` when this tool can express the search.\n- Supports full regular expressions, `type` filters, glob filters, and multiline search.\n- Output modes: `content` shows matching lines, `files_with_matches` lists file paths, and `count` shows per-file match counts.\n- `head_limit` and `offset` work in every output mode. Pass `head_limit: 0` only when you truly need unlimited results.\n- For open-ended multi-step exploration, prefer the `agent` tool's `explore` subagent instead of repeatedly broadening searches in the parent agent.",
    kind: "filesystem",
    inputSchema: {
      type: "object",
      required: ["pattern"],
      additionalProperties: false,
      properties: {
        pattern: {
          type: "string",
          description: "The regular expression pattern to search for in file contents.",
        },
        path: {
          type: "string",
          description: "File or directory to search within. Defaults to the workspace root.",
        },
        glob: {
          type: "string",
          description:
            "Glob pattern to filter files (for example '*.js' or '*.{ts,tsx}'). Maps to ripgrep's --glob.",
        },
        output_mode: {
          type: "string",
          enum: ["content", "files_with_matches", "count"],
          description:
            "Output mode: 'content' shows matching lines, 'files_with_matches' lists file paths, and 'count' shows per-file counts. Defaults to 'files_with_matches'.",
        },
        "-B": {
          type: "integer",
          description: "Number of lines to show before each match (content mode only).",
        },
        "-A": {
          type: "integer",
          description: "Number of lines to show after each match (content mode only).",
        },
        "-C": {
          type: "integer",
          description: "Alias for context: number of lines to show before and after each match.",
        },
        context: {
          type: "integer",
          description: "Number of lines to show before and after each match (content mode only).",
        },
        "-n": {
          type: "boolean",
          description:
            "When false, hide line numbers in content mode output. Defaults to true. Ignored in other modes.",
        },
        "-i": {
          type: "boolean",
          description: "When true, perform case-insensitive matching.",
        },
        type: {
          type: "string",
          description:
            "File type to search (for example 'js', 'ts', 'py'). Maps to ripgrep's --type filter.",
        },
        head_limit: {
          type: "integer",
          description:
            "Maximum number of output entries to return. Applies to all output modes. Defaults to 250; pass 0 for unlimited.",
        },
        offset: {
          type: "integer",
          description: "Skip this many output entries before returning results.",
        },
        multiline: {
          type: "boolean",
          description:
            "When true, enable multiline mode where . matches newlines and patterns can span lines.",
        },
      },
    },
    maxResultBytes: 200_000,
    isReadOnly: () => true,
    isConcurrencySafe: () => true,
    execute: async (input, context) => {
      const resolved = resolvePilotDeckWorkspacePath(input.path ?? ".", context, { mustExist: true });
      if (!resolved.ok) {
        throw new PilotDeckToolRuntimeError(resolved.error.code, resolved.error.message, resolved.error.details);
      }

      const mode = input.output_mode ?? "files_with_matches";
      const target = await resolveSearchTarget(resolved.absolutePath, resolved.relativePath);
      const stdout = await runRipgrep({
        cwd: target.cwd,
        args: buildRipgrepArgs(input, mode, target.target),
        env: context.env,
        signal: context.abortSignal,
        toolName: "grep",
      });

      if (mode === "content") {
        const parsedLines = splitRipgrepLines(stdout)
          .map((line) => parseContentLine(line))
          .filter((line) => line.type === "separator" || !isIgnoredPath(line.file));
        const renderedLines = parsedLines.map((line) =>
          line.type === "separator"
            ? line.raw
            : formatContentLine(line, target.workspaceBaseDir, input["-n"] ?? true),
        );
        const page = paginate(renderedLines, input.head_limit, input.offset);
        const files = uniqueSorted(
          parsedLines
            .filter((line): line is Extract<ParsedContentLine, { type: "content" }> => line.type === "content")
            .map((line) => toWorkspaceFile(line.file, target.workspaceBaseDir)),
        );
        return {
          content: [{ type: "text", text: page.items.join("\n") }],
          data: {
            mode,
            files,
            count: parsedLines.length,
            truncated: page.truncated,
          },
          metadata: { truncated: page.truncated },
        };
      }

      if (mode === "count") {
        const countEntries = splitRipgrepLines(stdout)
          .map((line) => parseCountEntry(line))
          .filter((entry): entry is ParsedCountEntry => entry !== undefined && !isIgnoredPath(entry.file))
          .sort((left, right) => left.file.localeCompare(right.file));
        const totalMatches = countEntries.reduce((sum, entry) => sum + entry.count, 0);
        const renderedEntries = countEntries.map((entry) => ({
          file: toWorkspaceFile(entry.file, target.workspaceBaseDir),
          text: `${toWorkspaceFile(entry.file, target.workspaceBaseDir)}:${entry.count}`,
        }));
        const page = paginate(renderedEntries, input.head_limit, input.offset);
        return {
          content: [{ type: "text", text: page.items.map((entry) => entry.text).join("\n") }],
          data: {
            mode,
            files: page.items.map((entry) => entry.file),
            count: totalMatches,
            truncated: page.truncated,
          },
          metadata: { truncated: page.truncated },
        };
      }

      const rawFiles = splitRipgrepLines(stdout)
        .map(normalizeRelativePath)
        .filter((file) => !isIgnoredPath(file));
      const sortedFiles = await sortFilesByModifiedTime(rawFiles, target.cwd);
      const workspaceFiles = sortedFiles.map((file) => toWorkspaceFile(file, target.workspaceBaseDir));
      const page = paginate(workspaceFiles, input.head_limit, input.offset);
      return {
        content: [{ type: "text", text: page.items.join("\n") }],
        data: {
          mode,
          files: page.items,
          count: workspaceFiles.length,
          truncated: page.truncated,
        },
        metadata: { truncated: page.truncated },
      };
    },
  };
}

type ParsedContentLine =
  | {
      type: "separator";
      raw: string;
    }
  | {
      type: "content";
      raw: string;
      file: string;
      separator: ":" | "-";
      lineNumber: number;
      lineSeparator: ":" | "-";
      content: string;
    };

type ParsedCountEntry = {
  file: string;
  count: number;
};

async function resolveSearchTarget(absolutePath: string, relativePath: string): Promise<{
  cwd: string;
  target: string;
  workspaceBaseDir: string;
}> {
  const fileStat = await stat(absolutePath);
  const normalizedRelativePath = normalizeRelativePath(relativePath);
  if (fileStat.isDirectory()) {
    return {
      cwd: absolutePath,
      target: ".",
      workspaceBaseDir: normalizedRelativePath === "." ? "" : normalizedRelativePath,
    };
  }
  const directory = path.posix.dirname(normalizedRelativePath);
  return {
    cwd: path.dirname(absolutePath),
    target: path.basename(absolutePath),
    workspaceBaseDir: directory === "." ? "" : directory,
  };
}

function buildRipgrepArgs(
  input: GrepInput,
  mode: NonNullable<GrepInput["output_mode"]>,
  target: string,
): string[] {
  const args = ["--hidden", "--max-columns", String(MAX_COLUMNS)];
  for (const excluded of EXCLUDED_DIRECTORY_GLOBS) {
    args.push("--glob", excluded);
  }

  if (input.multiline) {
    args.push("-U", "--multiline-dotall");
  }
  if (input["-i"]) {
    args.push("-i");
  }
  if (input.type) {
    args.push("--type", input.type);
  }
  for (const pattern of splitGlobPatterns(input.glob)) {
    args.push("--glob", pattern);
  }

  if (mode === "files_with_matches") {
    args.push("-l");
  } else if (mode === "count") {
    args.push("-c", "--with-filename");
  } else {
    args.push("--with-filename", "-n");
    addContextArgs(args, input);
  }

  if (input.pattern.startsWith("-")) {
    args.push("-e", input.pattern);
  } else {
    args.push(input.pattern);
  }
  args.push(target);
  return args;
}

function addContextArgs(args: string[], input: GrepInput): void {
  if (input.context !== undefined) {
    args.push("-C", String(input.context));
    return;
  }
  if (input["-C"] !== undefined) {
    args.push("-C", String(input["-C"]));
    return;
  }
  if (input["-B"] !== undefined) {
    args.push("-B", String(input["-B"]));
  }
  if (input["-A"] !== undefined) {
    args.push("-A", String(input["-A"]));
  }
}

function splitGlobPatterns(glob: string | undefined): string[] {
  if (!glob) return [];
  const patterns: string[] = [];
  for (const rawPattern of glob.split(/\s+/).filter(Boolean)) {
    if (rawPattern.includes("{") && rawPattern.includes("}")) {
      patterns.push(rawPattern);
      continue;
    }
    patterns.push(...rawPattern.split(",").filter(Boolean));
  }
  return patterns;
}

function parseContentLine(line: string): ParsedContentLine {
  if (line === "--") {
    return { type: "separator", raw: line };
  }
  const match = line.match(/^(.*?)([:-])(\d+)([:-])(.*)$/);
  if (!match) {
    throw new PilotDeckToolRuntimeError(
      "tool_execution_failed",
      `Unexpected ripgrep content output: ${line}`,
    );
  }
  return {
    type: "content",
    raw: line,
    file: normalizeRelativePath(match[1]),
    separator: match[2] as ":" | "-",
    lineNumber: Number.parseInt(match[3], 10),
    lineSeparator: match[4] as ":" | "-",
    content: match[5],
  };
}

function formatContentLine(
  line: Extract<ParsedContentLine, { type: "content" }>,
  workspaceBaseDir: string,
  showLineNumbers: boolean,
): string {
  const file = toWorkspaceFile(line.file, workspaceBaseDir);
  if (showLineNumbers) {
    return `${file}${line.separator}${line.lineNumber}${line.lineSeparator}${line.content}`;
  }
  return `${file}${line.separator}${line.content}`;
}

function parseCountEntry(line: string): ParsedCountEntry | undefined {
  const separator = line.lastIndexOf(":");
  if (separator <= 0) {
    return undefined;
  }
  const file = normalizeRelativePath(line.slice(0, separator));
  const count = Number.parseInt(line.slice(separator + 1), 10);
  if (!Number.isFinite(count)) {
    return undefined;
  }
  return { file, count };
}

async function sortFilesByModifiedTime(files: string[], cwd: string): Promise<string[]> {
  const stats = await Promise.allSettled(files.map((file) => stat(path.join(cwd, file))));
  return files
    .map((file, index) => ({
      file,
      modifiedAt:
        stats[index]?.status === "fulfilled" ? (stats[index].value.mtimeMs ?? 0) : 0,
    }))
    .sort((left, right) => {
      const delta = right.modifiedAt - left.modifiedAt;
      return delta !== 0 ? delta : left.file.localeCompare(right.file);
    })
    .map((entry) => entry.file);
}

function paginate<T>(
  items: T[],
  headLimit: number | undefined,
  offset: number | undefined,
): { items: T[]; truncated: boolean } {
  const normalizedOffset = Math.max(0, offset ?? 0);
  if (headLimit === 0) {
    return {
      items: items.slice(normalizedOffset),
      truncated: false,
    };
  }
  const normalizedLimit = Math.max(0, headLimit ?? DEFAULT_HEAD_LIMIT);
  const page = items.slice(normalizedOffset, normalizedOffset + normalizedLimit);
  return {
    items: page,
    truncated: items.length > normalizedOffset + normalizedLimit,
  };
}

function toWorkspaceFile(file: string, workspaceBaseDir: string): string {
  return workspaceBaseDir.length > 0
    ? path.posix.join(workspaceBaseDir, normalizeRelativePath(file))
    : normalizeRelativePath(file);
}

function uniqueSorted(items: string[]): string[] {
  return [...new Set(items)].sort();
}