import { getMetadata, tokenName, type Token } from '@frontmcp/di';
import { type ServerCapabilities } from '@frontmcp/protocol';
import { ensureMaxLen, getRuntimeContext, isEntryAvailable, sepFor } from '@frontmcp/utils';

import {
  type AppEntry,
  type EntryLineage,
  type EntryOwnerRef,
  type ScopeEntry,
  type ToolEntry,
  type ToolRecord,
  type ToolType,
} from '../common';
import { logAvailabilityFiltering } from '../common/availability';
import { resolveToolVisibility } from '../common/metadata/tool.metadata';
import { isSendElicitationResultTool } from '../elicitation/send-elicitation-result.tool';
import {
  EntryValidationError,
  NameDisambiguationError,
  RegistryDefinitionNotFoundError,
  RegistryGraphEntryNotFoundError,
} from '../errors';
import type ProviderRegistry from '../provider/provider.registry';
import { RegistryAbstract, type RegistryBuildMapResult } from '../regsitry';
import { ownerKeyOf, qualifiedNameOf } from '../utils/lineage.utils';
import { normalizeOwnerPath, normalizeProviderId, normalizeSegment } from '../utils/naming.utils';
import CallToolFlow from './flows/call-tool.flow';
import ToolsListFlow from './flows/tools-list.flow';
import { ToolEmitter, type ToolChangeEvent } from './tool.events';
import { ToolInstance } from './tool.instance';
import { DEFAULT_EXPORT_OPTS, type ExportNameOptions, type IndexedTool } from './tool.types';
import { normalizeTool, toolDiscoveryDeps } from './tool.utils';

export default class ToolRegistry extends RegistryAbstract<
  ToolInstance, // IMPORTANT: instances map holds ToolInstance (not the interface)
  ToolRecord,
  ToolType[]
