import express from 'express';
import { Repository } from '../db/repository.js';
import { logger } from '../logger.js';
import { loadConfig } from '../config.js';
import { requireAuth, requireAdmin } from './auth.js';
import { canEditInSpace } from './visibility.js';
import { isKeyConfigured } from '../mcp/crypto.js';
import { createRegistry } from '../mcp/registry.js';
import { createTokenManager } from '../mcp/token-manager.js';
import { createToolCache } from '../mcp/tool-cache.js';
import { createAggregator } from '../mcp/aggregator.js';
import { createMcpClient } from '../mcp/client-factory.js';
import { executeMcpCall } from '../mcp/tool-executor.js';
import { refreshAccessToken } from '../mcp/discovery.js';
import { setMcpAggregator } from '../engine/tools/index.js';
import { setMcpToolLookup } from '../engine/tools/docs.js';
import { setCalendarRepo } from '../engine/tools/calendar.js';
import { createMcpOauthRouter } from '../mcp/oauth-routes.js';
import { createAdminRouter as createMcpAdminRouter, createUserRouter as createMcpUserRouter, createUserServersRouter as createMcpUserServersRouter } from './mcp-api.js';
import { mergeMcpConfig } from '../mcp/config.js';
import type { McpCatalogDeps } from './tools-api.js';

export interface McpSubsystemDeps {
  repo: Repository;
  authActive: boolean;
  workerManager?: import('../worker-manager.js').WorkerManager;
  /** OAuth callback base URL (derived from auth config by the caller). */
  callbackBaseUrl: string;
}

/**
 * Wire the MCP subsystem (registry / token manager / tool cache / aggregator)
 * and mount its OAuth + admin/connections/user-servers routers. Gated on
 * MCP_ENCRYPTION_KEY: when the key is absent the subsystem is skipped and the
 * function returns null.
 *
 * Returns the McpCatalogDeps that /api/tools uses to surface per-user MCP
 * tools in the workspace Tools settings editor (null when MCP is disabled).
 *
 * Extracted verbatim from createCoreServer (server.ts) to keep that bootstrap
 * function focused; see server-subsystems.test.ts for the gate characterization.
 */
