import { isRecord } from "@best-agent/foundation";
import type { ToolInputValue, ToolSummary } from "@best-agent/protocol";
import {
  looksSecretLikeTextForHistory,
  redactSecretLikeTextForHistory,
} from "../policy/capability-history.js";

export type ToolHistoryInputDefinition = Readonly<{
  name: string;
  source: ToolSummary["source"];
  providerHistoryInput?: (input: ToolInputValue) => ToolInputValue;
  historyInput?: (input: ToolInputValue) => ToolInputValue | undefined;
  isContentBearingInput?: (input: ToolInputValue) => boolean;
}>;

export const CONTENT_TOOL_FOLLOW_UP_INPUT_TOOL_NAMES = new Set([
  "append-scratch",
  "attach-text",
  "apply_patch",
  "edit",
  "write-artifact",
  "write-scratch",
  "write-workspace",
]);

export function renderToolInputForProviderHistory(
  toolName: string,
  input: ToolInputValue,
): ToolInputValue {
  const markerContext = createContentOmittedMarkerContext();
  if (toolName === "process-session-write") {
    return renderProcessSessionWriteFollowUpInput(input, markerContext);
  }
  if (toolName === "shell") {
    return renderShellCommandForProviderHistory(input, markerContext);
  }
  if (!CONTENT_TOOL_FOLLOW_UP_INPUT_TOOL_NAMES.has(toolName)) {
    return input;
  }
  return renderNamedContentToolFollowUpInput(input, markerContext);
}

export function isContentBearingToolInputForProviderHistory(toolName: string): boolean {
  return (
    toolName === "process-session-write" ||
    toolName === "shell" ||
    CONTENT_TOOL_FOLLOW_UP_INPUT_TOOL_NAMES.has(toolName)
  );
}

export function renderToolInputForProviderHistoryFromDefinition(
  tool: ToolHistoryInputDefinition,
  input: ToolInputValue,
): ToolInputValue {
  if (tool.providerHistoryInput) {
    const rendered = tool.providerHistoryInput(input);
    return isExternalToolSource(tool.source)
      ? sanitizeExternalToolInputForHistory(rendered)
      : rendered;
  }
  if (isExternalToolSource(tool.source)) {
    return sanitizeExternalToolInputForHistory(input);
  }
  return renderToolInputForProviderHistory(tool.name, input);
}

export function toHistorySafeToolInputFromDefinition(
  tool: ToolHistoryInputDefinition,
  input: ToolInputValue,
): ToolInputValue | undefined {
  if (tool.historyInput) {
    const rendered = tool.historyInput(input);
    if (rendered === undefined) {
      return undefined;
    }
    return isExternalToolSource(tool.source)
      ? sanitizeExternalToolInputForToolHistory(rendered)
      : rendered;
  }
  if (isExternalToolSource(tool.source)) {
    return sanitizeExternalToolInputForToolHistory(input);
  }
  return toHistorySafeToolInput(tool.name, input);
}

export function isContentBearingToolInputForCheckpoint(
  tool: ToolHistoryInputDefinition,
  input: ToolInputValue,
): boolean {
  const externalWouldSanitize =
    isExternalToolSource(tool.source) && wouldSanitizeExternalToolInput(input);

  if (tool.isContentBearingInput) {
    return tool.isContentBearingInput(input) || externalWouldSanitize;
  }
  if (isExternalToolSource(tool.source)) {
    return externalWouldSanitize;
  }
  return isContentBearingToolInputForProviderHistory(tool.name);
}

function isExternalToolSource(source: ToolHistoryInputDefinition["source"]): boolean {
  return source === "plugin" || source === "mcp" || source === "extension";
}

