import { BestAgentError, normalizeWhitespace, truncateText } from "@best-agent/foundation";
import type {
  CapabilityRequest,
  ToolApprovalClass,
  ToolHeartbeatPolicy,
  ToolInputValue,
  ToolReplayPolicy,
  ToolSummary,
} from "@best-agent/protocol";
import { createBuiltInTools } from "./builtins/index.js";
import type {
  AnyToolDefinition,
  NormalizedAskUserInput,
  RuntimeToolDefinition,
  ToolDefinition,
  ToolExecutionContext,
  ToolExecutionResultMetadata,
  ToolRegistryOptions,
} from "./contracts.js";
import { parseRuntimeToolInput, toRuntimeToolDefinition } from "./definition-adapter.js";
import {
  coerceLooseStringInput,
  formatAskUserAnswers,
  normalizeAskUserInput,
} from "./input-normalization.js";
import { createLspTools } from "./lsp.js";
import { getToolInputJsonSchema } from "./schema.js";
export { toolInputSchema, typedToolInputSchema, wireInputSchema } from "./schema.js";
export { searchWorkspacePaths } from "./builtins/search-tools.js";
export type {
  AnyToolDefinition,
  NormalizedAskUserInput,
  RuntimeToolDefinition,
  ToolDefinition,
  ToolExecutionContext,
  ToolExecutionResultMetadata,
  ToolInputBase,
  ToolInputJsonSchema,
  ToolInputMode,
  ToolRegistryOptions,
} from "./contracts.js";
export {
  coerceLooseStringInput,
  formatAskUserAnswers,
  normalizeAskUserInput,
} from "./input-normalization.js";

export interface PreparedToolInvocation {
  capabilityRequests: CapabilityRequest[];
  displayInput: string;
  normalizedInput: ToolInputValue;
}

export interface DefaultToolRegistryOptions extends ToolRegistryOptions {
  excludeBuiltIns?: string[];
  extraTools?: ToolDefinition[];
  noBuiltIns?: boolean;
}

export class ToolRegistry {
  readonly #tools = new Map<string, RuntimeToolDefinition>();

  constructor(tools: ToolDefinition[], options?: ToolRegistryOptions);
  constructor(tools: AnyToolDefinition[], options?: ToolRegistryOptions);
  constructor(tools: ToolDefinition[] | AnyToolDefinition[], options: ToolRegistryOptions = {}) {
    const strategy = options.conflictStrategy ?? "error";
    for (const tool of tools) {
      if (this.#tools.has(tool.name)) {
        if (strategy === "error") {
          throw new BestAgentError("TOOL_CONFLICT", `Tool ${tool.name} is already registered.`);
        }
        if (strategy === "first-wins") {
          continue;
        }
      }
      this.#tools.set(tool.name, normalizeRuntimeToolDefinition(tool));
    }
  }

  async prepare(
    toolName: string,
    input: ToolInputValue,
    context: ToolExecutionContext,
  ): Promise<PreparedToolInvocation> {
    const tool = this.#getTool(toolName);
    const parsedInput = await tool.parse(input, context);

    return {
      capabilityRequests: await tool.capabilities(parsedInput, context),
      displayInput: tool.formatInput?.(parsedInput) ?? formatToolInput(parsedInput),
      normalizedInput: parsedInput,
    };
  }

  async getCapabilities(
    toolName: string,
    input: ToolInputValue,
    context: ToolExecutionContext,
  ): Promise<CapabilityRequest[]> {
    return (await this.prepare(toolName, input, context)).capabilityRequests;
  }

  validateExactInput(toolName: string, input: ToolInputValue): void {
    const tool = this.#getTool(toolName);
    parseRuntimeToolInput(tool.name, tool.inputSchema, input);
  }

