/**
 * AstGrep — AST-pattern matching across files.
 *
 * Backed by the native `astGrep` API which uses Tree-sitter pattern matching
 * (ast-grep-style). Patterns are written in the source language with `$VAR` /
 * `$$$ARGS` meta-variables that bind to matching nodes.
 *
 * Example patterns:
 *   - `console.log($MSG)`               — any console.log call, capture argument
 *   - `function $NAME($$$ARGS) { $$$ }` — any function declaration
 *   - `if ($COND) { $$$ }`              — any if statement
 *
 * Much higher precision than text grep for code: a substring search for
 * "function foo" matches comments, strings, and unrelated identifiers;
 * astGrep matches only actual function declarations named `foo`.
 */

import { type Static, Type } from "@sinclair/typebox";
import type { AgentTool } from "@singularity-forge/agent-core";
import { astGrep } from "@singularity-forge/native/ast";
import { resolveToCwd } from "./path-utils.js";

const astGrepSchema = Type.Object({
	patterns: Type.Array(Type.String(), {
		description:
			"One or more AST patterns to match. Use `$VAR` for single-node captures and `$$$ARGS` for variadic captures. Example: `console.log($MSG)`.",
		minItems: 1,
	}),
	path: Type.Optional(
		Type.String({
			description:
				"Root path to search (defaults to cwd). May be a file or directory.",
		}),
	),
	glob: Type.Optional(
		Type.String({
			description:
				"Glob pattern to restrict which files are scanned (e.g. `**/*.ts`). When omitted, the native engine infers from the file/directory.",
		}),
	),
	lang: Type.Optional(
		Type.String({
			description:
				"Force a specific tree-sitter grammar (ts, tsx, js, jsx, py, rs, …). Default: infer per-file from extension.",
		}),
	),
	selector: Type.Optional(
		Type.String({
			description:
				"Optional AST node selector to restrict the kinds of nodes a pattern can bind to.",
		}),
	),
	strictness: Type.Optional(
		Type.String({
			description:
				"Pattern strictness (cst|smart|relaxed|signature). Default: smart.",
		}),
	),
	limit: Type.Optional(
		Type.Number({
			description: "Maximum number of matches to return.",
			minimum: 1,
		}),
	),
	includeMeta: Type.Optional(
		Type.Boolean({
			description:
				"Include meta-variable bindings ($VAR / $$$ARGS) in each match. Default: true for single-pattern queries.",
		}),
	),
	context: Type.Optional(
		Type.Number({
			description:
				"Number of context lines to return around each match (analogous to grep -C).",
			minimum: 0,
		}),
	),
});

type AstGrepParams = Static<typeof astGrepSchema>;

export function createAstGrepTool(cwd: string): AgentTool<typeof astGrepSchema> {
	return {
		name: "AstGrep",
		label: "AstGrep",
		description:
			"Search code by AST pattern via Tree-sitter (ast-grep-style). Patterns use `$VAR` and `$$$ARGS` meta-variables. Higher precision than text grep for code — matches only real syntactic constructs, not substring occurrences in comments/strings/identifiers. Use to find call sites of a function, detect anti-patterns, locate all if/for/while of a shape, etc. Returns matches with file path, byte range, line/column, and (when includeMeta) captured meta-variable bindings.",
		parameters: astGrepSchema,
		execute: async (
			_toolCallId: string,
			params: AstGrepParams,
			signal?: AbortSignal,
		) => {
			if (signal?.aborted) {
				throw new Error("AstGrep cancelled before start");
			}
			const root = params.path ? resolveToCwd(cwd, params.path) : cwd;
			try {
				const result = astGrep({
					patterns: params.patterns,
					path: root,
					glob: params.glob,
					lang: params.lang,
					selector: params.selector,
					strictness: params.strictness,
					limit: params.limit,
					includeMeta: params.includeMeta ?? true,
					context: params.context,
				});
				const header =
					`AstGrep: ${result.totalMatches} match(es) in ${result.filesWithMatches} file(s) ` +
					`(scanned ${result.filesSearched})` +
					(result.limitReached ? " — limit reached" : "");
				const lines: string[] = [header];
				for (const m of result.matches) {
					const meta =
						m.metaVariables && Object.keys(m.metaVariables).length > 0
							? ` ${JSON.stringify(m.metaVariables)}`
							: "";
					lines.push(`  ${m.path}:${m.startLine}:${m.startColumn}${meta}`);
					for (const ln of m.text.split("\n").slice(0, 3)) {
						lines.push(`    | ${ln}`);
					}
				}
				if (result.parseErrors && result.parseErrors.length > 0) {
					lines.push(
						`(${result.parseErrors.length} file(s) had parse errors; matches in those files are skipped)`,
					);
				}
				return {
					content: [{ type: "text" as const, text: lines.join("\n") }],
					details: {
						totalMatches: result.totalMatches,
						filesWithMatches: result.filesWithMatches,
						filesSearched: result.filesSearched,
						limitReached: result.limitReached,
						parseErrors: result.parseErrors,
						matches: result.matches,
					},
				};
			} catch (err: unknown) {
				const message = err instanceof Error ? err.message : String(err);
				return {
					content: [
						{
							type: "text" as const,
							text: `AstGrep failed: ${message}`,
						},
					],
					details: { error: message },
					isError: true,
				};
			}
		},
	};
}