export function toHistorySafeDisplayInput(
  toolName: string,
  displayInput: string,
  normalizedInput?: ToolInputValue,
): string {
  if (normalizedInput === undefined) {
    return displayInput;
  }
  if (CONTENT_TOOL_FOLLOW_UP_INPUT_TOOL_NAMES.has(toolName)) {
    const safe = toHistorySafeContentToolInput(toolName, normalizedInput);
    if (safe === undefined || !isToolInputRecord(safe)) {
      return displayInput;
    }
    if (toolName === "apply_patch") {
      const patch_bytes = safe.patch_bytes;
      const byteSummary =
        typeof patch_bytes === "number" || typeof patch_bytes === "string" ? patch_bytes : "?";
      return `patch [content omitted: ${byteSummary} bytes]`;
    }
    const file_path =
      typeof safe.file_path === "string"
        ? safe.file_path
        : typeof safe.path === "string"
          ? safe.path
          : undefined;
    if (!file_path) {
      return displayInput;
    }
    if (toolName === "edit") {
      return `${file_path}|edit`;
    }
    const content_bytes = safe.content_bytes;
    const byteSummary =
      typeof content_bytes === "number" || typeof content_bytes === "string" ? content_bytes : "?";
    return `${file_path}|[content omitted: ${byteSummary} bytes]`;
  }
  if (toolName === "shell") {
    const safe = toHistorySafeShellInput(normalizedInput);
    if (safe === undefined || !isToolInputRecord(safe) || typeof safe.command !== "string") {
      return displayInput;
    }
    if (
      isToolInputRecord(normalizedInput) &&
      typeof normalizedInput.command === "string" &&
      safe.command === normalizedInput.command &&
      safe.cwd === normalizedInput.cwd &&
      safe.timeout_ms === normalizedInput.timeout_ms &&
      safe.command_bytes === undefined
    ) {
      return displayInput;
    }
    const cwd = typeof safe.cwd === "string" ? ` cwd=${safe.cwd}` : "";
    return `shell command: ${safe.command}${cwd}`;
  }
  return displayInput;
}

export function toHistorySafeToolInput(
  toolName: string,
  input: ToolInputValue,
): ToolInputValue | undefined {
  if (CONTENT_TOOL_FOLLOW_UP_INPUT_TOOL_NAMES.has(toolName)) {
    return toHistorySafeContentToolInput(toolName, input);
  }
  if (toolName === "process-session-write") {
    return toHistorySafeProcessSessionWriteInput(input);
  }
  if (toolName === "shell") {
    return toHistorySafeShellInput(input);
  }
  return input;
}

export function sanitizeStoredToolHistoryInput(
  toolName: string,
  input: ToolInputValue | undefined,
): ToolInputValue | undefined {
  if (input === undefined) {
    return undefined;
  }
  const safe = toHistorySafeToolInput(toolName, input);
  if (safe !== input || !isContentBearingToolInputForProviderHistory(toolName)) {
    return safe;
  }
  return renderToolInputForProviderHistory(toolName, input);
}

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

export function isToolInputValue(value: unknown): value is ToolInputValue {
  if (
    value === null ||
    typeof value === "string" ||
    typeof value === "number" ||
    typeof value === "boolean"
  ) {
    return true;
  }
  if (Array.isArray(value)) {
    return value.every(isToolInputValue);
  }
  if (typeof value === "object") {
    return Object.values(value).every(isToolInputValue);
  }
  return false;
}

type ContentOmittedMarkerContext = {
  nextRef: number;
};

type ContentOmittedMarkerFactory = (
  content: string,
  markerContext: ContentOmittedMarkerContext,
) => string;

function createContentOmittedMarkerContext(): ContentOmittedMarkerContext {
  return { nextRef: 1 };
}

function renderNamedContentToolFollowUpInput(
  input: ToolInputValue,
  markerContext: ContentOmittedMarkerContext,
): ToolInputValue {
  if (typeof input === "string") {
    const separator = input.indexOf("|");
    if (separator < 0) {
      return createToolInputContentOmittedMarker(input, markerContext);
    }
    const path = input.slice(0, separator);
    const content = input.slice(separator + 1);
    return `${path}|${createToolInputContentOmittedMarker(content, markerContext)}`;
  }
  return sanitizeContentBearingInputForHistory(input, markerContext);
}