  list(toolNames?: string[]): ToolSummary[] {
    const tools = toolNames
      ? toolNames
          .map((toolName) => this.#tools.get(toolName))
          .filter((tool): tool is RuntimeToolDefinition => Boolean(tool))
      : Array.from(this.#tools.values());

    return tools.map((tool) => ({
      approvalClass: resolveToolApprovalClass(tool),
      concurrencyGroup: resolveToolConcurrencyGroup(tool),
      description: tool.description,
      effect: tool.effect,
      executionClass: tool.executionClass,
      expectedDurationClass: tool.expectedDurationClass,
      heartbeatPolicy: resolveToolHeartbeatPolicy(tool),
      idempotent: tool.idempotent,
      inputSchema: getToolInputJsonSchema(tool.inputSchema),
      name: tool.name,
      replayPolicy: resolveToolReplayPolicy(tool),
      source: tool.source,
      sourceExtension: tool.sourceExtension,
      sourcePlugin: tool.sourcePlugin,
      sourceServer: tool.sourceServer,
    }));
  }

  summarize(toolName: string, input: ToolInputValue, output: string): string {
    const tool = this.#getTool(toolName);
    if (!tool.summarize) {
      return defaultToolSummary(toolName, input, output);
    }
    try {
      return tool.summarize(input, output);
    } catch {
      return defaultToolSummary(toolName, input, output);
    }
  }

  entries(): RuntimeToolDefinition[] {
    return Array.from(this.#tools.values());
  }

  getDefinition(toolName: string): RuntimeToolDefinition {
    return this.#getTool(toolName);
  }

  async execute(
    toolName: string,
    input: ToolInputValue,
    context: ToolExecutionContext,
  ): Promise<string> {
    return this.executeRaw(toolName, input, context);
  }

  async executeRaw(
    toolName: string,
    input: ToolInputValue,
    context: ToolExecutionContext,
  ): Promise<string> {
    const tool = this.#getTool(toolName);
    const parsedInput = await tool.parse(input, context);
    return this.executePrepared(toolName, parsedInput, context);
  }

  async executePrepared(
    toolName: string,
    normalizedInput: ToolInputValue,
    context: ToolExecutionContext,
  ): Promise<string> {
    const tool = this.#getTool(toolName);
    return tool.execute(normalizedInput, context);
  }

  #getTool(toolName: string): RuntimeToolDefinition {
    const tool = this.#tools.get(toolName);
    if (!tool) {
      throw new BestAgentError("TOOL_NOT_FOUND", `Tool ${toolName} is not registered`);
    }
    return tool;
  }
}

export function resolveToolReplayPolicy(
  tool: Pick<ToolDefinition, "executionClass" | "idempotent" | "replayPolicy">,
): ToolReplayPolicy {
  if (tool.replayPolicy) {
    return tool.replayPolicy;
  }
  if (tool.executionClass === "read-only" && tool.idempotent) {
    return "auto-if-idempotent";
  }
  if (tool.executionClass === "workspace-mutation") {
    return "repair-only";
  }
  return "never";
}

export function resolveToolApprovalClass(
  tool: Pick<ToolDefinition, "approvalClass" | "executionClass">,
): ToolApprovalClass {
  return tool.approvalClass ?? tool.executionClass;
}

export function resolveToolConcurrencyGroup(
  tool: Pick<ToolDefinition, "concurrencyGroup" | "executionClass" | "name">,
): string {
  if (tool.concurrencyGroup) {
    return tool.concurrencyGroup;
  }
  return tool.executionClass === "read-only" ? "read-only" : tool.name;
}

export function resolveToolHeartbeatPolicy(
  tool: Pick<ToolDefinition, "heartbeatPolicy">,
): ToolHeartbeatPolicy {
  return tool.heartbeatPolicy ?? "none";
}

function defaultToolSummary(toolName: string, input: ToolInputValue, output: string): string {
  const normalizedInput = truncate(formatToolInput(input));
  const normalizedOutput = truncate(output);
  return `Tool ${toolName} ran with input "${normalizedInput}" and returned "${normalizedOutput}".`;
}

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

export function createDefaultToolRegistry(
  options: ToolDefinition[] | DefaultToolRegistryOptions = [],
): ToolRegistry {
  const resolvedOptions = Array.isArray(options) ? { extraTools: options } : options;
  const builtInTools: AnyToolDefinition[] = [
    ...createBuiltInTools(),
    ...createLspTools().map(toRuntimeToolDefinition),
  ];
  const filteredBuiltIns = resolvedOptions.noBuiltIns
    ? []
    : resolvedOptions.excludeBuiltIns && resolvedOptions.excludeBuiltIns.length > 0
      ? builtInTools.filter((tool) => !resolvedOptions.excludeBuiltIns?.includes(tool.name))
      : builtInTools;

  return new ToolRegistry(
    [...filteredBuiltIns, ...(resolvedOptions.extraTools ?? []).map(toRuntimeToolDefinition)],
    {
      conflictStrategy: resolvedOptions.conflictStrategy,
    },
  );
}

function formatToolInput(input: ToolInputValue): string {
  if (typeof input === "string") {
    return input;
  }
  return JSON.stringify(input);
}

function normalizeRuntimeToolDefinition(
  tool: ToolDefinition | RuntimeToolDefinition,
): RuntimeToolDefinition {
  if (!("parse" in tool)) {
    return toRuntimeToolDefinition(tool);
  }
  return {
    ...tool,
    effect: tool.effect,
  };
}