export function setupMcpSubsystem(app: express.Application, deps: McpSubsystemDeps): McpCatalogDeps | null {
  const { repo, authActive, workerManager, callbackBaseUrl } = deps;

  // Surfaces per-user MCP servers into /api/tools so the workspace Tools
  // settings editor can include them. Populated when the subsystem initialises;
  // remains null otherwise.
  let mcpCatalogDeps: McpCatalogDeps | null = null;

  if (isKeyConfigured()) {
    const mcpConfig = mergeMcpConfig(loadConfig().mcp);
    const mcpRegistry = createRegistry(repo.getDb());
    const mcpTokenManager = createTokenManager(repo.getDb(), {
      doRefresh: async (serverId: string, refreshToken: string) => {
        const server = mcpRegistry.getDecrypted(serverId);
        if (!server || !server.tokenEndpoint) {
          throw new Error(`server or token endpoint missing for ${serverId}`);
        }
        return refreshAccessToken({
          tokenEndpoint: server.tokenEndpoint,
          clientId: server.oauthClientId,
          clientSecret: server.oauthClientSecret,
          refreshToken,
        });
      },
    });
    const mcpToolCache = createToolCache(repo.getDb(), mcpConfig.toolCacheTtlSeconds);
    const mcpAggregator = createAggregator({
      registry: mcpRegistry,
      tokenManager: mcpTokenManager,
      toolCache: mcpToolCache,
      executeCall: async (args) => {
        const server = mcpRegistry.getDecrypted(args.serverId);
        if (!server) return { output: `未登録の MCP サーバー: ${args.serverId}`, isError: true };
        const { client, close } = await createMcpClient(server, args.accessToken, {
          callTimeoutMs: mcpConfig.callTimeoutSeconds * 1000,
          allowPrivateAddresses: mcpConfig.allowPrivateAddresses,
        });
        try {
          return await executeMcpCall({
            client,
            serverId: args.serverId,
            toolName: args.toolName,
            input: args.input,
            ctx: args.ctx,
          });
        } finally {
          await close();
        }
      },
    });
    setMcpAggregator(mcpAggregator);
    setMcpToolLookup((serverId, toolName) => mcpToolCache.get(serverId, toolName));
    setCalendarRepo(repo);
    workerManager?.setMcpDeps({ tokenManager: mcpTokenManager });

    // Expose MCP enumeration to /api/tools so the workspace Tools settings
    // editor can list per-user MCP tools alongside builtin ones. Methods on the
    // registry/tokenManager/toolCache surfaces are already DB-backed and
    // safe to call per-request.
    mcpCatalogDeps = {
      registry: { listEnabledForUser: (uid) => mcpRegistry.listEnabledForUser(uid) },
      tokenManager: { hasToken: (uid, sid) => mcpTokenManager.hasToken(uid, sid) },
      toolCache: { getAllForServers: (ids) => mcpToolCache.getAllForServers(ids) },
    };

    app.use(
      '/auth/mcp',
      createMcpOauthRouter({
        db: repo.getDb(),
        registry: mcpRegistry,
        tokenManager: mcpTokenManager,
        pendingTtlMinutes: mcpConfig.oauthPendingTtlMinutes,
        getCallbackBaseUrl: () => callbackBaseUrl,
        getAuthenticatedUserId: (req) => (req.user as { id?: string } | undefined)?.id ?? null,
        resumeWaitingJobs: (uid, sid) => {
          repo.resumeMcpWaitingJobs(uid, sid);
        },
        listToolsAfterAuth: async (serverId: string, accessToken: string) => {
          const server = mcpRegistry.getDecrypted(serverId);
          if (!server) return;
          const { client, close } = await createMcpClient(server, accessToken, {
            callTimeoutMs: mcpConfig.callTimeoutSeconds * 1000,
            allowPrivateAddresses: mcpConfig.allowPrivateAddresses,
          });
          try {
            const list = (await client.listTools()) as {
              tools: Array<{ name: string; description?: string; inputSchema?: unknown }>;
            };
            mcpToolCache.replaceForServer(serverId, list.tools);
            logger.info(
              `[mcp] auto list_tools after OAuth server=${serverId} count=${list.tools.length}`,
            );
          } finally {
            await close();
          }
        },
      }),
    );

    app.use('/api/mcp/servers', express.json(), createMcpAdminRouter({
      db: repo.getDb(),
      registry: mcpRegistry,
      tokenManager: mcpTokenManager,
      toolCache: mcpToolCache,
      requireAdmin: authActive ? requireAdmin : (_req, _res, next) => next(),
      requireAuth: authActive ? requireAuth : (_req, _res, next) => next(),
      getUserId: (req) => (req.user as { id?: string } | undefined)?.id ?? null,
      getSpace: (spaceId, viewer) => repo.getSpace(spaceId, { viewer }),
      allowPrivateAddresses: mcpConfig.allowPrivateAddresses,
    }));

    app.use('/api/mcp/connections', express.json(), createMcpUserRouter({
      db: repo.getDb(),
      registry: mcpRegistry,
      tokenManager: mcpTokenManager,
      toolCache: mcpToolCache,
      requireAdmin: authActive ? requireAdmin : (_req, _res, next) => next(),
      requireAuth: authActive ? requireAuth : (_req, _res, next) => next(),
      getUserId: (req) => (req.user as { id?: string } | undefined)?.id ?? null,
      allowPrivateAddresses: mcpConfig.allowPrivateAddresses,
    }));

    app.use('/api/mcp/user-servers', express.json(), createMcpUserServersRouter({
      db: repo.getDb(),
      registry: mcpRegistry,
      tokenManager: mcpTokenManager,
      toolCache: mcpToolCache,
      requireAdmin: authActive ? requireAdmin : (_req, _res, next) => next(),
      requireAuth: authActive ? requireAuth : (_req, _res, next) => next(),
      // No-auth single-user mode: passport is not mounted, so req.user is
      // never populated. Fall back to the synthetic `local` admin (the same
      // owner the SSH user router and space API use) so per-user / per-space
      // MCP server management stays usable; otherwise getUserId is null and
      // every register/list 401s, making the per-space MCP panel dead in
      // no-auth deployments. No-op when auth is active (req.user wins).
      getUserId: (req) => (req.user as { id?: string } | undefined)?.id ?? (authActive ? null : 'local'),
      // Mirror the no-auth fallback: when auth is off and passport hasn't
      // populated a viewer, authorize the space against the synthetic `local`
      // admin so getSpace doesn't reject the (null-owner) local space.
      getSpace: (spaceId, viewer) =>
        repo.getSpace(spaceId, {
          viewer: viewer ?? (authActive ? undefined : ({ id: 'local', role: 'admin', orgIds: [] } as unknown as Express.User)),
        }),
      // Space-as-Principal P2: edit-rights check so space members can share the
      // management of space-owned api_key MCP servers. Mirrors the no-auth
      // synthetic local admin used by getUserId/getSpace above.
      canEditSpace: async (spaceId, req) => {
        const viewer =
          (req.user as Express.User | undefined) ??
          (authActive ? undefined : ({ id: 'local', role: 'admin', orgIds: [] } as unknown as Express.User));
        if (!viewer) return false;
        const space = await repo.getSpace(spaceId, { viewer });
        if (!space) return false;
        const role = repo.getSpaceMemberRole(spaceId, viewer.id);
        return canEditInSpace(viewer, { ownerId: space.ownerId }, role);
      },
      insecureLocalTestMode: false,
      allowPrivateAddresses: mcpConfig.allowPrivateAddresses,
    }));

    logger.info('[mcp] subsystem initialised');
  } else {
    logger.warn('[mcp] MCP_ENCRYPTION_KEY not configured — MCP features disabled');
  }

  return mcpCatalogDeps;
}
