import {
  BestAgentError,
  isRecord,
  normalizeWhitespace,
  stableJsonStringify,
  truncateText,
} from "@best-agent/foundation";
import type { ToolInputValue } from "@best-agent/protocol";

// ── Types ───────────────────────────────────────────────────────────────

export type NamedContentInput = { content: string; path: string };
export type PathInput = { path: string };
export type WorkspaceDirectoryInput = { path: string };
export type WorkspaceSearchInput = { limit: number; query: string };
export type WorkspaceFilesInput = { limit: number; offset: number };
export type ContentSearchInput = {
  case_sensitive: boolean;
  is_regex: boolean;
  limit: number;
  path: string;
  pattern: string;
};

// ── Constants ───────────────────────────────────────────────────────────

export const READ_FILE_INLINE_MAX_BYTES = 16 * 1024;
export const READ_FILE_INLINE_MAX_LINES = 400;
export const READ_FILE_PREVIEW_MAX_BYTES = 128 * 1024;
export const READ_FILE_PREVIEW_MAX_LINES = 3_000;
export const READ_FILE_PREVIEW_HEAD_LINES = 16;
export const READ_FILE_PREVIEW_TAIL_LINES = 16;
export const READ_FILE_RANGE_MAX_LINES = 200;
export const READ_FILE_LINE_PREVIEW_CHARS = 120;
export const CONTENT_SEARCH_LINE_PREVIEW_CHARS = 200;
export const CONTENT_SEARCH_MAX_FILE_BYTES = 1024 * 1024;
export const WORKSPACE_FILES_DEFAULT_LIMIT = 25;
export const WORKSPACE_FILES_MAX_LIMIT = 100;
export const WORKSPACE_FILES_IGNORED_DIRECTORIES = new Set([
  ".build",
  ".best-agent",
  ".git",
  ".pnpm-store",
  "build",
  "coverage",
  "dist",
  "node_modules",
]);

// ── Shared helpers ──────────────────────────────────────────────────────

export function isPlainObject(
  value: ToolInputValue | unknown,
): value is Record<string, ToolInputValue> {
  return isRecord(value);
}

export function readStringField(
  source: Record<string, ToolInputValue>,
  fields: string[],
): string | undefined {
  for (const field of fields) {
    const value = source[field];
    if (typeof value === "string" && value.trim().length > 0) {
      return value;
    }
  }
  return undefined;
}

export function readIntegerField(
  source: Record<string, ToolInputValue>,
  fields: string[],
): number | undefined {
  for (const field of fields) {
    const value = source[field];
    if (typeof value === "number" && Number.isInteger(value)) {
      return value;
    }
    if (typeof value === "string" && /^\d+$/.test(value.trim())) {
      return Number(value.trim());
    }
  }
  return undefined;
}

export function readPresentStringField(
  source: Record<string, ToolInputValue>,
  fields: string[],
): string | undefined {
  for (const field of fields) {
    if (!(field in source)) {
      continue;
    }
    const value = source[field];
    if (typeof value === "string") {
      return value;
    }
  }
  return undefined;
}

export function truncate(value: string): string {
  return truncateText(normalizeWhitespace(value), 120);
}

export function splitTextFileLines(content: string): string[] {
  if (content.length === 0) {
    return [];
  }

  const lines = content.split("\n");
  if (lines.at(-1) === "") {
    lines.pop();
  }
  return lines.map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line));
}

export function stripProviderToolNoise(value: string): string {
  return value.trim();
}

export function normalizePathInput(
  toolName: string,
  input: ToolInputValue,
  aliases: string[],
): PathInput {
  if (typeof input === "string") {
    return { path: input };
  }
  if (!isPlainObject(input)) {
    throw new BestAgentError(
      "TOOL_INPUT_INVALID",
      `${toolName} requires an object input with a path field.`,
    );
  }

  const path = readStringField(input, ["path", ...aliases]);
  if (!path) {
    throw new BestAgentError("TOOL_INPUT_INVALID", `${toolName} requires a path field.`);
  }
  return { path };
}

export function normalizeNamedContentInput(
  toolName: string,
  input: ToolInputValue,
): NamedContentInput {
  if (typeof input === "string") {
    return parseNamedContentString(toolName, input);
  }
  if (!isPlainObject(input)) {
    throw new BestAgentError(
      "TOOL_INPUT_INVALID",
      `${toolName} requires an object input like {"path":"relative/path","content":"full file content"}.`,
    );
  }

  const path = readStringField(input, ["path", "file", "name", "filename", "pathname", "target"]);
  const content = readNamedContentValue(input, [
    "content",
    "text",
    "body",
    "value",
    "data",
    "json",
  ]);
  if (!path || content === undefined) {
    throw new BestAgentError(
      "TOOL_INPUT_INVALID",
      `${toolName} requires path and content fields, for example {"path":"relative/path","content":"full file content"}.`,
    );
  }
  return { path, content };
}

export function formatNamedContentInput(input: NamedContentInput): string {
  return `${input.path}|${input.content}`;
}

export function normalizeOptionalPathInput(
  toolName: string,
  input: ToolInputValue,
): WorkspaceDirectoryInput {
  if (input === undefined || input === null) {
    return { path: "." };
  }
  if (typeof input === "string") {
    const path = input.trim();
    return { path: path.length > 0 ? path : "." };
  }
  if (!isPlainObject(input)) {
    throw new BestAgentError(
      "TOOL_INPUT_INVALID",
      `${toolName} requires an object input with an optional path field.`,
    );
  }

  const path = readStringField(input, ["path", "cwd", "directory"]);
  return { path: path?.trim().length ? path.trim() : "." };
}

export function isDeepEqual(left: unknown, right: unknown): boolean {
  return stableToolInputString(left) === stableToolInputString(right);
}

export function parseToolInputJsonValue(value: string): ToolInputValue | undefined {
  try {
    return JSON.parse(value) as ToolInputValue;
  } catch {
    return undefined;
  }
}

function stableToolInputString(value: unknown): string {
  return stableJsonStringify(value);
}

function parseNamedContentString(toolName: string, input: string): NamedContentInput {
  const separatorIndex = input.indexOf("|");
  if (separatorIndex <= 0) {
    throw new BestAgentError(
      "SCRATCH_INVALID",
      `${toolName} input must use the format <relative-path>|<content>.`,
    );
  }

  const path = input.slice(0, separatorIndex).trim();
  const content = input.slice(separatorIndex + 1);
  if (!path) {
    throw new BestAgentError(
      "SCRATCH_INVALID",
      `${toolName} input must include a relative output path before the | separator.`,
    );
  }

  return { path, content };
}

function readNamedContentValue(
  input: Record<string, ToolInputValue>,
  fields: string[],
): string | undefined {
  for (const field of fields) {
    if (!(field in input)) {
      continue;
    }
    const value = input[field];
    if (typeof value === "string") {
      return value;
    }
    if (
      typeof value === "number" ||
      typeof value === "boolean" ||
      value === null ||
      Array.isArray(value) ||
      isPlainObject(value)
    ) {
      return JSON.stringify(value);
    }
    return undefined;
  }
  return undefined;
}