function renderProcessSessionWriteFollowUpInput(
  input: ToolInputValue,
  markerContext: ContentOmittedMarkerContext,
): ToolInputValue {
  if (!isToolInputRecord(input)) {
    return createToolInputContentOmittedMarker(String(input), markerContext);
  }

  const sanitized = sanitizeContentBearingInputForHistory(input, markerContext);
  if (!isToolInputRecord(sanitized)) {
    return sanitized;
  }

  return {
    ...sanitized,
    ...(typeof input.session_id === "string" ? { session_id: input.session_id } : {}),
  };
}

export const SHELL_COMMAND_HISTORY_SAFE_MAX_CHARS = 2048;

function renderShellCommandForProviderHistory(
  input: ToolInputValue,
  markerContext: ContentOmittedMarkerContext,
): ToolInputValue {
  if (!isToolInputRecord(input)) {
    if (
      typeof input === "string" &&
      (input.length > SHELL_COMMAND_HISTORY_SAFE_MAX_CHARS || input.includes("\n"))
    ) {
      return createToolInputContentOmittedMarker(input, markerContext);
    }
    if (typeof input === "string") {
      return redactSecretLikeTextForHistory(input);
    }
    return input;
  }

  const sanitized = sanitizeShellExtraInputFields(input, markerContext);

  if (typeof input.cwd === "string") {
    sanitized.cwd = isShellCwdHistoryValue(input.cwd)
      ? input.cwd
      : createToolInputContentOmittedMarker(input.cwd, markerContext);
  }
  if (typeof input.timeout_ms === "number") {
    sanitized.timeout_ms = input.timeout_ms;
  }
  if (typeof input.command !== "string") {
    return sanitized;
  }
  if (
    input.command.length <= SHELL_COMMAND_HISTORY_SAFE_MAX_CHARS &&
    !input.command.includes("\n")
  ) {
    sanitized.command = redactSecretLikeTextForHistory(input.command);
    return sanitized;
  }

  sanitized.command = createToolInputContentOmittedMarker(input.command, markerContext);
  sanitized.command_bytes = Buffer.byteLength(input.command, "utf8");
  return sanitized;
}

function toHistorySafeShellInput(input: ToolInputValue): ToolInputValue | undefined {
  if (!isToolInputRecord(input)) {
    if (
      typeof input === "string" &&
      (input.length > SHELL_COMMAND_HISTORY_SAFE_MAX_CHARS || input.includes("\n"))
    ) {
      return createToolHistoryContentOmittedMarker(input, createContentOmittedMarkerContext());
    }
    if (typeof input === "string") {
      return redactSecretLikeTextForHistory(input);
    }
    return input;
  }

  const markerContext = createContentOmittedMarkerContext();
  const sanitized = sanitizeShellExtraInputFields(
    input,
    markerContext,
    createToolHistoryContentOmittedMarker,
  );

  if (typeof input.cwd === "string") {
    sanitized.cwd = isShellCwdHistoryValue(input.cwd)
      ? input.cwd
      : createToolHistoryContentOmittedMarker(input.cwd, markerContext);
  }
  if (typeof input.timeout_ms === "number") {
    sanitized.timeout_ms = input.timeout_ms;
  }
  if (typeof input.command !== "string") {
    return sanitized;
  }
  if (
    input.command.length <= SHELL_COMMAND_HISTORY_SAFE_MAX_CHARS &&
    !input.command.includes("\n")
  ) {
    sanitized.command = redactSecretLikeTextForHistory(input.command);
    return sanitized;
  }

  sanitized.command = createToolHistoryContentOmittedMarker(input.command, markerContext);
  sanitized.command_bytes = Buffer.byteLength(input.command, "utf8");
  return sanitized;
}

