import type { ToolDef } from '../llm/openai-compat.js';
import type { McpRegistry } from './registry.js';
import type { McpTokenManager } from './token-manager.js';
import type { McpToolCache } from './tool-cache.js';
import { buildToolDefsFromCache, parseToolName } from './tool-adapter.js';
import type { ExecuteCtx, ExecuteResult } from './tool-executor.js';
import { isKeyConfigured } from './crypto.js';
import { logger } from '../logger.js';

export interface AggregatorDeps {
  registry: McpRegistry;
  tokenManager: McpTokenManager;
  toolCache: McpToolCache;
  executeCall: (args: {
    serverId: string;
    toolName: string;
    input: Record<string, unknown>;
    ctx: ExecuteCtx;
    accessToken: string;
    serverUrl: string;
  }) => Promise<ExecuteResult>;
}

export interface AggregatorCtx extends ExecuteCtx {
  // Per-space isolation: the job's space. undefined = legacy NULL-space job.
  // Used as a defense-in-depth guard in executeTool so a server outside the
  // job's space cannot be invoked even if its tool name is somehow guessed.
  spaceId?: string | null;
}

export function createAggregator(deps: AggregatorDeps) {
  return {
    /**
     * Enumerate MCP tool defs the agent may see for this job.
     *
     * Per-space isolation (spaces foundation, fully-independent model): the
     * server set is scoped to `spaceId` via registry.listEnabledForSpace —
     * a job sees ONLY its space's servers; global/other-space servers are
     * invisible. When `spaceId` is undefined (gitea-issue / subtask /
     * unmigrated job that carries no space), we fall back to the legacy
     * NULL-space set (`listEnabledForSpace(null)`), so non-space jobs keep
     * their previous behaviour.
     *
     * Tokens remain per-user (OAuth) and are still resolved by userId.
     */
    async getToolDefs(
      userId: string,
      allowedPatterns: string[],
      spaceId?: string | null,
    ): Promise<ToolDef[]> {
      if (!isKeyConfigured()) return [];
      const mcpPatterns = allowedPatterns.filter((p) => p.startsWith('mcp__'));
      if (mcpPatterns.length === 0) return [];

      // spaceId === undefined → legacy fallback (NULL-space servers).
      const servers = deps.registry.listEnabledForSpace(spaceId ?? null);
      const connected = servers.filter((s) => deps.tokenManager.hasToken(userId, s.id));
      if (connected.length === 0) return [];

      const cache = deps.toolCache.getAllForServers(connected.map((s) => s.id));
      const names = new Map(connected.map((s) => [s.id, s.name]));
      return buildToolDefsFromCache(cache, mcpPatterns, names);
    },

    async executeTool(
      name: string,
      input: Record<string, unknown>,
      ctx: AggregatorCtx,
    ): Promise<ExecuteResult | null> {
      if (!name.startsWith('mcp__')) return null;
      const parsed = parseToolName(name);
      if (!parsed) return { output: `不正な MCP ツール名: ${name}`, isError: true };

      const server = deps.registry.getDecrypted(parsed.serverId);
      if (!server || !server.enabled) {
        return { output: `MCP サーバーが利用不可: ${parsed.serverId}`, isError: true };
      }

      // Defense-in-depth per-space isolation: only servers belonging to the
      // job's space (or the legacy NULL-space set when ctx.spaceId is
      // undefined) may be invoked. This mirrors the enumeration scoping in
      // getToolDefs and blocks cross-space calls even if a tool name leaks.
      const ctxSpaceId = ctx.spaceId ?? null;
      if ((server.spaceId ?? null) !== ctxSpaceId) {
        logger.warn(
          `[mcp:aggregator] cross-space MCP call blocked server=${parsed.serverId} serverSpace=${server.spaceId ?? 'null'} jobSpace=${ctxSpaceId ?? 'null'}`,
        );
        return { output: `MCP サーバーが利用不可: ${parsed.serverId}`, isError: true };
      }

      let accessToken: string;
      try {
        accessToken = await deps.tokenManager.getValidToken(ctx.ownerId, parsed.serverId);
      } catch (err) {
        return { output: `MCP 認証が必要: ${(err as Error).message}`, isError: true };
      }

      logger.debug(`[mcp:aggregator] execute server=${parsed.serverId} tool=${parsed.toolName}`);
      return deps.executeCall({
        serverId: parsed.serverId,
        toolName: parsed.toolName,
        input,
        ctx,
        accessToken,
        serverUrl: server.url,
      });
    },
  };
}

export type McpAggregator = ReturnType<typeof createAggregator>;