> {
  /** Who owns this registry (used for provenance). Optional. */
  owner: EntryOwnerRef;

  /** Tools truly owned/constructed by THIS registry (with lineage applied) */
  private localRows: IndexedTool[] = [];

  /** Adopted tool rows from each child registry (references to the same instances) */
  private adopted = new Map<ToolRegistry, IndexedTool[]>();

  /** Children registries that we track */
  private children = new Set<ToolRegistry>();

  /** Remote app tools tracked by app ID for efficient cleanup during re-adoption */
  private remoteAppTools = new Map<string, IndexedTool[]>();

  // ---- O(1) indexes over EFFECTIVE set (local + adopted) ----
  private byQualifiedId = new Map<string, IndexedTool>(); // qualifiedId -> row
  private byName = new Map<string, IndexedTool[]>(); // baseName -> rows
  private byOwnerAndName = new Map<string, IndexedTool>(); // "ownerKey:name" -> row
  private byProviderAndName = new Map<string, IndexedTool>(); // "providerId:name" -> row (best-effort)
  private byOwner = new Map<string, IndexedTool[]>(); // ownerKey -> rows

  // version + emitter
  private version = 0;
  private emitter = new ToolEmitter();

  constructor(providers: ProviderRegistry, list: ToolType[], owner: EntryOwnerRef) {
    // disable auto so subclass fields initialize first
    super('ToolRegistry', providers, list, false);
    this.owner = owner;

    // now it’s safe to run the lifecycle
    this.buildGraph();
    this.ready = this.initialize();
  }

  /* -------------------- Build-time: defs + dep checks -------------------- */

  protected override buildMap(list: ToolType[]): RegistryBuildMapResult<ToolRecord> {
    const tokens = new Set<Token>();
    const defs = new Map<Token, ToolRecord>();
    const graph = new Map<Token, Set<Token>>();
    for (const raw of list) {
      const rec = normalizeTool(raw);
      const provide = rec.provide;
      tokens.add(provide);
      defs.set(provide, rec);
      graph.set(provide, new Set());
    }
    return { tokens, defs, graph };
  }

  protected buildGraph() {
    for (const token of this.tokens) {
      const rec = this.defs.get(token);
      if (!rec) throw new RegistryDefinitionNotFoundError('ToolRegistry', String(token));
      const deps = toolDiscoveryDeps(rec);

      for (const d of deps) {
        // Validate against hierarchical providers; throws early if missing
        this.providers.get(d);
        const tokenDeps = this.graph.get(token);
        if (!tokenDeps) throw new RegistryGraphEntryNotFoundError('ToolRegistry', String(token));
        tokenDeps.add(d);
      }
    }
  }

  /* -------------------- Initialize: create ONE FrontMcpToolInstance per local tool -------------------- */

  protected override async initialize(): Promise<void> {
    // Instantiate each local tool once and store in this.instances (from RegistryAbstract)
    for (const token of this.tokens) {
      const rec = this.defs.get(token);
      if (!rec) throw new RegistryDefinitionNotFoundError('ToolRegistry', String(token));

      // Single, authoritative instance per local tool
      const ti = new ToolInstance(rec, this.providers, this.owner);
      this.instances.set(token as Token<ToolInstance>, ti);

      const lineage: EntryLineage = this.owner ? [this.owner] : [];
      const row = this.makeRow(token, ti, lineage, this);
      this.localRows.push(row);
    }

    const childAppRegistries = this.providers.getRegistries('AppRegistry');
    const scope = this.providers.getActiveScope();
    childAppRegistries.forEach((appRegistry) => {
      const apps = appRegistry.getApps();
      for (const app of apps) {
        if (app.isRemote) {
          // Remote apps: adopt tools directly from the app's tools registry
          this.adoptToolsFromRemoteApp(app, scope);
        } else {
          // Local apps: adopt from child ToolRegistry instances
          this.adoptToolsFromLocalApp(app);
        }
      }
    });

    const childToolRegistries = this.providers.getRegistries('ToolRegistry');
    childToolRegistries
      .filter((t) => t != this)
      .forEach((toolRegistry) => {
        this.adoptFromChild(toolRegistry as ToolRegistry, toolRegistry.owner);
      });

    // Build effective indexes from (locals + already adopted children)
    this.reindex();

    // Log availability filtering results at boot time.
    // This is a REGISTRY-LEVEL concern — not HTTP/flow-level auth or rule-based filtering.
    logAvailabilityFiltering('ToolRegistry', this.listAllInstances(), scope.logger);

    this.bump('reset');

    await scope.registryFlows(ToolsListFlow, CallToolFlow);
  }

  /* -------------------- Adoption: reference child instances (no cloning) -------------------- */

  /**
   * Adopt tools from a child registry. Parent runs after children are ready.
   * We *reference* the child's tool instances; no duplicates are created.
   *
   * IMPORTANT:
   * - Child rows already include the child's own lineage (e.g., adapter:openapi).
   * - Here we only prepend the **parent's** owner, to avoid double-prefixing the child.
   */
  adoptFromChild(child: ToolRegistry, _childOwner: EntryOwnerRef): void {
    if (this.children.has(child)) return;

    const childRows = child.listAllIndexed(); // includes child's lineage
    const prepend: EntryLineage = this.owner ? [this.owner] : [];

    const adoptedRows = childRows.map((r) => this.relineage(r, prepend));

    this.adopted.set(child, adoptedRows);
    this.children.add(child);

    // keep live if child changes
    child.subscribe({ immediate: false }, () => {
      const latest = child.listAllIndexed().map((r) => this.relineage(r, prepend));
      this.adopted.set(child, latest);
      this.reindex();
      this.bump('reset');
    });

    this.reindex();
    this.bump('reset');
  }

  /**
   * Type guard to check if an object has the ToolRegistry subscribe interface.
   * Used for duck-typing remote registries that may not be full ToolRegistry instances.
   */
  private hasSubscribeInterface(obj: unknown): obj is { subscribe: ToolRegistry['subscribe'] } {
    if (obj === null || typeof obj !== 'object') return false;
    const registry = obj as Record<string, unknown>;
    // Check for subscribe method with expected signature (function that accepts options and callback)
    return typeof registry['subscribe'] === 'function';
  }

  /**
   * Adopt tools directly from a remote app's tools registry.
   * Remote apps expose tools via proxy entries that forward execution to the remote server.
   * This also subscribes to updates from the remote app's registry for lazy-loaded tools.
   */
  private adoptToolsFromRemoteApp(app: AppEntry, scope: ScopeEntry): void {
    // Validate that app.tools has the expected interface before casting
    // Remote apps may have different registry implementations
    if (!app.tools || typeof app.tools.getTools !== 'function') {
      scope.logger.warn(`Remote app ${app.id} does not have a valid tools registry interface`);
      return;
    }
    const remoteRegistry = app.tools;

    // Helper to adopt/re-adopt tools from the remote app
    const adoptRemoteTools = () => {
      try {
        // Remove any previously adopted tools from this remote app using the dedicated Map
        const previousTools = this.remoteAppTools.get(app.id);
        if (previousTools && previousTools.length > 0) {
          const previousTokens = new Set(previousTools.map((row) => row.token));
          this.localRows = this.localRows.filter((row) => !previousTokens.has(row.token));
        }

        const remoteTools = app.tools.getTools();
        const newRows: IndexedTool[] = [];

        if (remoteTools.length > 0) {
          const prepend: EntryLineage = this.owner ? [this.owner] : [];
          for (const remoteTool of remoteTools) {
            if (!remoteTool || typeof remoteTool.name !== 'string') {
              scope.logger.warn(`Invalid remote tool in app ${app.id}: missing name`);
              continue;
            }
            // Use Symbol.for() for stable, deterministic tokens across registry operations
            const stableToken = Symbol.for(`tool:${app.id}:${remoteTool.name}`);
            const row = this.makeRow(stableToken, remoteTool, prepend, this);
            this.localRows.push(row);
            newRows.push(row);
          }
        }

        // Track the new rows in the remoteAppTools Map for efficient cleanup
        this.remoteAppTools.set(app.id, newRows);

        this.reindex();
        this.bump('reset');
      } catch (error) {
        scope.logger.error(`Failed to get tools from remote app ${app.id}: ${(error as Error).message}`);
      }
    };

    // Initial adoption (may be empty if remote app hasn't connected yet)
    adoptRemoteTools();

    // Subscribe to remote app's tool registry for lazy-loaded tools
    // Remote apps discover tools asynchronously after connection
    if (this.hasSubscribeInterface(remoteRegistry)) {
      remoteRegistry.subscribe({ immediate: false }, () => {
        adoptRemoteTools();
      });
    }
  }

  /**
   * Adopt tools from a local app's child ToolRegistry instances.
   * Local apps use the hierarchical registry pattern for tool discovery.
   */
  private adoptToolsFromLocalApp(app: AppEntry): void {
    const appToolsRegistries = app.providers.getRegistries('ToolRegistry');
    appToolsRegistries
      .filter((t) => t.owner.kind === 'app')
      .forEach((appToolRegistry) => {
        this.adoptFromChild(appToolRegistry as ToolRegistry, appToolRegistry.owner);
      });
  }

  // NOTE: `any` is intentional here - heterogeneous tool collections can't use `unknown`
  // because ToolEntry has constrained generics (InSchema extends ToolInputType, etc.)
  //
  // AVAILABILITY FILTERING (registry-level, not HTTP/flow-level):
  // `availableWhen` is evaluated against the process's RuntimeContext (OS, runtime, env).
  // This is a hard constraint — unlike hideFromDiscovery (soft listing hide) or
  // authorization (per-request in HTTP flows). Filtered entries cannot be listed or called.
  getTools(includeHidden = false): ToolEntry<any, any>[] {
    const ctx = getRuntimeContext();
    const local = [...this.localRows].flat().map((t) => t.instance);
    const adopted = [...this.adopted.values()].flat().map((t) => t.instance);
    return [...local, ...adopted].filter((t) => {
      // Availability: hard filter — unavailable entries are excluded from both listing and execution
      if (!isEntryAvailable(t.metadata.availableWhen, ctx)) return false;
      // Visibility: soft filter — hidden/internal entries are excluded from listings.
      // `internal` and `hidden` both behave like the legacy hideFromDiscovery here;
      // the call-tool flow enforces the additional external-call gate for `internal`.
      if (!includeHidden && resolveToolVisibility(t.metadata) !== 'public') return false;
      return true;
    });
  }

  /**
   * Get tools appropriate for MCP listing based on client elicitation support.
   *
   * Filters applied:
   * - Excludes `visibility !== 'public'` tools (hidden + internal). Hidden tools
   *   remain externally callable by name; internal tools do not (gate enforced
   *   in the call-tool flow).
   * - Adds the sendElicitationResult fallback tool when the client doesn't
   *   support standard elicitation. The fallback is itself `visibility: 'hidden'`
   *   (it must remain externally callable), so the elicitation lookup uses
   *   `getTools(true)` to bypass the visibility filter for that one case.
   *
   * @param supportsElicitation - Whether the client supports MCP elicitation (from session payload)
   * @returns Tools appropriate for this client
   */
  // NOTE: `any` is intentional - see getTools comment above
  getToolsForListing(supportsElicitation?: boolean): ToolEntry<any, any>[] {
    const tools = this.getTools(false); // visibility=public only

    // If client doesn't support standard elicitation, add the fallback tool
    if (!supportsElicitation) {
      const allTools = this.getTools(true); // Include hidden/internal to find elicitation tool
      const elicitTool = allTools.find((t) => isSendElicitationResultTool(t.metadata?.name ?? ''));
      if (elicitTool && resolveToolVisibility(elicitTool.metadata) !== 'internal' && !tools.includes(elicitTool)) {
        return [...tools, elicitTool];
      }
    }

    return tools;
  }

  // NOTE: `any` is intentional - see getTools comment above
  getInlineTools(): ToolEntry<any, any>[] {
    return [...this.instances.values()];
  }

  /** Internal snapshot of effective indexed rows (locals + adopted). */
  private listAllIndexed(): IndexedTool[] {
    return [...this.localRows, ...[...this.adopted.values()].flat()];
  }

  /* -------------------- Indexing & lookups -------------------- */

  private reindex() {
    const effective = this.listAllIndexed();

    this.byQualifiedId.clear();
    this.byName.clear();
    this.byOwnerAndName.clear();
    this.byProviderAndName.clear();
    this.byOwner.clear();

    for (const r of effective) {
      this.byQualifiedId.set(r.qualifiedId, r);

      const listByName = this.byName.get(r.baseName) ?? [];
      listByName.push(r);
      this.byName.set(r.baseName, listByName);

      const byOwnerKey = this.byOwner.get(r.ownerKey) ?? [];
      byOwnerKey.push(r);
      this.byOwner.set(r.ownerKey, byOwnerKey);

      const on = `${r.ownerKey}:${r.baseName}`;
      if (!this.byOwnerAndName.has(on)) this.byOwnerAndName.set(on, r);

      const provId = this.providerIdOf(r.instance);
      if (provId) {
        const pn = `${provId}:${r.baseName}`;
        if (!this.byProviderAndName.has(pn)) this.byProviderAndName.set(pn, r);
      }
    }
  }

  /** List all instances (locals + adopted). */
  listAllInstances(): readonly ToolEntry[] {
    return this.listAllIndexed().map((r) => r.instance);
  }

  /** List instances by owner path (e.g. "app:Portal/plugin:Okta") */
  listByOwner(ownerPath: string): readonly ToolEntry[] {
    return (this.byOwner.get(ownerPath) ?? []).map((r) => r.instance);
  }

  /* -------------------- Conflict-aware exported names -------------------- */

  /**
   * Produce unique, MCP-valid exported names.
   * - Base (standardized) = metadata.name cased (snake/camel/kebab/dot)
   * - If conflicting:
   *    - Locals keep bare base (unless >1 locals conflict → they get owner prefixes)
   *    - Children with same base get prefixed by providerId (or owner path)
   */
  exportResolvedNames(opts?: ExportNameOptions): Array<{ name: string; instance: ToolEntry }> {
    const cfg = { ...DEFAULT_EXPORT_OPTS, ...(opts ?? {}) };

    const rows = this.listAllIndexed().map((r) => {
      const base = normalizeSegment(r.baseName, cfg.case);
      const isLocal = r.source === this;
      const provider = normalizeProviderId(this.providerIdOf(r.instance), cfg.case);
      const ownerPath = normalizeOwnerPath(r.ownerKey, cfg.case);
      return { base, row: r, isLocal, provider, ownerPath };
    });

    // group by standardized base
    const byBase = new Map<string, typeof rows>();
    for (const r of rows) {
      const list = byBase.get(r.base) ?? [];
      list.push(r);
      byBase.set(r.base, list);
    }

    const out = new Map<string, ToolEntry>();

    for (const [base, group] of byBase.entries()) {
      if (group.length === 1) {
        const g = group[0];
        out.set(ensureMaxLen(base, cfg.maxLen), g.row.instance);
        continue;
      }

      const locals = group.filter((g) => g.isLocal);
      const children = group.filter((g) => !g.isLocal);

      if (cfg.prefixChildrenOnConflict && locals.length > 0) {
        // Locals
        if (locals.length === 1) {
          const localName = ensureMaxLen(base, cfg.maxLen);
          out.set(disambiguate(localName, out, cfg), locals[0].row.instance);
        } else {
          for (const l of locals) {
            const pref = normalizeOwnerPath(l.ownerPath, cfg.case);
            const name = ensureMaxLen(`${pref}${sepFor(cfg.case)}${base}`, cfg.maxLen);
            out.set(disambiguate(name, out, cfg), l.row.instance);
          }
        }

        // Children
        for (const c of children) {
          const pre = cfg.prefixSource === 'provider' ? (c.provider ?? c.ownerPath) : c.ownerPath;
          const name = ensureMaxLen(`${pre}${sepFor(cfg.case)}${base}`, cfg.maxLen);
          out.set(disambiguate(name, out, cfg), c.row.instance);
        }
      } else {
        // Prefix everyone by source
        for (const r of group) {
          const pre = cfg.prefixSource === 'provider' ? (r.provider ?? r.ownerPath) : r.ownerPath;
          const name = ensureMaxLen(`${pre}${sepFor(cfg.case)}${base}`, cfg.maxLen);
          out.set(disambiguate(name, out, cfg), r.row.instance);
        }
      }
    }

    return [...out.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([name, instance]) => ({ name, instance }));

    function disambiguate(candidate: string, pool: Map<string, ToolEntry>, cfg: Required<ExportNameOptions>): string {
      if (!pool.has(candidate)) return candidate;
      const maxAttempts = 10000;
      let n = 2;
      while (n <= maxAttempts) {
        const withN = ensureMaxLen(`${candidate}${sepFor(cfg.case)}${n}`, cfg.maxLen);
        if (!pool.has(withN)) return withN;
        n++;
      }
      throw new NameDisambiguationError(candidate, maxAttempts);
    }
  }

  /** Lookup by the exported (resolved) name. */
  getExported(name: string, opts?: ExportNameOptions): ToolEntry | undefined {
    const pairs = this.exportResolvedNames(opts);
    return pairs.find((p) => p.name === name)?.instance;
  }

  /* -------------------- Subscriptions -------------------- */

  subscribe(
    opts: { immediate?: boolean; filter?: (i: ToolEntry) => boolean },
    cb: (evt: ToolChangeEvent) => void,
  ): () => void {
    const filter = opts.filter ?? (() => true);
    if (opts.immediate) {
      cb({
        kind: 'reset',
        changeScope: 'global',
        version: this.version,
        snapshot: this.listAllInstances().filter(filter),
      });
    }
    return this.emitter.on((e) => cb({ ...e, snapshot: this.listAllInstances().filter(filter) }));
  }

  private bump(kind: ToolChangeEvent['kind']) {
    const version = ++this.version;
    this.emitter.emit({ kind, changeScope: 'global', version, snapshot: this.listAllInstances() });
  }

  /* -------------------- Helpers -------------------- */

  /** Build an IndexedTool row */
  private makeRow(token: Token, instance: ToolEntry, lineage: EntryLineage, source: ToolRegistry): IndexedTool {
    const ownerKey = ownerKeyOf(lineage);
    const baseName = instance.name;
    const qualifiedName = qualifiedNameOf(lineage, baseName);
    const qualifiedId = `${ownerKey}:${tokenName(token)}`;
    return { token, instance, baseName, lineage, ownerKey, qualifiedName, qualifiedId, source };
  }

  /** Clone a child row and prepend lineage (with adjacent de-dup to avoid double prefixes). */
  private relineage(row: IndexedTool, prepend: EntryLineage): IndexedTool {
    const merged = [...prepend, ...row.lineage];
    const lineage = dedupLineage(merged);

    const ownerKey = ownerKeyOf(lineage);
    const qualifiedName = qualifiedNameOf(lineage, row.baseName);
    const qualifiedId = `${ownerKey}:${tokenName(row.token)}`;
    return {
      token: row.token,
      instance: row.instance, // REFERENCE the same instance
      baseName: row.baseName,
      lineage,
      ownerKey,
      qualifiedName,
      qualifiedId,
      source: row.source, // keep original source (who constructed instance)
    };
  }

  /** Best-effort provider id used for prefixing (inspects class metadata if present). */
  private providerIdOf(inst: ToolEntry): string | undefined {
    // Try reading provider id from the tool metadata (cast through unknown for type safety)
    try {
      const meta = inst.metadata as unknown as Record<string, unknown> | undefined;
      if (!meta || typeof meta !== 'object') return undefined;

      const maybe = meta['providerId'] ?? meta['provider'] ?? meta['ownerId'];
      if (typeof maybe === 'string' && maybe.length) return maybe;

      const cls = meta['cls'];
      if (cls && typeof cls === 'function') {
        const id = getMetadata('id', cls);
        if (typeof id === 'string' && id.length) return id;
        if ('name' in cls && typeof cls.name === 'string') return cls.name;
      }
    } catch {
      /* ignore */
    }
    return undefined;
  }

  /** True if this registry (or adopted children) has any tools. */
  hasAny(): boolean {
    return this.listAllIndexed().length > 0 || this.tokens.size > 0;
  }

  /**
   * Replace all tools owned by the given owner.
   * Clears local rows, rebuilds from new list, reindexes, and emits 'reset'.
   * Used by adapter polling to hot-swap tools when specs change.
   */
  replaceAll(list: ToolType[], owner: EntryOwnerRef): void {
    // Clear local rows, instances, and remote app tool tracking
    this.localRows = [];
    this.instances.clear();
    this.remoteAppTools.clear();

    // Clear internal maps from base class
    this.tokens.clear();
    this.defs.clear();
    this.graph.clear();

    // Rebuild from new list
    const { tokens, defs, graph } = this.buildMap(list);
    for (const [key, val] of defs) {
      this.defs.set(key, val);
      this.graph.set(key, graph.get(key) ?? new Set());
    }
    for (const t of tokens) {
      this.tokens.add(t);
    }

    // Rebuild graph dependencies
    this.buildGraph();

    // Recreate instances and local rows
    for (const token of this.tokens) {
      const rec = this.defs.get(token);
      if (!rec) throw new RegistryDefinitionNotFoundError('ToolRegistry', String(token));
      const ti = new ToolInstance(rec, this.providers, owner);
      this.instances.set(token as Token<ToolInstance>, ti);

      const lineage: EntryLineage = owner ? [owner] : [];
      const row = this.makeRow(token, ti, lineage, this);
      this.localRows.push(row);
    }

    // Update owner reference
    this.owner = owner;

    // Rebuild indexes and notify
    this.reindex();
    this.bump('reset');
  }

  /**
   * Type guard to check if a ToolEntry has the required properties for registration.
   * Validates that the tool has record with provide, kind, metadata, and a valid name.
   */
  private isRegisterableTool(
    tool: ToolEntry,
  ): tool is ToolEntry & { record: { provide: Token; kind: string; metadata: unknown }; name: string } {
    // Check for required name property
    if (typeof tool.name !== 'string' || !tool.name) {
      return false;
    }

    // Check for record property (exists on ToolInstance and RemoteToolInstance)
    const toolWithRecord = tool as { record?: { provide?: unknown; kind?: unknown; metadata?: unknown } };
    if (!toolWithRecord.record) {
      return false;
    }

    const { record } = toolWithRecord;
    return record.provide !== undefined && record.kind !== undefined && record.metadata !== undefined;
  }

  /**
   * Register an existing ToolInstance directly (for agent-scoped or remote tools).
   * This allows pre-constructed tool instances to be added without going through
   * the standard token-based initialization flow.
   *
   * **IMPORTANT: Scope Binding**
   * The ToolInstance captures its scope and providers at construction time.
   * The tool will use the scope from its original `providers` argument, NOT this registry's scope.
   * Therefore, you must create the ToolInstance with the correct providers for the target scope:
   *
   * ```typescript
   * // Correct: Create with agent's providers
   * const tool = new ToolInstance(record, agentProviders, owner);
   * agentTools.registerToolInstance(tool);
   *
   * // For remote tools: Use RemoteToolInstance
   * const remoteTool = new RemoteToolInstance(record, mcpClient, providers, owner);
   * remoteTools.registerToolInstance(remoteTool);
   *
   * // Wrong: Reusing app-scoped tool in agent context
   * // The tool will still use app's scope/hooks, not agent's!
   * agentTools.registerToolInstance(existingAppTool);
   * ```
   *
   * @param tool - The tool instance to register (ToolInstance or RemoteToolInstance)
   * @throws Error if tool is not a valid instance
   */
  registerToolInstance(tool: ToolEntry): void {
    // Validate using type guard - accepts both ToolInstance and RemoteToolInstance
    if (!this.isRegisterableTool(tool)) {
      // Provide specific error messages for debugging
      if (typeof tool.name !== 'string' || !tool.name) {
        throw new EntryValidationError('Tool', 'missing required name property');
      }
      const toolWithRecord = tool as { record?: { provide?: unknown; kind?: unknown; metadata?: unknown } };
      if (!toolWithRecord.record) {
        throw new EntryValidationError('Tool', 'missing required record property');
      }
      if (!toolWithRecord.record.provide) {
        throw new EntryValidationError('Tool', 'missing required record.provide property');
      }
      if (!toolWithRecord.record.kind || !toolWithRecord.record.metadata) {
        throw new EntryValidationError('Tool', 'missing required record.kind or record.metadata');
      }
      throw new EntryValidationError('Tool', 'validation failed');
    }

    const token = tool.record.provide as Token;

    // Check for duplicate registration
    if (this.instances.has(token as Token<ToolInstance>)) {
      return; // Already registered, skip silently
    }

    // Add to instances map - the type guard ensures tool has the required properties
    this.instances.set(token as Token<ToolInstance>, tool as ToolInstance);

    // Create an indexed row for this tool
    const lineage: EntryLineage = this.owner ? [this.owner] : [];
    const row = this.makeRow(token, tool, lineage, this);
    this.localRows.push(row);

    // Rebuild indexes
    this.reindex();
    this.bump('reset');
  }

  /**
   * Unregister a tool instance previously added via `registerToolInstance`.
   * Returns true if the token was found and removed, false otherwise.
   *
   * Used by dynamic registrants (e.g. the OpenAPI internal-tool adapter) when
   * a bundle swap drops or replaces operations. The instance map and indexed
   * rows are kept in sync; downstream consumers see the change after `reindex`.
   */
  unregisterToolInstance(token: Token): boolean {
    const typedToken = token as Token<ToolInstance>;
    const existed = this.instances.delete(typedToken);
    const before = this.localRows.length;
    this.localRows = this.localRows.filter((row) => row.token !== token);
    const removed = existed || this.localRows.length !== before;
    if (removed) {
      this.reindex();
      this.bump('reset');
    }
    return removed;
  }

  /**
   * Get the MCP capabilities for tools.
   * These are reported to clients during initialization.
   *
   * Issue #407: we always advertise the `tools` capability so the MCP SDK
   * accepts the always-registered `tools/list` handler (see
   * `createMcpHandlers` — registering a handler without the matching
   * capability throws `Server does not support tools (required for tools/list)`).
   * `listChanged` still reflects whether anything is actually registered.
   */
  getCapabilities(): Partial<ServerCapabilities> {
    return {
      tools: {
        listChanged: this.hasAny(),
      },
    };
  }
}

/* -------------------- lineage utility -------------------- */

/** Remove only adjacent duplicates like [adapter:x, adapter:x] → [adapter:x] */
function dedupLineage(l: EntryLineage): EntryLineage {
  const out: EntryLineage = [];
  for (const o of l) {
    const last = out[out.length - 1];
    if (!last || last.kind !== o.kind || last.id !== o.id) out.push(o);
  }
  return out;
}