function sanitizeContentBearingInputForHistory(
  value: ToolInputValue,
  markerContext: ContentOmittedMarkerContext,
  createMarker: ContentOmittedMarkerFactory = createToolInputContentOmittedMarker,
): ToolInputValue {
  if (typeof value === "string") {
    return createMarker(value, markerContext);
  }
  if (Array.isArray(value)) {
    return value.map((entry) =>
      sanitizeContentBearingInputForHistory(entry, markerContext, createMarker),
    );
  }
  if (!isToolInputRecord(value)) {
    return value;
  }
  const output: Record<string, ToolInputValue> = {};
  for (const [key, child] of Object.entries(value)) {
    if (typeof child === "string" && PATH_LIKE_HISTORY_KEYS.has(key)) {
      output[key] = isPathLikeHistoryValue(child) ? child : createMarker(child, markerContext);
      continue;
    }
    if (key === "name" && typeof child === "string") {
      output[key] = isPathLikeHistoryValue(child) ? child : createMarker(child, markerContext);
      continue;
    }
    if (typeof child === "string" && CONTENT_LIKE_HISTORY_KEYS.has(key)) {
      output[key] = createMarker(child, markerContext);
      continue;
    }
    output[key] = sanitizeContentBearingInputForHistory(child, markerContext, createMarker);
  }
  return output;
}

const EXTERNAL_TOOL_HISTORY_SAFE_MAX_INLINE_STRING_CHARS = 2048;

const NORMALIZED_EXTERNAL_CONTENT_KEYS = new Set([
  "body",
  "content",
  "data",
  "json",
  "payload",
  "script",
  "stdin",
]);

const NORMALIZED_EXTERNAL_SECRET_KEYS = new Set([
  "accesstoken",
  "apikey",
  "auth",
  "authorization",
  "bearer",
  "bearertoken",
  "clientsecret",
  "credential",
  "credentials",
  "password",
  "privatekey",
  "secret",
  "token",
]);

const NORMALIZED_EXTERNAL_TEXT_KEYS = new Set(["input", "query", "text", "value"]);

const NORMALIZED_EXTERNAL_PATH_LIKE_KEYS = new Set([
  "file",
  "filepath",
  "filename",
  "path",
  "pathname",
  "target",
  "targetpath",
  "workspacepath",
]);

function sanitizeExternalToolInputForHistory(input: ToolInputValue): ToolInputValue {
  return sanitizeExternalToolInputValue(
    input,
    createContentOmittedMarkerContext(),
    createToolInputContentOmittedMarker,
  );
}

function sanitizeExternalToolInputForToolHistory(input: ToolInputValue): ToolInputValue {
  return sanitizeExternalToolInputValue(
    input,
    createContentOmittedMarkerContext(),
    createToolHistoryContentOmittedMarker,
  );
}

function sanitizeExternalToolInputValue(
  value: ToolInputValue,
  markerContext: ContentOmittedMarkerContext,
  createMarker: ContentOmittedMarkerFactory,
  key?: string,
): ToolInputValue {
  if (typeof value === "string") {
    if (key === undefined) {
      return createMarker(value, markerContext);
    }
    return shouldSanitizeExternalToolString(key, value)
      ? createMarker(value, markerContext)
      : value;
  }
  if (Array.isArray(value)) {
    return value.map((entry) =>
      sanitizeExternalToolInputValue(entry, markerContext, createMarker, key),
    );
  }
  if (!isToolInputRecord(value)) {
    return value;
  }
  const output: Record<string, ToolInputValue> = {};
  for (const [childKey, child] of Object.entries(value)) {
    output[childKey] = sanitizeExternalToolInputValue(child, markerContext, createMarker, childKey);
  }
  return output;
}

