import { Type } from '@sinclair/typebox';
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
import { readFile, writeFile, readdir, stat } from 'node:fs/promises';
import { guardPath, guardPathUnsafe, SandboxEscapeError } from './path-guard.js';

const MAX_READ_BYTES = 512 * 1024; // 512 KB read limit
const MAX_WRITE_BYTES = 1024 * 1024; // 1 MB write limit

export function createFileTools(baseDir: string): ToolDefinition[] {
  const readFileTool: ToolDefinition = {
    name: 'fs_read_file',
    label: 'Read File',
    description:
      'Read the contents of a file. Path is resolved relative to the sandbox base directory.',
    parameters: Type.Object({
      path: Type.String({
        description: 'File path (relative to sandbox base or absolute within it)',
      }),
      encoding: Type.Optional(
        Type.String({ description: 'Encoding: utf8 (default), base64, hex' }),
      ),
    }),
    async execute(_toolCallId, params) {
      const { path, encoding } = params as { path: string; encoding?: string };
      let safePath: string;
      try {
        safePath = guardPath(path, baseDir);
      } catch (err) {
        if (err instanceof SandboxEscapeError) {
          return {
            content: [{ type: 'text' as const, text: `Error: ${err.message}` }],
            details: undefined,
          };
        }
        return {
          content: [{ type: 'text' as const, text: `Error: ${String(err)}` }],
          details: undefined,
        };
      }

      try {
        const info = await stat(safePath);
        if (!info.isFile()) {
          return {
            content: [{ type: 'text' as const, text: `Error: path is not a file: ${path}` }],
            details: undefined,
          };
        }
        if (info.size > MAX_READ_BYTES) {
          return {
            content: [
              {
                type: 'text' as const,
                text: `Error: file too large (${info.size} bytes, limit ${MAX_READ_BYTES} bytes)`,
              },
            ],
            details: undefined,
          };
        }
        const enc = (encoding ?? 'utf8') as BufferEncoding;
        const content = await readFile(safePath, { encoding: enc });
        return {
          content: [{ type: 'text' as const, text: String(content) }],
          details: undefined,
        };
      } catch (err) {
        return {
          content: [{ type: 'text' as const, text: `Error reading file: ${String(err)}` }],
          details: undefined,
        };
      }
    },
  };

  const writeFileTool: ToolDefinition = {
    name: 'fs_write_file',
    label: 'Write File',
    description:
      'Write content to a file. Path is resolved relative to the sandbox base directory. Overwrites existing file.',
    parameters: Type.Object({
      path: Type.String({
        description: 'File path (relative to sandbox base or absolute within it)',
      }),
      content: Type.String({ description: 'Content to write' }),
      encoding: Type.Optional(Type.String({ description: 'Encoding: utf8 (default), base64' })),
    }),
    async execute(_toolCallId, params) {
      const { path, content, encoding } = params as {
        path: string;
        content: string;
        encoding?: string;
      };
      let safePath: string;
      try {
        safePath = guardPathUnsafe(path, baseDir);
      } catch (err) {
        if (err instanceof SandboxEscapeError) {
          return {
            content: [{ type: 'text' as const, text: `Error: ${err.message}` }],
            details: undefined,
          };
        }
        return {
          content: [{ type: 'text' as const, text: `Error: ${String(err)}` }],
          details: undefined,
        };
      }

      if (Buffer.byteLength(content, 'utf8') > MAX_WRITE_BYTES) {
        return {
          content: [
            {
              type: 'text' as const,
              text: `Error: content too large (limit ${MAX_WRITE_BYTES} bytes)`,
            },
          ],
          details: undefined,
        };
      }

      try {
        const enc = (encoding ?? 'utf8') as BufferEncoding;
        await writeFile(safePath, content, { encoding: enc });
        return {
          content: [{ type: 'text' as const, text: `File written successfully: ${path}` }],
          details: undefined,
        };
      } catch (err) {
        return {
          content: [{ type: 'text' as const, text: `Error writing file: ${String(err)}` }],
          details: undefined,
        };
      }
    },
  };

  const listDirectoryTool: ToolDefinition = {
    name: 'fs_list_directory',
    label: 'List Directory',
    description: 'List files and directories at a given path within the sandbox base directory.',
    parameters: Type.Object({
      path: Type.Optional(
        Type.String({
          description: 'Directory path (relative to sandbox base). Defaults to base directory.',
        }),
      ),
    }),
    async execute(_toolCallId, params) {
      const { path } = params as { path?: string };
      const target = path ?? '.';
      let safePath: string;
      try {
        safePath = guardPath(target, baseDir);
      } catch (err) {
        if (err instanceof SandboxEscapeError) {
          return {
            content: [{ type: 'text' as const, text: `Error: ${err.message}` }],
            details: undefined,
          };
        }
        return {
          content: [{ type: 'text' as const, text: `Error: ${String(err)}` }],
          details: undefined,
        };
      }

      try {
        const info = await stat(safePath);
        if (!info.isDirectory()) {
          return {
            content: [{ type: 'text' as const, text: `Error: path is not a directory: ${target}` }],
            details: undefined,
          };
        }
        const entries = await readdir(safePath, { withFileTypes: true });
        const items = entries.map((e) => ({
          name: e.name,
          type: e.isDirectory() ? 'directory' : e.isSymbolicLink() ? 'symlink' : 'file',
        }));
        return {
          content: [{ type: 'text' as const, text: JSON.stringify(items, null, 2) }],
          details: undefined,
        };
      } catch (err) {
        return {
          content: [{ type: 'text' as const, text: `Error listing directory: ${String(err)}` }],
          details: undefined,
        };
      }
    },
  };

  const editFileTool: ToolDefinition = {
    name: 'fs_edit_file',
    label: 'Edit File',
    description:
      'Make targeted text replacements in a file. Each edit replaces an exact match of oldText with newText. ' +
      'All edits are matched against the original file content (not incrementally). ' +
      'Each oldText must be unique in the file and edits must not overlap.',
    parameters: Type.Object({
      path: Type.String({
        description: 'File path (relative to sandbox base or absolute within it)',
      }),
      edits: Type.Array(
        Type.Object({
          oldText: Type.String({
            description: 'Exact text to find and replace (must be unique in the file)',
          }),
          newText: Type.String({ description: 'Replacement text' }),
        }),
        { description: 'One or more targeted replacements', minItems: 1 },
      ),
    }),
    async execute(_toolCallId, params) {
      const { path, edits } = params as {
        path: string;
        edits: Array<{ oldText: string; newText: string }>;
      };

      let safePath: string;
      try {
        safePath = guardPath(path, baseDir);
      } catch (err) {
        if (err instanceof SandboxEscapeError) {
          return {
            content: [{ type: 'text' as const, text: `Error: ${err.message}` }],
            details: undefined,
          };
        }
        return {
          content: [{ type: 'text' as const, text: `Error: ${String(err)}` }],
          details: undefined,
        };
      }

      try {
        const info = await stat(safePath);
        if (!info.isFile()) {
          return {
            content: [{ type: 'text' as const, text: `Error: path is not a file: ${path}` }],
            details: undefined,
          };
        }
        if (info.size > MAX_READ_BYTES) {
          return {
            content: [
              {
                type: 'text' as const,
                text: `Error: file too large for editing (${info.size} bytes, limit ${MAX_READ_BYTES} bytes)`,
              },
            ],
            details: undefined,
          };
        }
      } catch (err) {
        return {
          content: [{ type: 'text' as const, text: `Error reading file: ${String(err)}` }],
          details: undefined,
        };
      }

      let content: string;
      try {
        content = await readFile(safePath, { encoding: 'utf8' });
      } catch (err) {
        return {
          content: [{ type: 'text' as const, text: `Error reading file: ${String(err)}` }],
          details: undefined,
        };
      }

      // Validate all edits before applying any
      const errors: string[] = [];
      for (let i = 0; i < edits.length; i++) {
        const edit = edits[i]!;
        const occurrences = content.split(edit.oldText).length - 1;
        if (occurrences === 0) {
          errors.push(`Edit ${i + 1}: oldText not found in file`);
        } else if (occurrences > 1) {
          errors.push(`Edit ${i + 1}: oldText matches ${occurrences} locations (must be unique)`);
        }
      }

      // Check for overlapping edits
      if (errors.length === 0) {
        const positions = edits.map((edit, i) => ({
          index: i,
          start: content.indexOf(edit.oldText),
          end: content.indexOf(edit.oldText) + edit.oldText.length,
        }));
        positions.sort((a, b) => a.start - b.start);
        for (let i = 1; i < positions.length; i++) {
          if (positions[i]!.start < positions[i - 1]!.end) {
            errors.push(
              `Edits ${positions[i - 1]!.index + 1} and ${positions[i]!.index + 1} overlap`,
            );
          }
        }
      }

      if (errors.length > 0) {
        return {
          content: [
            {
              type: 'text' as const,
              text: `Edit validation failed:\n${errors.join('\n')}`,
            },
          ],
          details: undefined,
        };
      }

      // Apply edits: process from end to start to preserve positions
      const positions = edits.map((edit) => ({
        edit,
        start: content.indexOf(edit.oldText),
      }));
      positions.sort((a, b) => b.start - a.start); // reverse order

      let result = content;
      for (const { edit } of positions) {
        result = result.replace(edit.oldText, edit.newText);
      }

      if (Buffer.byteLength(result, 'utf8') > MAX_WRITE_BYTES) {
        return {
          content: [
            {
              type: 'text' as const,
              text: `Error: resulting file too large (limit ${MAX_WRITE_BYTES} bytes)`,
            },
          ],
          details: undefined,
        };
      }

      try {
        await writeFile(safePath, result, { encoding: 'utf8' });
        return {
          content: [
            {
              type: 'text' as const,
              text: `File edited successfully: ${path} (${edits.length} edit(s) applied)`,
            },
          ],
          details: undefined,
        };
      } catch (err) {
        return {
          content: [{ type: 'text' as const, text: `Error writing file: ${String(err)}` }],
          details: undefined,
        };
      }
    },
  };

  return [readFileTool, writeFileTool, listDirectoryTool, editFileTool];
}
