// ABOUTME: File reading tool with schema-based validation and line range support
// ABOUTME: Safe file access with AI-optimized error messages and misspelling suggestions

import { z } from 'zod';
import { Tool } from '../tool';
import { FilePath, LineNumber } from '../schemas/common';
import type { ToolResult, ToolContext } from '../types';
import type { RuntimePath, ToolRuntime } from '../runtime/types';
import { findSimilarPaths } from '../utils/file-suggestions';
import { formatFileSize } from '@lace/agent/tools/utils/format-file-size';

const MAX_FILE_SIZE = 64 * 1024; // 64KB limit for whole file reads
const MAX_RANGE_SIZE = 2000; // Maximum lines in a ranged read

const fileReadSchema = z
  .object({
    path: FilePath,
    startLine: LineNumber.optional(),
    endLine: LineNumber.optional(),
  })
  .refine(
    (data) => {
      if (data.startLine && data.endLine) {
        return data.endLine >= data.startLine;
      }
      return true;
    },
    {
      message: 'endLine must be >= startLine',
      path: ['endLine'],
    }
  );

export class FileReadTool extends Tool {
  name = 'file_read';
  description = `Read file contents using cat -n format, with line numbers starting at 1. Full reads limited to ${Math.floor(MAX_FILE_SIZE / 1024)}KB, use ranges of up to ${MAX_RANGE_SIZE} lines (startLine/endLine) for larger files. If you request more than ${MAX_RANGE_SIZE} lines, you'll get the first ${MAX_RANGE_SIZE} lines with metadata indicating truncation.`;
  schema = fileReadSchema;
  annotations = {
    readOnlyHint: true,
    idempotentHint: true,
    readOnlySafe: true,
  };

  protected async executeValidated(
    args: z.infer<typeof fileReadSchema>,
    context: ToolContext
  ): Promise<ToolResult> {
    if (context.signal.aborted) {
      return this.createCancellationResult();
    }

    if (!context.runtime) {
      return this.createError('Tool context missing runtime. This is a system error.');
    }

    let runtimePath: RuntimePath | undefined;

    try {
      runtimePath = await context.runtime.paths.resolve(args.path);

      // Check file size before reading (unless using line range)
      if (!args.startLine && !args.endLine) {
        const sizeError = await this.validateFileSizeForWholeRead(context.runtime, runtimePath);
        if (sizeError) {
          return this.createError(sizeError);
        }
      }

      // Read file content
      const content = await context.runtime.fs.readTextFile(runtimePath);
      const lines = content.split('\n');

      // Validate line numbers against actual file content
      if (args.startLine && args.startLine > lines.length) {
        return this.createError(
          `Line ${args.startLine} exceeds file length (${lines.length} lines). Use a line number between 1 and ${lines.length}.`
        );
      }

      // Apply line range if specified
      let resultContent: string;
      let linesReturned: number;
      let startLineNum: number;
      let endLineNum: number;
      let wasTruncated = false;
      let requestedRange: { start: number; end: number } | undefined;

      if (args.startLine || args.endLine) {
        const start = Math.max(0, (args.startLine || 1) - 1);
        let end = args.endLine !== undefined ? Math.min(lines.length, args.endLine) : lines.length;

        // Check if requested range exceeds MAX_RANGE_SIZE
        const requestedSize = end - start;
        if (requestedSize > MAX_RANGE_SIZE) {
          wasTruncated = true;
          requestedRange = {
            start: args.startLine || 1,
            end: args.endLine || lines.length,
          };
          end = start + MAX_RANGE_SIZE;
        }

        const resultLines = lines.slice(start, end);
        startLineNum = start + 1;
        endLineNum = end;
        linesReturned = resultLines.length;

        // Add line numbers to output (cat -n format)
        const maxLineNum = startLineNum + resultLines.length - 1;
        const width = String(maxLineNum).length;
        resultContent = resultLines
          .map((line, idx) => {
            const lineNum = startLineNum + idx;
            return `${String(lineNum).padStart(width, ' ')}\t${line}`;
          })
          .join('\n');
      } else {
        // Full file read - add line numbers (cat -n format)
        startLineNum = 1;
        endLineNum = lines.length;
        linesReturned = lines.length;
        const width = String(lines.length).length;
        resultContent = lines
          .map((line, idx) => `${String(idx + 1).padStart(width, ' ')}\t${line}`)
          .join('\n');
      }

      const metadata: Record<string, unknown> = {
        displayPath: runtimePath.displayPath,
        totalLines: lines.length,
        linesReturned,
        range: { start: startLineNum, end: endLineNum },
        fileSize: formatFileSize(content.length),
      };

      if (wasTruncated && requestedRange) {
        metadata.warning = `Requested ${requestedRange.end - requestedRange.start + 1} lines (${requestedRange.start}-${requestedRange.end}), but limit is ${MAX_RANGE_SIZE}. Returned first ${MAX_RANGE_SIZE} lines (${startLineNum}-${endLineNum}). File has ${lines.length} total lines.`;
        metadata.truncated = true;
        metadata.requestedRange = requestedRange;
      }

      context.markFileRead?.(runtimePath);
      return this.createResult(resultContent, metadata);
    } catch (error: unknown) {
      // Type guard for Node.js filesystem errors
      const isNodeError = (err: unknown): err is { code: string; message: string } => {
        return (
          err instanceof Error &&
          'code' in err &&
          typeof (err as { code: unknown }).code === 'string'
        );
      };

      // Handle file not found with helpful suggestions
      if (isNodeError(error) && error.code === 'ENOENT') {
        const suggestionPath = runtimePath?.hostPath ?? runtimePath?.runtimePath;
        const suggestions =
          runtimePath?.hostPath && suggestionPath ? await findSimilarPaths(suggestionPath) : [];
        const suggestionText =
          suggestions.length > 0 ? `\n\nSimilar files: ${suggestions.join(', ')}` : '';

        return this.createError(`File not found: ${args.path}${suggestionText}`);
      }

      // Handle permission errors
      if (isNodeError(error) && error.code === 'EACCES') {
        return this.createError(
          `Permission denied accessing file: ${args.path}. Check file permissions and try again.`
        );
      }

      // Handle other file system errors
      if (isNodeError(error)) {
        return this.createError(
          `File system error (${error.code}): ${error.message}. Check the file path and try again.`
        );
      }

      // Return error for unexpected errors instead of throwing
      return this.createError(
        `Unexpected error reading file: ${error instanceof Error ? error.message : 'Unknown error'}`
      );
    }
  }

  private async validateFileSizeForWholeRead(
    runtime: ToolRuntime,
    runtimePath: RuntimePath
  ): Promise<string | null> {
    try {
      const fileStats = await runtime.fs.stat(runtimePath);
      if (fileStats.size > MAX_FILE_SIZE) {
        const fileSizeFormatted = formatFileSize(fileStats.size);
        return `File is too large (${fileSizeFormatted}) for whole-file read. Use startLine and endLine parameters for ranged reads (e.g., startLine: 1, endLine: 100). File size limit is ${formatFileSize(
          MAX_FILE_SIZE
        )} for whole-file reads.`;
      }
      return null;
    } catch {
      // If we can't stat the file, let the main read operation handle the error
      return null;
    }
  }
}