function wouldSanitizeExternalToolInput(value: ToolInputValue, key?: string): boolean {
  if (typeof value === "string") {
    if (key === undefined) {
      return true;
    }
    return shouldSanitizeExternalToolString(key, value);
  }
  if (Array.isArray(value)) {
    return value.some((entry) => wouldSanitizeExternalToolInput(entry, key));
  }
  if (!isToolInputRecord(value)) {
    return false;
  }
  return Object.entries(value).some(([childKey, child]) =>
    wouldSanitizeExternalToolInput(child, childKey),
  );
}

function shouldSanitizeExternalToolString(key: string | undefined, value: string): boolean {
  const normalizedKey = key ? normalizeHistoryKey(key) : undefined;
  if (normalizedKey && NORMALIZED_EXTERNAL_PATH_LIKE_KEYS.has(normalizedKey)) {
    return !isPathLikeHistoryValue(value);
  }
  if (normalizedKey && NORMALIZED_EXTERNAL_SECRET_KEYS.has(normalizedKey)) {
    return true;
  }
  if (normalizedKey && NORMALIZED_EXTERNAL_CONTENT_KEYS.has(normalizedKey)) {
    return true;
  }
  if (normalizedKey && NORMALIZED_EXTERNAL_TEXT_KEYS.has(normalizedKey)) {
    return (
      value.length > EXTERNAL_TOOL_HISTORY_SAFE_MAX_INLINE_STRING_CHARS ||
      value.includes("\n") ||
      looksSecretLikeTextForHistory(value)
    );
  }
  return (
    value.length > EXTERNAL_TOOL_HISTORY_SAFE_MAX_INLINE_STRING_CHARS ||
    looksSecretLikeTextForHistory(value)
  );
}

function normalizeHistoryKey(key: string): string {
  return key.replace(/[^A-Za-z0-9]/gu, "").toLowerCase();
}

function sanitizeShellExtraInputFields(
  input: Record<string, ToolInputValue>,
  markerContext: ContentOmittedMarkerContext,
  createMarker: ContentOmittedMarkerFactory = createToolInputContentOmittedMarker,
): Record<string, ToolInputValue> {
  const output: Record<string, ToolInputValue> = {};
  for (const [key, child] of Object.entries(input)) {
    if (key === "command" || key === "cwd" || key === "timeout_ms") {
      continue;
    }
    output[key] = sanitizeContentBearingInputForHistory(child, markerContext, createMarker);
  }
  return output;
}

function isShellCwdHistoryValue(value: string): boolean {
  return value.length <= 512 && !value.includes("\n");
}

function isPathLikeHistoryValue(value: string): boolean {
  return (
    value.length <= 512 &&
    !value.includes("\n") &&
    !looksSecretLikeTextForHistory(value) &&
    (value.includes("/") ||
      value.includes("\\") ||
      /^[\w.-]+\.[A-Za-z0-9]{1,12}$/u.test(value) ||
      /^[\w.@+-]+$/u.test(value))
  );
}

const CONTENT_LIKE_HISTORY_KEYS = new Set([
  "body",
  "content",
  "data",
  "find",
  "json",
  "new",
  "new_string",
  "old",
  "old_string",
  "patch",
  "payload",
  "replace",
  "script",
  "stdin",
  "text",
  "value",
]);

const PATH_LIKE_HISTORY_KEYS = new Set([
  "file",
  "file_path",
  "filename",
  "path",
  "pathname",
  "target",
  "target_path",
  "workspace_path",
]);

const TOOL_INPUT_CONTENT_OMITTED_MARKER_PREFIX = "[content omitted from provider follow-up:";

function createToolInputContentOmittedMarker(
  content: string,
  markerContext: ContentOmittedMarkerContext,
): string {
  if (content.startsWith(TOOL_INPUT_CONTENT_OMITTED_MARKER_PREFIX) && content.endsWith("]")) {
    return content;
  }
  const ref = `content-${markerContext.nextRef}`;
  markerContext.nextRef += 1;
  return `[content omitted from provider follow-up: ${content.length} chars, ref:${ref}]`;
}

