import fs from "node:fs";
import path from "node:path";
import { matchesAnyGlob, toPosixPath } from "./glob.js";
import { authorizePath, evaluateTraversalTarget, isHiddenPath, linkPolicyStatus, type AuthorizedPath } from "./path-policy.js";
import type { ProjectFsConfig } from "./config.js";

export interface ReadFileArgs {
  path: string;
  startLine?: number;
  endLine?: number;
  maxBytes?: number;
}

export interface ListDirArgs {
  path: string;
  depth?: number;
  include?: string[];
  exclude?: string[];
  includeHidden?: boolean;
}

export interface GrepFilesArgs {
  root: string;
  pattern: string;
  include?: string[];
  exclude?: string[];
  caseSensitive?: boolean;
  maxResults?: number;
}

export interface StatArgs {
  path: string;
}

export interface ReadManyArgs {
  paths: string[];
  maxBytesPerFile?: number;
}

function normalizeInteger(value: unknown, fieldName: string, defaultValue?: number): number {
  if (value === undefined) {
    if (defaultValue === undefined) {
      throw new Error(`${fieldName} mancante.`);
    }
    return defaultValue;
  }

  const parsed = Number(value);
  if (!Number.isInteger(parsed) || parsed <= 0) {
    throw new Error(`${fieldName} deve essere un intero positivo.`);
  }
  return parsed;
}

function clamp(value: number, maxValue: number): number {
  return Math.min(value, maxValue);
}

function listFromUnknown(value: unknown, fieldName: string): string[] {
  if (value === undefined) return [];
  if (!Array.isArray(value)) {
    throw new Error(`${fieldName} deve essere un array di stringhe.`);
  }
  return value.map((entry) => String(entry).trim()).filter(Boolean);
}

function textBlock(text: string) {
  return [{ type: "text", text }];
}

function normalizeContentText(value: unknown): string {
  return typeof value === "string" ? value : JSON.stringify(value, null, 2);
}

export function makeToolSuccess(text: unknown, structuredContent: Record<string, unknown>) {
  return {
    content: textBlock(normalizeContentText(text)),
    structuredContent
  };
}

export function makeToolError(error: unknown, context: Record<string, unknown> = {}) {
  const message = error instanceof Error ? error.message : String(error);
  return {
    content: textBlock(`Errore: ${message}`),
    structuredContent: {
      ok: false,
      error: message,
      ...context
    },
    isError: true
  };
}

function readLimitedBuffer(filePath: string, maxBytes: number): { buffer: Buffer; fileSize: number; truncated: boolean } {
  const stats = fs.statSync(filePath);
  const fileSize = stats.size;
  const bytesToRead = Math.min(fileSize, maxBytes);
  const buffer = Buffer.alloc(bytesToRead);

  const handle = fs.openSync(filePath, "r");
  try {
    fs.readSync(handle, buffer, 0, bytesToRead, 0);
  } finally {
    fs.closeSync(handle);
  }

  return {
    buffer,
    fileSize,
    truncated: fileSize > maxBytes
  };
}

function isBinaryBuffer(buffer: Buffer): boolean {
  const sampleSize = Math.min(buffer.length, 4096);
  let zeroBytes = 0;

  for (let index = 0; index < sampleSize; index += 1) {
    if (buffer[index] === 0) {
      zeroBytes += 1;
    }
  }

  return zeroBytes > 0;
}

function summarizeAuthorizedPath(config: ProjectFsConfig, authorized: AuthorizedPath) {
  return {
    requested_path: authorized.requestedPath,
    absolute_path: authorized.absolutePath,
    real_path: authorized.realPath,
    relative_path: authorized.relativePath,
    root: authorized.root.realPath,
    hidden: authorized.hidden,
    had_links: authorized.hadLinks,
    link_policy: linkPolicyStatus(config.followLinks, authorized.hadLinks),
    size: authorized.stats.size,
    type: authorized.stats.isDirectory() ? "directory" : authorized.stats.isFile() ? "file" : "other",
    mtime: authorized.stats.mtime.toISOString(),
    ctime: authorized.stats.ctime.toISOString()
  };
}

function parseTextLines(content: string): string[] {
  return content.split(/\r?\n/);
}

function filterByPatterns(relativePath: string, include: string[], exclude: string[]): boolean {
  const normalized = toPosixPath(relativePath);
  if (exclude.length > 0 && matchesAnyGlob(normalized, exclude)) {
    return false;
  }
  if (include.length === 0) {
    return true;
  }
  return matchesAnyGlob(normalized, include);
}

function buildPreviewLine(lineNumber: number, lineText: string) {
  return `${lineNumber}: ${lineText}`;
}

