// The tool host: WorkspaceToolHost implements the harness ToolPort. It narrows untrusted
// `Record<string,unknown>` arguments, dispatches the 6 tools through a handler map, and returns
// a ToolCallResult whose `output` is already redacted. run_command is the only tool that sets
// commandExecuted:true (the executor counts those against maxCommandExecutions). All filesystem
// reads go through the workspace layer; all writes through the WorkspaceWriter; all spawns through
// the injected SpawnFn — so a unit test needs no real secrets and no real processes.
import { discoverWithStats, readWorkspaceFile, } from "@oscharko-dev/keiko-workspace";
import { nodeWorkspaceFs } from "@oscharko-dev/keiko-workspace/internal/fs";
import { nodeSpawnFn, runCommand } from "./exec.js";
import { CommandCancelledError, ToolArgumentError, UnknownToolError } from "./errors.js";
import { applyPatch, renderDryRun, validatePatch } from "./patch.js";
import { TOOL_DEFINITIONS } from "./schemas.js";
import { nodeWorkspaceWriter } from "./writer.js";
import { resolveToolHostConfig, } from "./types.js";
function requireString(args, key, tool) {
    const value = args[key];
    if (typeof value !== "string" || value.length === 0) {
        throw new ToolArgumentError(`argument '${key}' must be a non-empty string`, tool);
    }
    return value;
}
function optionalString(args, key, tool) {
    const value = args[key];
    if (value === undefined) {
        return undefined;
    }
    if (typeof value !== "string") {
        throw new ToolArgumentError(`argument '${key}' must be a string`, tool);
    }
    return value;
}
function optionalNumber(args, key, tool) {
    const value = args[key];
    if (value === undefined) {
        return undefined;
    }
    if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
        throw new ToolArgumentError(`argument '${key}' must be a non-negative number`, tool);
    }
    return value;
}
function optionalBoolean(args, key, tool) {
    const value = args[key];
    if (value === undefined) {
        return undefined;
    }
    if (typeof value !== "boolean") {
        throw new ToolArgumentError(`argument '${key}' must be a boolean`, tool);
    }
    return value;
}
function optionalStringArray(args, key, tool) {
    const value = args[key];
    if (value === undefined) {
        return undefined;
    }
    if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
        throw new ToolArgumentError(`argument '${key}' must be a string array`, tool);
    }
    return value;
}
function summarizeCommand(result) {
    return JSON.stringify({
        exitCode: result.exitCode,
        signal: result.signal,
        timedOut: result.timedOut,
        truncated: result.truncated,
        stdout: result.stdout,
        stderr: result.stderr,
    });
}
export class WorkspaceToolHost {
    workspace;
    fs;
    writer;
    spawn;
    resolveExecutable;
    config;
    processEnv;
    now;
    constructor(deps) {
        this.workspace = deps.workspace;
        this.fs = deps.fs ?? nodeWorkspaceFs;
        this.writer = deps.writer ?? nodeWorkspaceWriter;
        this.spawn = deps.spawn ?? nodeSpawnFn;
        this.resolveExecutable = deps.resolveExecutable;
        this.config = resolveToolHostConfig(deps.config);
        this.processEnv = deps.processEnv ?? process.env;
        this.now = deps.now ?? Date.now;
    }
    listTools() {
        return TOOL_DEFINITIONS;
    }
    async execute(request) {
        if (request.signal.aborted) {
            throw new CommandCancelledError("tool call cancelled before dispatch");
        }
        const startedAt = this.now();
        const handled = await this.dispatch(request);
        return {
            toolCallId: request.toolCallId,
            output: handled.output,
            durationMs: this.now() - startedAt,
            commandExecuted: handled.commandExecuted,
            ...(handled.metadata === undefined ? {} : { metadata: handled.metadata }),
        };
    }
    dispatch(request) {
        const args = request.arguments;
        switch (request.toolName) {
            case "read_file":
                return Promise.resolve(this.readFile(args));
            case "list_files":
                return Promise.resolve(this.listFiles(args));
            case "inspect_package_scripts":
                return Promise.resolve(this.inspectScripts(args));
            case "run_command":
                return this.runCommandTool(args, request.signal);
            case "propose_patch":
                return Promise.resolve(this.proposePatch(args));
            case "apply_patch":
                return Promise.resolve(this.applyPatchTool(args, request.signal));
            default:
                throw new UnknownToolError(`no such tool: ${request.toolName}`, request.toolName);
        }
    }
    readFile(args) {
        const path = requireString(args, "path", "read_file");
        const maxBytes = optionalNumber(args, "maxBytes", "read_file") ?? this.config.maxReadBytes;
        const content = readWorkspaceFile(this.workspace, path, { maxBytes }, this.fs);
        return { output: JSON.stringify(content), commandExecuted: false };
    }
    listFiles(args) {
        const maxDepth = optionalNumber(args, "maxDepth", "list_files");
        const maxFiles = optionalNumber(args, "maxFiles", "list_files");
        const applyGitignore = optionalBoolean(args, "applyGitignore", "list_files");
        const result = discoverWithStats(this.workspace, {
            maxDepth: maxDepth ?? 12,
            maxFiles: maxFiles ?? 5_000,
            applyGitignore: applyGitignore ?? true,
        }, this.fs);
        return { output: JSON.stringify(result), commandExecuted: false };
    }
    inspectScripts(args) {
        const path = optionalString(args, "path", "inspect_package_scripts") ?? "package.json";
        const content = readWorkspaceFile(this.workspace, path, { maxBytes: this.config.maxReadBytes }, this.fs);
        const scripts = parseScripts(content.text, path);
        return { output: JSON.stringify({ path, scripts }), commandExecuted: false };
    }
    async runCommandTool(args, signal) {
        const command = requireString(args, "command", "run_command");
        const cmdArgs = optionalStringArray(args, "args", "run_command") ?? [];
        const cwd = optionalString(args, "cwd", "run_command");
        const timeoutMs = optionalNumber(args, "timeoutMs", "run_command");
        const result = await runCommand({ command, args: cmdArgs, cwd, timeoutMs, signal }, {
            workspace: this.workspace,
            policy: this.config.sandbox,
            commandRules: this.config.commandRules,
            spawn: this.spawn,
            resolveExecutable: this.resolveExecutable,
            processEnv: this.processEnv,
            now: this.now,
        });
        return {
            output: summarizeCommand(result),
            commandExecuted: true,
            metadata: {
                kind: "command",
                executable: command,
                argCount: cmdArgs.length,
                exitCode: result.exitCode,
                timedOut: result.timedOut,
                sandbox: {
                    envAllowlist: this.config.sandbox.envAllowlist,
                    network: this.config.sandbox.network,
                    maxOutputBytes: this.config.sandbox.maxOutputBytes,
                    timeoutMs: timeoutMs ?? this.config.sandbox.defaultTimeoutMs,
                    terminationGraceMs: this.config.sandbox.terminationGraceMs,
                    cwdRequested: cwd !== undefined,
                },
            },
        };
    }
    proposePatch(args) {
        const diff = requireString(args, "diff", "propose_patch");
        const validation = validatePatch(this.workspace, diff, {
            fs: this.fs,
            limits: this.config.patchLimits,
        });
        return {
            output: JSON.stringify({ validation, preview: renderDryRun(validation) }),
            commandExecuted: false,
        };
    }
    applyPatchTool(args, signal) {
        const diff = requireString(args, "diff", "apply_patch");
        const result = applyPatch(this.workspace, diff, {
            applyEnabled: this.config.applyEnabled,
            signal,
            fs: this.fs,
            writer: this.writer,
            limits: this.config.patchLimits,
        });
        return {
            output: JSON.stringify(result),
            commandExecuted: false,
            metadata: {
                kind: "patch-apply",
                changedFiles: result.changedFiles.length,
                created: result.created.length,
                deleted: result.deleted.length,
            },
        };
    }
}
function parseScripts(text, path) {
    let parsed;
    try {
        parsed = JSON.parse(text);
    }
    catch {
        throw new ToolArgumentError(`${path} is not valid JSON`, "inspect_package_scripts");
    }
    if (typeof parsed !== "object" || parsed === null) {
        return {};
    }
    const scripts = parsed.scripts;
    return typeof scripts === "object" && scripts !== null
        ? scripts
        : {};
}
