/**
 * ReadTool — read a file and return its contents with line numbers (cat -n style).
 *
 * Supports offset/limit for partial reads. Detects binary files and refuses to
 * return their raw bytes. Default limit is 2000 lines.
 */

import { readFile, stat } from 'node:fs/promises';

import { z } from 'zod';

import { checkPathWithinCwd } from './path-guard.js';
import { createZodFunctionTool } from '../implementations/function-tool';

import type { FunctionTool } from '../implementations/function-tool';
import type { ISandboxToolOptions } from '../sandbox/types.js';
import type { TToolResult } from '../types/tool-result.js';

const DEFAULT_LIMIT = 2000;

const ReadSchema = z.object({
  filePath: z.string().describe('The absolute path to the file to read'),
  offset: z
    .number()
    .optional()
    .describe(
      'The line number to start reading from (1-based). Only provide if the file is too large to read at once',
    ),
  limit: z
    .number()
    .optional()
    .describe(
      `The number of lines to read (default: ${DEFAULT_LIMIT}). Only provide if the file is too large to read at once`,
    ),
});

type TReadArgs = z.infer<typeof ReadSchema>;

/**
 * Heuristic binary detection: scan the first 8 KB for null bytes.
 */
function isBinary(buffer: Buffer): boolean {
  const checkLength = Math.min(buffer.length, 8192);
  for (let i = 0; i < checkLength; i++) {
    if (buffer[i] === 0) return true;
  }
  return false;
}

/**
 * Format lines with 1-based line numbers in cat -n style.
 * Pads line number to the width of the highest line number.
 */
function formatWithLineNumbers(lines: string[], startLine: number): string {
  const lastLineNum = startLine + lines.length - 1;
  const width = String(lastLineNum).length;
  return lines
    .map((line, idx) => {
      const lineNum = String(startLine + idx).padStart(width, ' ');
      return `${lineNum}\t${line}`;
    })
    .join('\n');
}

function formatReadResult(
  filePath: string,
  content: string,
  startLine: number,
  limit: number,
): string {
  const allLines = content.split('\n');

  // Remove trailing empty line if file ends with newline (common in Unix files)
  if (allLines[allLines.length - 1] === '') {
    allLines.pop();
  }

  const zeroBasedStart = startLine - 1;
  const selectedLines = allLines.slice(zeroBasedStart, zeroBasedStart + limit);

  const output = formatWithLineNumbers(selectedLines, startLine);

  const totalLines = allLines.length;
  const returnedLines = selectedLines.length;
  const header =
    returnedLines < totalLines
      ? `[File: ${filePath} (lines ${startLine}-${startLine + returnedLines - 1} of ${totalLines})]\n`
      : `[File: ${filePath} (${totalLines} lines)]\n`;

  const result: TToolResult = {
    success: true,
    output: header + output,
  };
  return JSON.stringify(result);
}

async function readFileTool(args: TReadArgs, options: ISandboxToolOptions = {}): Promise<string> {
  const { filePath, offset, limit = DEFAULT_LIMIT } = args;
  const startLine = offset !== undefined && offset > 0 ? offset : 1;

  if (options.sandboxClient) {
    try {
      const content = await options.sandboxClient.readFile(filePath);
      return formatReadResult(filePath, content, startLine, limit);
    } catch (err) {
      // allow-fallback: sandbox read failure → surface as TToolResult error
      const result: TToolResult = {
        success: false,
        output: '',
        error: err instanceof Error ? err.message : String(err),
      };
      return JSON.stringify(result);
    }
  }

  const pathError = checkPathWithinCwd(filePath, options.cwd);
  if (pathError !== undefined) return pathError;

  let fileStats: Awaited<ReturnType<typeof stat>> | undefined;
  try {
    fileStats = await stat(filePath);
  } catch (err) {
    // allow-fallback: stat failure means file not found → TToolResult error
    const result: TToolResult = {
      success: false,
      output: '',
      error: `File not found: ${filePath}`,
    };
    return JSON.stringify(result);
  }

  if (!fileStats.isFile()) {
    const result: TToolResult = {
      success: false,
      output: '',
      error: `Path is not a file: ${filePath}`,
    };
    return JSON.stringify(result);
  }

  let buffer: Buffer;
  try {
    buffer = await readFile(filePath);
  } catch (err) {
    // allow-fallback: read failure → TToolResult error (permissions, locks)
    const result: TToolResult = {
      success: false,
      output: '',
      error: err instanceof Error ? err.message : String(err),
    };
    return JSON.stringify(result);
  }

  if (isBinary(buffer)) {
    const result: TToolResult = {
      success: false,
      output: '',
      error: `Binary file not supported: ${filePath}`,
    };
    return JSON.stringify(result);
  }

  const content = buffer.toString('utf8');
  return formatReadResult(filePath, content, startLine, limit);
}

/**
 * Create a ReadTool instance — register with Robota agent tools registry.
 */
export function createReadTool(options: ISandboxToolOptions = {}): FunctionTool {
  return createZodFunctionTool(
    'Read',
    'Reads a file from the local filesystem.\n\nBy default, reads up to 2000 lines from the beginning of the file. You can optionally specify offset and limit for partial reads.\n\nResults are returned using cat -n format, with line numbers starting at 1.\n\nThe file_path parameter must be an absolute path, not a relative path.',
    ReadSchema,
    async (params) => {
      return readFileTool(params as TReadArgs, options);
    },
  );
}

/**
 * ReadTool instance — register with Robota agent tools registry.
 */
export const readTool = createReadTool();