function makeRealPathKey(realPath: string): string {
  return process.platform === "win32" ? realPath.toLowerCase() : realPath;
}

export function readFileTool(config: ProjectFsConfig, args: ReadFileArgs) {
  const authorized = authorizePath(config, args.path, "file");
  const maxBytes = clamp(normalizeInteger(args.maxBytes, "maxBytes", config.maxFileBytes), config.maxFileBytes);
  const startLine = normalizeInteger(args.startLine, "startLine", 1);
  const readResult = readLimitedBuffer(authorized.realPath, maxBytes);

  if (isBinaryBuffer(readResult.buffer)) {
    throw new Error("Il file richiesto sembra binario e non puo' essere restituito come testo.");
  }

  const text = readResult.buffer.toString("utf8");
  const lines = parseTextLines(text);
  const requestedEndLine = args.endLine === undefined ? lines.length : normalizeInteger(args.endLine, "endLine");
  if (requestedEndLine < startLine) {
    throw new Error("endLine deve essere maggiore o uguale a startLine.");
  }

  const slice = lines.slice(startLine - 1, requestedEndLine);
  const effectiveEndLine = slice.length === 0 ? startLine - 1 : startLine + slice.length - 1;
  const selectedText = slice.join("\n");

  return makeToolSuccess(selectedText, {
    ok: true,
    tool: "read_file",
    ...summarizeAuthorizedPath(config, authorized),
    start_line: startLine,
    end_line: effectiveEndLine,
    total_lines: lines.length,
    max_bytes: maxBytes,
    truncated: readResult.truncated,
    text: selectedText
  });
}

export function statTool(config: ProjectFsConfig, args: StatArgs) {
  const authorized = authorizePath(config, args.path, "any");
  return makeToolSuccess(summarizeAuthorizedPath(config, authorized), {
    ok: true,
    tool: "stat",
    ...summarizeAuthorizedPath(config, authorized)
  });
}

export function listDirTool(config: ProjectFsConfig, args: ListDirArgs) {
  const authorized = authorizePath(config, args.path, "dir");
  const depth = clamp(normalizeInteger(args.depth, "depth", 1), config.maxDepth);
  const include = listFromUnknown(args.include, "include");
  const exclude = listFromUnknown(args.exclude, "exclude");
  const includeHidden = args.includeHidden === true;

  const entries: Record<string, unknown>[] = [];
  const skipped: Record<string, unknown>[] = [];
  const seenEntries = new Set<string>();
  const visitedDirectories = new Set<string>([makeRealPathKey(authorized.realPath)]);
  let truncated = false;

  function walk(currentPath: string, currentDepth: number): void {
    if (currentDepth > depth || truncated) {
      return;
    }

    const dirEntries = fs.readdirSync(currentPath, { withFileTypes: true });
    for (const entry of dirEntries) {
      if (truncated) break;

      const entryPath = path.join(currentPath, entry.name);
      const decision = evaluateTraversalTarget(config, entryPath);
      if (!decision.allowed) {
        skipped.push({
          path: toPosixPath(path.relative(authorized.realPath, entryPath)),
          reason: decision.reason
        });
        continue;
      }

      const entryAuthorized = authorizePath(config, entryPath, "any");
      const relativePath = toPosixPath(path.relative(authorized.realPath, entryAuthorized.realPath));
      if (!includeHidden && isHiddenPath(entryAuthorized.realPath)) {
        continue;
      }

      const matchesFilters = filterByPatterns(relativePath, include, exclude);
      const entryKey = makeRealPathKey(entryAuthorized.realPath);

      if (matchesFilters && !seenEntries.has(entryKey)) {
        seenEntries.add(entryKey);
        entries.push({
          name: path.basename(entryAuthorized.realPath),
          path: relativePath,
          type: entryAuthorized.stats.isDirectory() ? "directory" : entryAuthorized.stats.isFile() ? "file" : "other",
          depth: currentDepth,
          size: entryAuthorized.stats.size,
          hidden: entryAuthorized.hidden,
          had_links: entryAuthorized.hadLinks
        });

        if (entries.length >= config.maxSearchResults) {
          truncated = true;
          break;
        }
      }

      if (entryAuthorized.stats.isDirectory() && currentDepth < depth && !visitedDirectories.has(entryKey)) {
        visitedDirectories.add(entryKey);
        walk(entryAuthorized.realPath, currentDepth + 1);
      }
    }
  }

  walk(authorized.realPath, 1);

  return makeToolSuccess(entries, {
    ok: true,
    tool: "list_dir",
    ...summarizeAuthorizedPath(config, authorized),
    depth,
    include,
    exclude,
    include_hidden: includeHidden,
    truncated,
    entries,
    skipped
  });
}

