import type { Tag } from "@routecraft/routecraft";
import type {
  McpTool,
  McpToolAnnotations,
  McpToolRegistryEntry,
} from "./types.ts";

/**
 * Derive capability tags from MCP `annotations` hints so MCP tools
 * surface alongside fns and direct routes under the same `Tag`
 * selectors (`tools([{ tagged: "read-only" }])`).
 *
 * Mapping: `readOnlyHint -> "read-only"`,
 * `destructiveHint -> "destructive"`, `idempotentHint -> "idempotent"`,
 * `openWorldHint -> "open-world"`. Returns an empty array (omitted
 * from the entry) when no hints apply.
 *
 * @internal
 */
function deriveTagsFromAnnotations(
  annotations: McpToolAnnotations | undefined,
): Tag[] {
  if (!annotations) return [];
  const tags: Tag[] = [];
  if (annotations.readOnlyHint) tags.push("read-only");
  if (annotations.destructiveHint) tags.push("destructive");
  if (annotations.idempotentHint) tags.push("idempotent");
  if (annotations.openWorldHint) tags.push("open-world");
  return tags;
}

/**
 * Central registry of MCP tools discovered from remote MCP servers.
 * Stored in context store under MCP_TOOL_REGISTRY for agent adapter discovery.
 *
 * Populated automatically by mcpPlugin for these sources:
 * - stdio clients: long-lived subprocess MCP servers
 * - HTTP clients: remote HTTP MCP servers (tools refreshed periodically)
 *
 * Local `mcp()` routes defined in the same context are NOT auto-populated here;
 * they live in the parallel `MCP_LOCAL_TOOL_REGISTRY` store and are read from
 * there by the MCP server when responding to `tools/list`. The "local" transport
 * label remains a valid value for callers that want to manually register tools
 * with that provenance.
 *
 * @experimental
 */
export class McpToolRegistry {
  /** Nested Map: source -> toolName -> McpToolRegistryEntry */
  private tools = new Map<string, Map<string, McpToolRegistryEntry>>();

  /**
   * Set all tools for a given source (replaces previous tools from that source).
   * Called on initial tool listing and on re-listing after restart/refresh.
   *
   * @param source - Identifier for the tool source (e.g. server ID or "local")
   * @param transport - How the tools are reached ("stdio", "http", or "local")
   * @param tools - Tool definitions to register for this source
   */
  setToolsForSource(
    source: string,
    transport: "stdio" | "http" | "local",
    tools: McpTool[],
  ): void {
    const sourceMap = new Map<string, McpToolRegistryEntry>();

    for (const tool of tools) {
      const entry: McpToolRegistryEntry = {
        name: tool.name,
        inputSchema: tool.inputSchema as Record<string, unknown>,
        source,
        transport,
      };
      if (tool.description !== undefined) {
        entry.description = tool.description;
      }
      if (tool.annotations !== undefined) {
        entry.annotations = tool.annotations;
      }
      const derivedTags = deriveTagsFromAnnotations(tool.annotations);
      if (derivedTags.length > 0) {
        entry.tags = derivedTags;
      }
      sourceMap.set(tool.name, entry);
    }

    this.tools.set(source, sourceMap);
  }

  /**
   * Remove all tools for a source (when a client is permanently stopped).
   *
   * @param source - Source identifier whose tools should be removed
   */
  removeSource(source: string): void {
    this.tools.delete(source);
  }

  /**
   * Get all tools across all sources.
   *
   * @returns Array of every registered tool entry
   */
  getTools(): McpToolRegistryEntry[] {
    const result: McpToolRegistryEntry[] = [];
    for (const sourceMap of this.tools.values()) {
      for (const entry of sourceMap.values()) {
        result.push(entry);
      }
    }
    return result;
  }

  /**
   * Get tools from a specific source/server.
   *
   * @param serverId - Source identifier to filter by
   * @returns Array of tool entries belonging to that source
   */
  getToolsByServer(serverId: string): McpToolRegistryEntry[] {
    const sourceMap = this.tools.get(serverId);
    if (!sourceMap) return [];
    return Array.from(sourceMap.values());
  }

  /**
   * Get a specific tool by name. Returns first match if name exists in multiple sources.
   *
   * @param name - Tool name to search for
   * @returns The first matching entry, or undefined
   */
  getTool(name: string): McpToolRegistryEntry | undefined {
    for (const sourceMap of this.tools.values()) {
      const entry = sourceMap.get(name);
      if (entry) return entry;
    }
    return undefined;
  }

  /**
   * Get a specific tool by source and name.
   *
   * @param source - Source identifier
   * @param name - Tool name
   * @returns The matching entry, or undefined
   */
  getToolBySource(
    source: string,
    name: string,
  ): McpToolRegistryEntry | undefined {
    return this.tools.get(source)?.get(name);
  }
}
