// src/tools/file/read.ts
// Copyright (C) 2025 DTP Technologies, LLC
// All Rights Reserved

import fs from "node:fs/promises";
import type { ToolDefinition } from "../../lib/ai-client.js";

import {
  DtpTool,
  type ToolArguments,
  type ToolContext,
} from "../../lib/tool.js";
import { ChatSessionMode } from "@/models/chat-session.js";
import { validateProjectPath } from "../../lib/path-security.js";
import { PROJECT_ROOT } from "../../config/env.js";
import HostMonitorService from "../../services/host-monitor.js";

const MAX_FILE_SIZE = 50 * 1024; // 50KB

export class FileReadTool extends DtpTool {
  get name(): string {
    return "FileReadTool";
  }
  get slug(): string {
    return "file-read";
  }
  get metadata() {
    return {
      name: this.definition.function.name || "file_read",
      category: "file",
      tags: ["file", "read", "view", "display"],
      modes: [
        ChatSessionMode.Plan,
        ChatSessionMode.Build,
        ChatSessionMode.Test,
        ChatSessionMode.Ship,
        ChatSessionMode.Develop,
      ],
    };
  }

  public definition: ToolDefinition = {
    type: "function",
    function: {
      name: "file_read",
      description:
        "Read the contents of a file with line numbers. Returns text with numbered lines. Supports reading a specific line range. Binary files cannot be displayed.",
      parameters: {
        type: "object",
        properties: {
          path: {
            type: "string",
            description: "Path to the file to read (relative or absolute).",
          },
          startLine: {
            type: "number",
            description: "Starting line number (1-indexed). Defaults to 1.",
          },
          endLine: {
            type: "number",
            description:
              "Ending line number (inclusive). Defaults to end of file.",
          },
        },
        required: ["path"],
      },
    },
  };

  public async execute(
    _context: ToolContext,
    args: ToolArguments,
  ): Promise<string> {
    const filePath = args.path as string | undefined;

    if (!filePath || filePath.trim().length === 0) {
      return this.error("MISSING_PARAMETER", "File path must not be empty.", {
        parameter: "path",
        recoveryHint: "Provide a valid file path.",
      });
    }

    const startLine = (args.startLine as number | undefined) ?? 1;
    const endLine = args.endLine as number | undefined;

    if (startLine < 1) {
      return this.error("INVALID_PARAMETER", "startLine must be >= 1.", {
        parameter: "startLine",
        expected: "A positive integer >= 1",
      });
    }

    if (endLine !== undefined && endLine < startLine) {
      return this.error("INVALID_PARAMETER", "endLine must be >= startLine.", {
        parameter: "endLine",
        expected: "An integer >= startLine",
      });
    }

    // Validate path security - prevent path traversal attacks
    const pathValidation = validateProjectPath(filePath, PROJECT_ROOT);
    if (!pathValidation.valid || !pathValidation.resolvedPath) {
      return this.error(
        "SECURITY_VIOLATION",
        pathValidation.error || "Invalid path",
        {
          parameter: "path",
          recoveryHint:
            "Provide a valid relative path within the project directory.",
        },
      );
    }

    const resolvedPath = pathValidation.resolvedPath;

    try {
      const stat = await fs.stat(resolvedPath);

      if (!stat.isFile()) {
        return this.error("INVALID_PARAMETER", `"${filePath}" is not a file.`, {
          parameter: "path",
          recoveryHint: "Provide a path to a regular file, not a directory.",
        });
      }

      if (stat.size > MAX_FILE_SIZE) {
        return this.error(
          "LIMIT_EXCEEDED",
          `File is too large to read (${(stat.size / 1024).toFixed(1)}KB). Maximum file size is ${(MAX_FILE_SIZE / 1024).toFixed(0)}KB.`,
          {
            parameter: "path",
            recoveryHint: `Use startLine and endLine to read a smaller portion of the file, or use shell_exec with head/tail commands.`,
          },
        );
      }

      const raw = await fs.readFile(resolvedPath);

      if (this.isBinary(raw)) {
        const output = `Binary file, cannot display: ${filePath}`;
        const byteCount = Buffer.byteLength(output, "utf-8");
        HostMonitorService.fileOperation(byteCount);
        HostMonitorService.toolCall(byteCount);

        // Return plain text response for binary files
        const plainTextResponse = `PATH: ${filePath}
TOTAL LINES: 0
LINES SHOWN: 0
FILE OPERATION: read
---
${output}`;

        return plainTextResponse;
      }

      const content = raw.toString("utf-8");
      const lines = content.split("\n");

      const startIdx = Math.max(0, startLine - 1);
      const endIdx =
        endLine !== undefined ? Math.min(endLine, lines.length) : lines.length;

      const selectedLines = lines.slice(startIdx, endIdx);
      const numberedLines = selectedLines
        .map((line, i) => `${startIdx + i + 1}: ${line}`)
        .join("\n");

      const totalLines = lines.length;
      const output =
        endLine !== undefined || startLine > 1
          ? `File: ${filePath} (lines ${startIdx + 1}-${endIdx} of ${totalLines})\n\n${numberedLines}`
          : `File: ${filePath} (${totalLines} lines)\n\n${numberedLines}`;

      const fileOperation = {
        type: "read" as const,
        path: filePath,
        isBinary: false,
        linesAdded: 0,
        linesRemoved: 0,
      };

      const byteCount = Buffer.byteLength(output, "utf-8");
      HostMonitorService.fileOperation(byteCount);
      HostMonitorService.toolCall(byteCount);

      // Return plain text response instead of JSON
      const plainTextResponse = `PATH: ${filePath}
TOTAL LINES: ${totalLines}
LINES SHOWN: ${selectedLines.length}
FILE OPERATION: ${fileOperation.type}
---
${output}`;

      return plainTextResponse;
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);

      if (errorMessage.includes("ENOENT")) {
        return this.error("NOT_FOUND", `File not found: ${filePath}`, {
          parameter: "path",
          recoveryHint: "Check the file path and ensure the file exists.",
        });
      }

      this.log.error("Failed to read file", {
        path: filePath,
        error: errorMessage,
      });

      return this.error(
        "OPERATION_FAILED",
        `Failed to read file: ${errorMessage}`,
      );
    }
  }

  private isBinary(buffer: Buffer): boolean {
    const sampleSize = Math.min(buffer.length, 8192);
    for (let i = 0; i < sampleSize; i++) {
      const byte = buffer[i];
      if (byte === undefined) continue;
      if (byte === 0) return true;
      if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) return true;
    }
    return false;
  }
}

export default new FileReadTool();