export function grepFilesTool(config: ProjectFsConfig, args: GrepFilesArgs) {
  const authorized = authorizePath(config, args.root, "dir");
  const include = listFromUnknown(args.include, "include");
  const exclude = listFromUnknown(args.exclude, "exclude");
  const pattern = String(args.pattern || "");
  if (!pattern) {
    throw new Error("pattern obbligatorio.");
  }

  const caseSensitive = args.caseSensitive === true;
  const normalizedNeedle = caseSensitive ? pattern : pattern.toLowerCase();
  const maxResults = clamp(normalizeInteger(args.maxResults, "maxResults", config.maxSearchResults), config.maxSearchResults);
  const deadline = Date.now() + config.searchTimeoutMs;

  const matches: Record<string, unknown>[] = [];
  const skipped: Record<string, unknown>[] = [];
  const visitedDirectories = new Set<string>([makeRealPathKey(authorized.realPath)]);
  const visitedFiles = new Set<string>();
  let truncated = false;
  let timedOut = false;

  function walk(currentPath: string, currentDepth: number): void {
    if (currentDepth > config.maxDepth || truncated || timedOut) {
      return;
    }

    if (Date.now() > deadline) {
      timedOut = true;
      truncated = true;
      return;
    }

    const dirEntries = fs.readdirSync(currentPath, { withFileTypes: true });
    for (const entry of dirEntries) {
      if (truncated || timedOut) break;

      const entryPath = path.join(currentPath, entry.name);
      const decision = evaluateTraversalTarget(config, entryPath);
      if (!decision.allowed) {
        skipped.push({
          path: toPosixPath(path.relative(authorized.realPath, entryPath)),
          reason: decision.reason
        });
        continue;
      }

      const entryAuthorized = authorizePath(config, entryPath, "any");
      const relativePath = toPosixPath(path.relative(authorized.realPath, entryAuthorized.realPath));
      const entryKey = makeRealPathKey(entryAuthorized.realPath);

      if (entryAuthorized.stats.isDirectory()) {
        if (!visitedDirectories.has(entryKey)) {
          visitedDirectories.add(entryKey);
          walk(entryAuthorized.realPath, currentDepth + 1);
        }
        continue;
      }

      if (!entryAuthorized.stats.isFile()) {
        continue;
      }

      if (!filterByPatterns(relativePath, include, exclude)) {
        continue;
      }

      if (visitedFiles.has(entryKey)) {
        continue;
      }
      visitedFiles.add(entryKey);

      const readResult = readLimitedBuffer(entryAuthorized.realPath, config.maxFileBytes);
      if (isBinaryBuffer(readResult.buffer)) {
        skipped.push({ path: relativePath, reason: "binary-file" });
        continue;
      }

      const fileText = readResult.buffer.toString("utf8");
      const lines = parseTextLines(fileText);
      for (let index = 0; index < lines.length; index += 1) {
        const lineText = lines[index];
        const haystack = caseSensitive ? lineText : lineText.toLowerCase();
        if (!haystack.includes(normalizedNeedle)) {
          continue;
        }

        matches.push({
          path: relativePath,
          line: index + 1,
          preview: buildPreviewLine(index + 1, lineText),
          truncated_file: readResult.truncated
        });

        if (matches.length >= maxResults) {
          truncated = true;
          return;
        }
      }
    }
  }

  walk(authorized.realPath, 1);

  return makeToolSuccess(matches, {
    ok: true,
    tool: "grep_files",
    ...summarizeAuthorizedPath(config, authorized),
    pattern,
    case_sensitive: caseSensitive,
    include,
    exclude,
    max_results: maxResults,
    truncated,
    timed_out: timedOut,
    matches,
    skipped
  });
}

export function readManyTool(config: ProjectFsConfig, args: ReadManyArgs) {
  const paths = listFromUnknown(args.paths, "paths");
  if (paths.length === 0) {
    throw new Error("paths deve contenere almeno un path.");
  }

  const maxBytesPerFile = clamp(normalizeInteger(args.maxBytesPerFile, "maxBytesPerFile", config.maxFileBytes), config.maxFileBytes);
  const results = paths.map((entryPath) => {
    try {
      const result = readFileTool(config, { path: entryPath, maxBytes: maxBytesPerFile });
      return {
        path: entryPath,
        ok: true,
        structured: result.structuredContent
      };
    } catch (error) {
      return {
        path: entryPath,
        ok: false,
        error: error instanceof Error ? error.message : String(error)
      };
    }
  });

  return makeToolSuccess(results, {
    ok: true,
    tool: "read_many",
    max_bytes_per_file: maxBytesPerFile,
    results
  });
}
