import { BestAgentError, truncateText } from "@best-agent/foundation";
import { readdirSync } from "node:fs";
import { relative, resolve } from "node:path";
import { z } from "zod";
import { toolInputSchema } from "../schema.js";
import { READ_FILE_INLINE_MAX_BYTES, READ_FILE_INLINE_MAX_LINES, READ_FILE_LINE_PREVIEW_CHARS, READ_FILE_PREVIEW_HEAD_LINES, READ_FILE_PREVIEW_MAX_BYTES, READ_FILE_PREVIEW_MAX_LINES, READ_FILE_PREVIEW_TAIL_LINES, READ_FILE_RANGE_MAX_LINES, isPlainObject, parseToolInputJsonValue, splitTextFileLines, } from "./shared.js";
const READ_FILE_LINE_NUMBER_SCHEMA = z.coerce.number().int().min(1);
const READ_FILE_INPUT_SCHEMA = toolInputSchema(z.strictObject({
    end_line: READ_FILE_LINE_NUMBER_SCHEMA.optional().describe("Inclusive ending line. Use with start_line; ranges may cover at most 200 lines."),
    limit: READ_FILE_LINE_NUMBER_SCHEMA.optional().describe("Line count when start_line is set; otherwise reads lines 1 through limit."),
    path: z.string().min(1).describe("Workspace-relative file path."),
    start_line: READ_FILE_LINE_NUMBER_SCHEMA.optional().describe("Inclusive starting line. If end_line is omitted, reads up to 200 lines from start_line."),
}));
const READABLE_PATH_BASENAME_REPAIR_IGNORED_DIRECTORIES = new Set([
    ".build",
    ".git",
    ".pnpm-store",
    ".state",
    "build",
    "coverage",
    "dist",
    "node_modules",
]);
// ── Tool factories ──────────────────────────────────────────────────────
export function createReadFileTool() {
    return {
        description: "Read one text file from the active workspace. Optionally provide start_line and end_line to read a line range of at most 200 lines; start_line alone reads up to 200 lines from that line.",
        effect: "read",
        executionClass: "read-only",
        idempotent: true,
        inputSchema: READ_FILE_INPUT_SCHEMA,
        name: "read-file",
        source: "built-in",
        capabilities(input, context) {
            return [
                {
                    group: "filesystem",
                    action: "read",
                    path: resolve(context.cwd, input.path),
                },
            ];
        },
        async execute(input, context) {
            const range = resolveReadFileRange(input);
            if (range) {
                return readWorkspaceFileRange(context, {
                    end_line: range.end_line,
                    path: input.path,
                    start_line: range.start_line,
                });
            }
            return readWorkspaceFile(context, input.path);
        },
        evidenceExtractor(input, output) {
            const range = resolveReadFileRange(input);
            return [
                workspaceReadEvidence("read-file", [input.path], output, range ? { end_line: range.end_line, start_line: range.start_line } : {}),
            ];
        },
        formatInput(input) {
            const range = resolveReadFileRange(input);
            if (range) {
                return `${input.path}:${range.start_line}-${range.end_line}`;
            }
            return input.path;
        },
        legacyAdapter: {
            adapt(input, context) {
                const normalized = normalizeReadFileInput(input, context);
                validateReadFileInputPaths(normalized, context);
                return normalized;
            },
        },
    };
}
function workspaceReadEvidence(operation, paths, output, extraPayload = {}) {
    const uniquePaths = Array.from(new Set(paths));
    return {
        confidence: 0.85,
        kind: "workspace-fact",
        payload: {
            operation,
            outputPreview: output.slice(0, 500),
            paths: uniquePaths,
            ...extraPayload,
        },
        relatedFilePaths: uniquePaths,
        summary: uniquePaths.length === 1
            ? `Read workspace file ${uniquePaths[0]}.`
            : `Read ${uniquePaths.length} workspace files.`,
    };
}
// ── Exported formatters (used by write-tools.ts) ────────────────────────
export function formatReadFileOutput(path, content, bytes) {
    const lines = splitTextFileLines(content);
    if (bytes <= READ_FILE_INLINE_MAX_BYTES && lines.length <= READ_FILE_INLINE_MAX_LINES) {
        return content;
    }
    const truncate = (line) => line.length <= READ_FILE_LINE_PREVIEW_CHARS
        ? line
        : line.slice(0, READ_FILE_LINE_PREVIEW_CHARS);
    return buildReadFilePreview({
        bytes,
        headLines: lines.slice(0, READ_FILE_PREVIEW_HEAD_LINES).map(truncate),
        mode: lines.length > READ_FILE_PREVIEW_MAX_LINES ? "range-required" : "preview",
        path,
        tailLines: lines.slice(-READ_FILE_PREVIEW_TAIL_LINES).map(truncate),
        totalLines: lines.length,
    });
}
export function formatLargeReadFileOutput(path, input) {
    return buildReadFilePreview({
        bytes: input.bytes,
        headLines: input.headLines,
        mode: "range-required",
        path,
        tailLines: input.tailLines,
        totalLines: input.totalLines,
    });
}
// ── Input normalization ─────────────────────────────────────────────────
function normalizeReadFileInput(input, context) {
    if (Array.isArray(input)) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "read-file reads one workspace file per call. Pass a single path string.");
    }
    if (typeof input === "string") {
        const trimmed = input.trim();
        if (isStringifiedPathArray(trimmed)) {
            throw new BestAgentError("TOOL_INPUT_INVALID", "read-file reads one workspace file per call. Pass a single path string.");
        }
        const path = normalizeReadableWorkspacePath(context, trimmed);
        if (!path) {
            throw new BestAgentError("TOOL_INPUT_NOT_FOUND", "read-file requires a non-empty workspace path.");
        }
        return { path };
    }
    if (!isPlainObject(input)) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "read-file requires an object input with a path field.");
    }
    if (Array.isArray(input.path)) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "read-file reads one workspace file per call. Pass a single path string.");
    }
    if (typeof input.path !== "string") {
        throw new BestAgentError("TOOL_INPUT_INVALID", "read-file requires an object input with a path field.");
    }
    const rawPath = input.path.trim();
    if (isStringifiedPathArray(rawPath)) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "read-file reads one workspace file per call. Pass a single path string.");
    }
    const path = normalizeReadableWorkspacePath(context, rawPath);
    if (!path) {
        throw new BestAgentError("TOOL_INPUT_NOT_FOUND", "read-file requires a non-empty workspace path.");
    }
    const objectRange = readReadFileLineRangeInputBounds(input);
    const start_line = objectRange?.start_line;
    const end_line = objectRange?.end_line;
    const limit = normalizeReadFileLineNumber(input.limit);
    if (input.limit !== undefined && limit === undefined) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "read-file limit must be a positive integer.");
    }
    if (start_line === undefined && end_line === undefined && limit === undefined) {
        return { path };
    }
    const effectiveStartLine = start_line ?? 1;
    return {
        end_line: resolveReadFileEndLine(effectiveStartLine, end_line, limit),
        path,
        start_line: effectiveStartLine,
    };
}
function resolveReadFileRange(input) {
    if (input.start_line === undefined && input.end_line === undefined && input.limit === undefined) {
        return undefined;
    }
    const startLine = input.start_line ?? 1;
    return {
        end_line: resolveReadFileEndLine(startLine, input.end_line, input.limit),
        start_line: startLine,
    };
}
function resolveReadFileEndLine(startLine, endLine, limit) {
    if (endLine !== undefined) {
        return endLine;
    }
    if (limit !== undefined) {
        return startLine + limit - 1;
    }
    return startLine + READ_FILE_RANGE_MAX_LINES - 1;
}
function readReadFileLineRangeInputBounds(input) {
    const start_line = normalizeReadFileLineNumber(input.start_line);
    const end_line = normalizeReadFileLineNumber(input.end_line);
    if ((input.start_line !== undefined && start_line === undefined) ||
        (input.end_line !== undefined && end_line === undefined)) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "read-file range input requires positive integer line numbers.");
    }
    if (start_line !== undefined || end_line !== undefined) {
        return { end_line, start_line };
    }
    return undefined;
}
function normalizeReadFileLineNumber(value) {
    if (value === undefined) {
        return undefined;
    }
    if (typeof value === "number" && Number.isInteger(value) && value >= 1) {
        return value;
    }
    if (typeof value === "string" && /^\d+$/u.test(value.trim())) {
        return Number(value.trim());
    }
    return undefined;
}
function isStringifiedPathArray(value) {
    const trimmed = value.trim();
    if (!trimmed.startsWith("[")) {
        return false;
    }
    const parsed = parseToolInputJsonValue(trimmed);
    return Array.isArray(parsed);
}
function validateReadFileInputPaths(input, context) {
    const resolved = resolve(context.cwd, input.path);
    if (!context.sandbox.exists(resolved)) {
        throw missingReadableWorkspacePathError("read-file", context, input.path, resolved);
    }
    if (context.sandbox.isDirectory(resolved)) {
        throw new BestAgentError("TOOL_INPUT_NOT_FOUND", `Path ${resolved} is a directory. read-file requires a file path.`);
    }
}
// ── Workspace file reading ──────────────────────────────────────────────
async function readWorkspaceFile(context, relativePath) {
    if (isStringifiedPathArray(relativePath)) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "read-file reads one workspace file per call. Pass a single path string.");
    }
    const path = resolve(context.cwd, relativePath);
    if (!context.sandbox.exists(path)) {
        throw missingReadableWorkspacePathError("read-file", context, relativePath, path);
    }
    if (context.sandbox.isDirectory(path)) {
        throw new BestAgentError("TOOL_INPUT_NOT_FOUND", `Path ${path} is a directory. read-file requires a file path.`);
    }
    const stats = context.sandbox.statFile(path);
    if (context.sandbox.looksLikeBinary(path)) {
        context.recordWorkspaceRead(path);
        return `File: ${relativePath}\nBytes: ${stats.bytes}\nBinary file detected — cannot display as text. Use an appropriate tool or command to inspect this file.`;
    }
    if (stats.bytes > READ_FILE_PREVIEW_MAX_BYTES) {
        const preview = await context.sandbox.inspectTextFile(path, {
            headLines: READ_FILE_PREVIEW_HEAD_LINES,
            tailLines: READ_FILE_PREVIEW_TAIL_LINES,
            maxCharsPerLine: READ_FILE_LINE_PREVIEW_CHARS,
        });
        context.recordWorkspaceRead(path);
        return formatLargeReadFileOutput(relativePath, preview);
    }
    const content = await context.sandbox.readTextFile(path);
    context.recordWorkspaceRead(path);
    return formatReadFileOutput(relativePath, content, stats.bytes);
}
async function readWorkspaceFileRange(context, input) {
    if (input.start_line === undefined || input.end_line === undefined) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "read-file range input requires both start_line and end_line.");
    }
    if (input.start_line < 1 || input.end_line < 1) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "read-file requires line numbers greater than or equal to 1.");
    }
    if (input.end_line < input.start_line) {
        throw new BestAgentError("TOOL_INPUT_INVALID", "read-file requires end_line to be greater than or equal to start_line.");
    }
    const requestedEndLine = input.end_line;
    const effectiveEndLine = input.end_line - input.start_line + 1 > READ_FILE_RANGE_MAX_LINES
        ? input.start_line + READ_FILE_RANGE_MAX_LINES - 1
        : input.end_line;
    const path = resolve(context.cwd, input.path.trim());
    if (!context.sandbox.exists(path)) {
        throw missingReadableWorkspacePathError("read-file", context, input.path.trim(), path);
    }
    if (context.sandbox.isDirectory(path)) {
        throw new BestAgentError("TOOL_INPUT_NOT_FOUND", `Path ${path} is a directory. read-file requires a file path.`);
    }
    if (context.sandbox.looksLikeBinary(path)) {
        const stats = context.sandbox.statFile(path);
        context.recordWorkspaceRead(path);
        return `File: ${input.path}\nBytes: ${stats.bytes}\nBinary file detected — cannot display as text. Use an appropriate tool or command to inspect this file.`;
    }
    const result = await context.sandbox.readTextFileRange(path, input.start_line, effectiveEndLine, {
        maxCharsPerLine: READ_FILE_LINE_PREVIEW_CHARS,
    });
    context.recordWorkspaceRead(path);
    return formatReadFileLineRangeOutput(input.path, input.start_line, requestedEndLine, result.lines, result.endLine, effectiveEndLine !== requestedEndLine);
}
export async function readScratchFile(context, relativePath) {
    const path = resolve(context.threadPaths.scratchDir, relativePath);
    if (!context.sandbox.exists(path)) {
        throw new BestAgentError("TOOL_INPUT_NOT_FOUND", `Path ${path} does not exist in scratch.`);
    }
    if (context.sandbox.isDirectory(path)) {
        throw new BestAgentError("TOOL_INPUT_NOT_FOUND", `Path ${path} is a directory. read-scratch requires a file path.`);
    }
    const stats = context.sandbox.statFile(path);
    if (context.sandbox.looksLikeBinary(path)) {
        return `File: ${relativePath}\nBytes: ${stats.bytes}\nBinary file detected — cannot display as text. Use an appropriate tool or command to inspect this file.`;
    }
    if (stats.bytes > READ_FILE_PREVIEW_MAX_BYTES) {
        const preview = await context.sandbox.inspectTextFile(path, {
            headLines: READ_FILE_PREVIEW_HEAD_LINES,
            tailLines: READ_FILE_PREVIEW_TAIL_LINES,
            maxCharsPerLine: READ_FILE_LINE_PREVIEW_CHARS,
        });
        return formatLargeReadFileOutput(relativePath, preview);
    }
    const content = await context.sandbox.readTextFile(path);
    return formatReadFileOutput(relativePath, content, stats.bytes);
}
// ── Path normalization and repair ───────────────────────────────────────
function normalizeReadableWorkspacePath(context, path) {
    const normalized = path.trim();
    if (!normalized || !context) {
        return normalized;
    }
    const resolved = resolve(context.cwd, normalized);
    if (context.sandbox.exists(resolved) && !context.sandbox.isDirectory(resolved)) {
        return normalized;
    }
    const repaired = inferSingleSegmentWorkspacePathRepair(context, normalized);
    if (repaired) {
        return repaired;
    }
    const replaced = inferSingleSegmentWorkspacePathReplacement(context, normalized);
    if (replaced) {
        return replaced;
    }
    const basenameCandidates = findReadableWorkspaceBasenameCandidates(context, normalized, 2);
    return basenameCandidates.length === 1 ? basenameCandidates[0] : normalized;
}
function missingReadableWorkspacePathError(toolName, context, requestedPath, resolvedPath) {
    const candidates = findReadableWorkspaceBasenameCandidates(context, requestedPath, 6);
    if (candidates.length > 0) {
        return new BestAgentError("TOOL_INPUT_NOT_FOUND", `Path ${resolvedPath} does not exist in the workspace. Did you mean one of: ${candidates.join(", ")}?`);
    }
    return new BestAgentError("TOOL_INPUT_NOT_FOUND", `Path ${resolvedPath} does not exist in the workspace.`);
}
function findReadableWorkspaceBasenameCandidates(context, path, maxCandidates) {
    if (!isSingleSegmentRelativePath(path)) {
        return [];
    }
    const matches = [];
    const walk = (directory) => {
        if (matches.length >= maxCandidates) {
            return;
        }
        if (isInsideThreadStorage(context, directory)) {
            return;
        }
        for (const entry of readDirectoryEntries(directory)) {
            const absolutePath = resolve(directory, entry.name);
            if (entry.isDirectory()) {
                if (!READABLE_PATH_BASENAME_REPAIR_IGNORED_DIRECTORIES.has(entry.name)) {
                    walk(absolutePath);
                }
                continue;
            }
            if (!entry.isFile() || entry.name !== path) {
                continue;
            }
            matches.push(relative(context.cwd, absolutePath).replace(/\\/g, "/"));
            if (matches.length >= maxCandidates) {
                return;
            }
        }
    };
    walk(context.cwd);
    return matches.sort((left, right) => left.localeCompare(right));
}
function isSingleSegmentRelativePath(path) {
    const normalized = path.trim();
    return (normalized.length > 0 &&
        normalized !== "." &&
        normalized !== ".." &&
        !normalized.startsWith("/") &&
        !/^[A-Z]:[\\/]/i.test(normalized) &&
        !normalized.includes("/") &&
        !normalized.includes("\\"));
}
function isInsideThreadStorage(context, directory) {
    const threadDir = context.threadPaths.threadDir;
    return directory === threadDir || directory.startsWith(`${threadDir}/`);
}
function readDirectoryEntries(path) {
    try {
        return readdirSync(path, { withFileTypes: true });
    }
    catch {
        return [];
    }
}
function inferSingleSegmentWorkspacePathRepair(context, path) {
    if (path.startsWith("/")) {
        return undefined;
    }
    const segments = path
        .split("/")
        .map((segment) => segment.trim())
        .filter(Boolean);
    if (segments.length < 2) {
        return undefined;
    }
    const candidates = new Set();
    for (let index = 0; index < segments.length - 1; index += 1) {
        const candidate = [...segments.slice(0, index), ...segments.slice(index + 1)].join("/");
        if (!candidate) {
            continue;
        }
        const resolved = resolve(context.cwd, candidate);
        if (context.sandbox.exists(resolved) && !context.sandbox.isDirectory(resolved)) {
            candidates.add(candidate);
        }
    }
    return candidates.size === 1 ? [...candidates][0] : undefined;
}
function inferSingleSegmentWorkspacePathReplacement(context, path) {
    if (path.startsWith("/")) {
        return undefined;
    }
    const segments = path
        .split("/")
        .map((segment) => segment.trim())
        .filter(Boolean);
    if (segments.length < 2) {
        return undefined;
    }
    const candidates = new Set();
    for (let index = 0; index < segments.length; index += 1) {
        const parentSegments = segments.slice(0, index);
        const parentPath = resolve(context.cwd, ...parentSegments);
        if (!context.sandbox.exists(parentPath) || !context.sandbox.isDirectory(parentPath)) {
            continue;
        }
        const suffix = segments.slice(index + 1);
        const currentSegment = segments[index] ?? "";
        for (const sibling of readDirectoryNames(parentPath)) {
            if (sibling === currentSegment || !segmentsMatchForPathRepair(currentSegment, sibling)) {
                continue;
            }
            const candidate = [...parentSegments, sibling, ...suffix].join("/");
            if (!candidate) {
                continue;
            }
            const resolvedCandidate = resolve(context.cwd, candidate);
            if (context.sandbox.exists(resolvedCandidate) &&
                !context.sandbox.isDirectory(resolvedCandidate)) {
                candidates.add(candidate);
            }
        }
    }
    return candidates.size === 1 ? [...candidates][0] : undefined;
}
function readDirectoryNames(path) {
    try {
        return readdirSync(path);
    }
    catch {
        return [];
    }
}
function segmentsMatchForPathRepair(requested, candidate) {
    const requestedTokens = tokenizePathRepairSegment(requested);
    const candidateTokens = tokenizePathRepairSegment(candidate);
    if (requestedTokens.length === 0 || candidateTokens.length === 0) {
        return false;
    }
    const requestedSet = new Set(requestedTokens);
    const candidateSet = new Set(candidateTokens);
    const sharedCount = [...requestedSet].filter((token) => candidateSet.has(token)).length;
    return sharedCount > 0 && sharedCount === Math.min(requestedSet.size, candidateSet.size);
}
function tokenizePathRepairSegment(value) {
    return value
        .toLowerCase()
        .split(/[^a-z0-9]+/i)
        .map((token) => token.trim())
        .filter(Boolean);
}
// ── Output formatting ───────────────────────────────────────────────────
function buildReadFilePreview(input) {
    const headEndLine = Math.min(input.headLines.length, input.totalLines);
    const tailStartLine = Math.max(1, input.totalLines - input.tailLines.length + 1);
    const lines = [
        `File: ${input.path}`,
        `Bytes: ${input.bytes}`,
        `Lines: ${input.totalLines}`,
        input.mode === "preview"
            ? "Mode: preview (full contents not inlined)"
            : 'Mode: range-required (use read-file with {"path":"...","start_line":N,"end_line":M})',
    ];
    if (input.totalLines === 0) {
        lines.push("(empty file)");
        return lines.join("\n");
    }
    lines.push("", `--- lines 1-${headEndLine} ---`, ...formatPreviewLines(input.headLines, 1));
    if (tailStartLine > headEndLine) {
        lines.push("", `--- lines ${tailStartLine}-${input.totalLines} ---`, ...formatPreviewLines(input.tailLines, tailStartLine));
    }
    return lines.join("\n");
}
function formatReadFileLineRangeOutput(path, requestedStartLine, requestedEndLine, lines, actualEndLine, clippedRange = false) {
    if (lines.length === 0) {
        return `File: ${path}\nRequested lines: ${requestedStartLine}-${requestedEndLine}\nNo lines were found in that range.`;
    }
    return [
        `File: ${path}`,
        `Requested lines: ${requestedStartLine}-${requestedEndLine}`,
        `Returned lines: ${requestedStartLine}-${actualEndLine}`,
        ...(clippedRange ? [`Range clipped to ${READ_FILE_RANGE_MAX_LINES} lines.`] : []),
        "",
        ...formatPreviewLines(lines, requestedStartLine),
    ].join("\n");
}
function formatPreviewLines(lines, start_line) {
    return lines.map((line, index) => {
        if (typeof line === "string") {
            return `${start_line + index} | ${truncatePreviewLine(line)}`;
        }
        const preview = truncatePreviewLine(line.content);
        if (!line.truncated) {
            return `${start_line + index} | ${preview}`;
        }
        return `${start_line + index} | ${preview}... [line truncated at ${READ_FILE_LINE_PREVIEW_CHARS} chars]`;
    });
}
function truncatePreviewLine(value) {
    return truncateText(value, READ_FILE_LINE_PREVIEW_CHARS);
}
//# sourceMappingURL=read-tools.js.map