function createToolHistoryContentOmittedMarker(
  content: string,
  _markerContext: ContentOmittedMarkerContext,
): string {
  return `[content omitted from tool history: ${content.length} chars]`;
}

function toHistorySafeNamedContentInput(input: ToolInputValue): ToolInputValue | undefined {
  if (!isToolInputRecord(input) || typeof input.path !== "string") {
    return input;
  }
  const safe: Record<string, ToolInputValue> = { path: input.path };
  if (typeof input.content === "string") {
    safe.content_bytes = Buffer.byteLength(input.content, "utf8");
  } else if (typeof input.content_bytes === "number" || typeof input.content_bytes === "string") {
    safe.content_bytes = input.content_bytes;
  }
  return safe;
}

function toHistorySafeContentToolInput(
  toolName: string,
  input: ToolInputValue,
): ToolInputValue | undefined {
  if (toolName === "write-workspace") {
    return toHistorySafeWorkspaceWriteInput(input);
  }
  if (toolName === "edit") {
    return toHistorySafeEditInput(input);
  }
  if (toolName === "apply_patch") {
    return toHistorySafePatchInput(input);
  }
  return toHistorySafeNamedContentInput(input);
}

function toHistorySafeWorkspaceWriteInput(input: ToolInputValue): ToolInputValue | undefined {
  if (!isToolInputRecord(input) || typeof input.file_path !== "string") {
    return input;
  }
  const safe: Record<string, ToolInputValue> = { file_path: input.file_path };
  if (typeof input.content === "string") {
    safe.content_bytes = Buffer.byteLength(input.content, "utf8");
  } else if (typeof input.content_bytes === "number" || typeof input.content_bytes === "string") {
    safe.content_bytes = input.content_bytes;
  }
  return safe;
}

function toHistorySafeEditInput(input: ToolInputValue): ToolInputValue | undefined {
  if (!isToolInputRecord(input) || typeof input.file_path !== "string") {
    return input;
  }
  const safe: Record<string, ToolInputValue> = { file_path: input.file_path };
  if (typeof input.old_string === "string") {
    safe.old_string_bytes = Buffer.byteLength(input.old_string, "utf8");
  } else if (
    typeof input.old_string_bytes === "number" ||
    typeof input.old_string_bytes === "string"
  ) {
    safe.old_string_bytes = input.old_string_bytes;
  }
  if (typeof input.new_string === "string") {
    safe.new_string_bytes = Buffer.byteLength(input.new_string, "utf8");
  } else if (
    typeof input.new_string_bytes === "number" ||
    typeof input.new_string_bytes === "string"
  ) {
    safe.new_string_bytes = input.new_string_bytes;
  }
  if (typeof input.replace_all === "boolean") {
    safe.replace_all = input.replace_all;
  }
  return safe;
}

function toHistorySafePatchInput(input: ToolInputValue): ToolInputValue | undefined {
  if (!isToolInputRecord(input)) {
    return input;
  }
  const safe: Record<string, ToolInputValue> = {};
  if (typeof input.patch === "string") {
    safe.patch_bytes = Buffer.byteLength(input.patch, "utf8");
  } else if (typeof input.patch_bytes === "number" || typeof input.patch_bytes === "string") {
    safe.patch_bytes = input.patch_bytes;
  }
  return safe;
}

function toHistorySafeProcessSessionWriteInput(input: ToolInputValue): ToolInputValue | undefined {
  if (!isToolInputRecord(input) || typeof input.session_id !== "string") {
    return input;
  }
  return {
    session_id: input.session_id,
    ...(typeof input.input === "string"
      ? { input_bytes: Buffer.byteLength(input.input, "utf8") }
      : typeof input.input_bytes === "number" || typeof input.input_bytes === "string"
        ? { input_bytes: input.input_bytes }
        : {}),
  };
}
