import { randomBytes } from "node:crypto";
import { createWriteStream, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { AgentTool } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
import { spawn } from "child_process";
import { getShellConfig, killProcessTree } from "../../utils/shell.js";
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js";

/**
 * Generate a unique temp file path for bash output
 */
function getTempFilePath(): string {
	const id = randomBytes(8).toString("hex");
	return join(tmpdir(), `pi-bash-${id}.log`);
}

const bashSchema = Type.Object({
	command: Type.String({ description: "Bash command to execute" }),
	timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
});

export interface BashToolDetails {
	truncation?: TruncationResult;
	fullOutputPath?: string;
}

export function createBashTool(cwd: string): AgentTool<typeof bashSchema> {
	return {
		name: "bash",
		label: "bash",
		description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
		parameters: bashSchema,
		execute: async (
			_toolCallId: string,
			{ command, timeout }: { command: string; timeout?: number },
			signal?: AbortSignal,
			onUpdate?,
		) => {
			return new Promise((resolve, reject) => {
				const { shell, args } = getShellConfig();

				if (!existsSync(cwd)) {
					throw new Error(`Working directory does not exist: ${cwd}\nCannot execute bash commands.`);
				}

				const child = spawn(shell, [...args, command], {
					cwd,
					detached: true,
					stdio: ["ignore", "pipe", "pipe"],
				});

				// We'll stream to a temp file if output gets large
				let tempFilePath: string | undefined;
				let tempFileStream: ReturnType<typeof createWriteStream> | undefined;
				let totalBytes = 0;

				// Keep a rolling buffer of the last chunk for tail truncation
				const chunks: Buffer[] = [];
				let chunksBytes = 0;
				// Keep more than we need so we have enough for truncation
				const maxChunksBytes = DEFAULT_MAX_BYTES * 2;

				let timedOut = false;

				// Set timeout if provided
				let timeoutHandle: NodeJS.Timeout | undefined;
				if (timeout !== undefined && timeout > 0) {
					timeoutHandle = setTimeout(() => {
						timedOut = true;
						onAbort();
					}, timeout * 1000);
				}

				const handleData = (data: Buffer) => {
					totalBytes += data.length;

					// Start writing to temp file once we exceed the threshold
					if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
						tempFilePath = getTempFilePath();
						tempFileStream = createWriteStream(tempFilePath);
						// Write all buffered chunks to the file
						for (const chunk of chunks) {
							tempFileStream.write(chunk);
						}
					}

					// Write to temp file if we have one
					if (tempFileStream) {
						tempFileStream.write(data);
					}

					// Keep rolling buffer of recent data
					chunks.push(data);
					chunksBytes += data.length;

					// Trim old chunks if buffer is too large
					while (chunksBytes > maxChunksBytes && chunks.length > 1) {
						const removed = chunks.shift()!;
						chunksBytes -= removed.length;
					}

					// Stream partial output to callback (truncated rolling buffer)
					if (onUpdate) {
						const fullBuffer = Buffer.concat(chunks);
						const fullText = fullBuffer.toString("utf-8");
						const truncation = truncateTail(fullText);
						onUpdate({
							content: [{ type: "text", text: truncation.content || "" }],
							details: {
								truncation: truncation.truncated ? truncation : undefined,
								fullOutputPath: tempFilePath,
							},
						});
					}
				};

				// Collect stdout and stderr together
				if (child.stdout) {
					child.stdout.on("data", handleData);
				}
				if (child.stderr) {
					child.stderr.on("data", handleData);
				}

				// Handle shell spawn errors to prevent session from crashing
				child.on("error", (err) => {
					if (timeoutHandle) {
						clearTimeout(timeoutHandle);
					}
					if (signal) {
						signal.removeEventListener("abort", onAbort);
					}
					reject(err);
				});

				// Handle process exit
				child.on("close", (code) => {
					if (timeoutHandle) {
						clearTimeout(timeoutHandle);
					}
					if (signal) {
						signal.removeEventListener("abort", onAbort);
					}

					// Close temp file stream
					if (tempFileStream) {
						tempFileStream.end();
					}

					// Combine all buffered chunks
					const fullBuffer = Buffer.concat(chunks);
					const fullOutput = fullBuffer.toString("utf-8");

					if (signal?.aborted) {
						let output = fullOutput;
						if (output) output += "\n\n";
						output += "Command aborted";
						reject(new Error(output));
						return;
					}

					if (timedOut) {
						let output = fullOutput;
						if (output) output += "\n\n";
						output += `Command timed out after ${timeout} seconds`;
						reject(new Error(output));
						return;
					}

					// Apply tail truncation
					const truncation = truncateTail(fullOutput);
					let outputText = truncation.content || "(no output)";

					// Build details with truncation info
					let details: BashToolDetails | undefined;

					if (truncation.truncated) {
						details = {
							truncation,
							fullOutputPath: tempFilePath,
						};

						// Build actionable notice
						const startLine = truncation.totalLines - truncation.outputLines + 1;
						const endLine = truncation.totalLines;

						if (truncation.lastLinePartial) {
							// Edge case: last line alone > 30KB
							const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8"));
							outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
						} else if (truncation.truncatedBy === "lines") {
							outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
						} else {
							outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
						}
					}

					if (code !== 0 && code !== null) {
						outputText += `\n\nCommand exited with code ${code}`;
						reject(new Error(outputText));
					} else {
						resolve({ content: [{ type: "text", text: outputText }], details });
					}
				});

				// Handle abort signal - kill entire process tree
				const onAbort = () => {
					if (child.pid) {
						killProcessTree(child.pid);
					}
				};

				if (signal) {
					if (signal.aborted) {
						onAbort();
					} else {
						signal.addEventListener("abort", onAbort, { once: true });
					}
				}
			});
		},
	};
}

/** Default bash tool using process.cwd() - for backwards compatibility */
export const bashTool = createBashTool(process.cwd());
