import { Router, type Request } from 'express';
import type Database from 'better-sqlite3';
import type { McpRegistry } from '../mcp/registry.js';
import type { McpTokenManager } from '../mcp/token-manager.js';
import type { McpToolCache } from '../mcp/tool-cache.js';
import { fetchDiscovery } from '../mcp/discovery.js';
import { createMcpClient } from '../mcp/client-factory.js';
import { logger } from '../logger.js';

export interface McpApiDeps {
  db: Database.Database;
  registry: McpRegistry;
  tokenManager: McpTokenManager;
  toolCache: McpToolCache;
  requireAdmin: import('express').RequestHandler;
  requireAuth: import('express').RequestHandler;
  getUserId: (req: Request) => string | null;
  insecureLocalTestMode?: boolean;
  allowPrivateAddresses?: boolean;
  /**
   * Spaces foundation. Optional repo accessor used to (a) authorize a
   * `space_id` supplied on register/list against the requesting viewer and
   * (b) resolve the space's existence. When omitted, space-scoped requests
   * are rejected (no way to authorize them), keeping global management intact.
   */
  getSpace?: (spaceId: string, viewer?: Express.User) => Promise<{ id: string } | null>;
  /**
   * Space-as-Principal P2: can this request's user EDIT space-owned resources in
   * the given space (editor/owner/admin)? Used to let space members share the
   * management of api_key (static-token) MCP servers — the credential is
   * master-encrypted (not user-tied), so it survives member turnover. OAuth
   * servers stay owner-gated (their tokens are per-user). When omitted, only the
   * server's owner can manage it.
   */
  canEditSpace?: (spaceId: string, req: Request) => Promise<boolean>;
}

const ID_REGEX = /^[a-z0-9_-]{1,64}$/;

/**
 * Normalize the space id from a register/list request body or query.
 * Accepts both `space_id` (snake, wire convention) and `spaceId` (camel).
 * Returns: string (a space), null (explicit global), or undefined (not given).
 */
function readSpaceId(src: Record<string, unknown> | undefined): string | null | undefined {
  if (!src) return undefined;
  const raw = (src.space_id ?? src.spaceId) as unknown;
  if (raw === undefined) return undefined;
  if (raw === null || raw === '') return null;
  // 文字列のみ受理（数値等の coerce を避ける。非文字列は未指定扱い）。
  return typeof raw === 'string' ? raw : undefined;
}

export function createAdminRouter(deps: McpApiDeps): Router {
  const router = Router();

  router.get('/', deps.requireAdmin, async (req, res) => {
    // Space-scoped listing: ?spaceId=<id> (or ?space_id=) returns only that
    // space's servers (visibility-checked). Omitting it returns all (global view).
    const spaceId = readSpaceId(req.query as Record<string, unknown>);
    if (typeof spaceId === 'string') {
      if (!deps.getSpace) {
        res.status(400).json({ error: 'space-scoped listing not available' });
        return;
      }
      const space = await deps.getSpace(spaceId, (req as Request & { user?: Express.User }).user);
      if (!space) {
        res.status(404).json({ error: 'unknown or inaccessible space' });
        return;
      }
      const scoped = deps.registry.listForSpace(spaceId).map((s) => ({
        ...s,
        toolCount: deps.toolCache.getForServer(s.id).length,
      }));
      res.json({ servers: scoped });
      return;
    }
    const servers = deps.registry.listPublic();
    const enriched = servers.map((s) => ({
      ...s,
      toolCount: deps.toolCache.getForServer(s.id).length,
    }));
    res.json({ servers: enriched });
  });

  router.post('/', deps.requireAdmin, async (req, res) => {
    const body = req.body as Partial<{
      id: string;
      name: string;
      url: string;
      authKind: string;
      oauthClientId: string;
      oauthClientSecret: string;
      oauthScopes: string;
      staticToken: string;
      authHeaderName: string;
      enabled: boolean;
      space_id: string | null;
      spaceId: string | null;
    }>;

    if (!body.id || !ID_REGEX.test(body.id)) {
      res.status(400).json({ error: 'id must match [a-z0-9_-]{1,64}' });
      return;
    }
    if (!body.name || !body.url) {
      res.status(400).json({ error: 'missing required fields' });
      return;
    }

    // Optional per-space scoping. When a space is supplied, authorize it
    // against the requesting admin/viewer via getSpace before storing.
    const spaceId = readSpaceId(body as Record<string, unknown>);
    if (typeof spaceId === 'string') {
      if (!deps.getSpace) {
        res.status(400).json({ error: 'space-scoped servers not available' });
        return;
      }
      const space = await deps.getSpace(spaceId, (req as Request & { user?: Express.User }).user);
      if (!space) {
        res.status(404).json({ error: 'unknown or inaccessible space' });
        return;
      }
    }

    // Editing a global server re-POSTs the same id. Preserve write-only secrets
    // when left blank, and keep the original authKind (the form locks it on edit).
    const existing = deps.registry.getDecrypted(body.id);
    const isEdit = existing !== null;
    const authKind = (isEdit ? existing!.authKind : (body.authKind ?? 'oauth')) as 'oauth' | 'api_key';
    const staticToken = body.staticToken || (isEdit ? existing!.staticToken ?? undefined : undefined);
    const oauthClientSecret = body.oauthClientSecret || (isEdit ? existing!.oauthClientSecret || undefined : undefined);
    const oauthClientId = body.oauthClientId || (isEdit ? existing!.oauthClientId || undefined : undefined);

    if (authKind === 'oauth') {
      if (!oauthClientId || !oauthClientSecret) {
        res.status(400).json({ error: 'authKind oauth requires oauthClientId and oauthClientSecret' });
        return;
      }
    } else if (authKind === 'api_key') {
      if (!staticToken) {
        res.status(400).json({ error: 'authKind api_key requires staticToken' });
        return;
      }
    } else {
      res.status(400).json({ error: `unknown authKind: ${authKind}` });
      return;
    }

    const adminId = deps.getUserId(req);
    // spaceId === undefined on edit → keep existing space; on create → null (global).
    const effectiveSpaceId = spaceId === undefined ? (isEdit ? existing!.spaceId ?? null : null) : spaceId;
    deps.registry.upsert({
      id: body.id,
      name: body.name,
      url: body.url,
      authKind,
      ownerId: null,
      spaceId: effectiveSpaceId,
      oauthClientId,
      oauthClientSecret,
      oauthScopes: body.oauthScopes ?? null,
      staticToken,
      authHeaderName: body.authHeaderName ?? null,
      enabled: body.enabled !== false,
      createdBy: adminId,
    });

    // Attempt discovery for OAuth only (best effort — defer on failure).
    if (authKind === 'oauth') {
      try {
        const meta = await fetchDiscovery(body.url, {
          insecureLocalTestMode: deps.insecureLocalTestMode,
        });
        deps.registry.setDiscovery(body.id, meta);
      } catch (err) {
        logger.warn(
          `[mcp:api] discovery deferred for server=${body.id}: ${(err as Error).message}`,
        );
      }
    }

    // Auto list_tools for api_key servers (token available immediately).
    // For OAuth servers, defer until callback handler.
    if (authKind === 'api_key' && body.staticToken) {
      try {
        const server = deps.registry.getDecrypted(body.id);
        if (server && server.staticToken) {
          const { client, close } = await createMcpClient(
            server,
            server.staticToken,
            {
              insecureLocalTestMode: deps.insecureLocalTestMode,
              allowPrivateAddresses: deps.allowPrivateAddresses,
              callTimeoutMs: 30_000,
            },
          );
          try {
            const list = (await client.listTools()) as {
              tools: Array<{ name: string; description?: string; inputSchema?: unknown }>;
            };
            deps.toolCache.replaceForServer(body.id, list.tools);
            logger.info(`[mcp:api] auto list_tools server=${body.id} count=${list.tools.length}`);
          } finally {
            await close();
          }
        }
      } catch (err) {
        logger.warn(
          `[mcp:api] auto list_tools failed (deferred to manual refresh) server=${body.id}: ${(err as Error).message}`,
        );
      }
    }

    res.json({ ok: true });
  });

  router.delete('/:id', deps.requireAdmin, (req, res) => {
    deps.registry.delete(req.params.id);
    res.json({ ok: true });
  });

  router.post('/:id/tools/refresh', deps.requireAdmin, async (req, res) => {
    const server = deps.registry.getDecrypted(req.params.id);
    if (!server) {
      res.status(404).json({ error: 'unknown server' });
      return;
    }
    // For api_key servers, use the static token; for oauth servers use 'anonymous'
    // (the OAuth token is per-user and not available at admin-refresh time).
    const accessToken = server.authKind === 'api_key' && server.staticToken
      ? server.staticToken
      : 'anonymous';
    try {
      const { client, close } = await createMcpClient(
        server,
        accessToken,
        {
          insecureLocalTestMode: deps.insecureLocalTestMode,
          callTimeoutMs: 30_000,
          allowPrivateAddresses: deps.allowPrivateAddresses,
        },
      );
      try {
        const list = (await client.listTools()) as {
          tools: Array<{ name: string; description?: string; inputSchema?: unknown }>;
        };
        deps.toolCache.replaceForServer(server.id, list.tools);
        res.json({ ok: true, count: list.tools.length });
      } finally {
        await close();
      }
    } catch (err) {
      logger.warn(
        `[mcp:api] list_tools failed server=${server.id}: ${(err as Error).message}`,
      );
      res.status(502).json({ error: 'list_tools failed', detail: (err as Error).message });
    }
  });

  return router;
}

export function createUserRouter(deps: McpApiDeps): Router {
  const router = Router();

  router.get('/', deps.requireAuth, (req, res) => {
    const userId = deps.getUserId(req);
    if (!userId) {
      res.status(401).json({ error: 'unauthenticated' });
      return;
    }
    const servers = deps.registry.listEnabledForUser(userId);
    const out = servers.map((s) => ({
      serverId: s.id,
      serverName: s.name,
      connected: deps.tokenManager.hasToken(userId, s.id),
      authKind: s.authKind,
      ownerId: s.ownerId,
    }));
    res.json({ connections: out });
  });

  router.delete('/:id', deps.requireAuth, (req, res) => {
    const userId = deps.getUserId(req);
    if (!userId) {
      res.status(401).json({ error: 'unauthenticated' });
      return;
    }
    const server = deps.registry.getDecrypted(req.params.id);
    if (!server) {
      res.status(404).json({ error: 'unknown server' });
      return;
    }
    // For api_key global servers, DELETE has no meaningful effect
    if (server.authKind === 'api_key' && server.ownerId === null) {
      res.status(400).json({ error: 'api_key servers do not use per-user tokens; disconnect is not applicable' });
      return;
    }
    // User-owned servers should be managed via /api/mcp/user-servers, not this route
    if (server.ownerId !== null) {
      res.status(400).json({ error: 'use /api/mcp/user-servers/:id to delete a user-owned server' });
      return;
    }
    deps.tokenManager.deleteToken(userId, req.params.id);
    res.json({ ok: true });
  });

  return router;
}

export function createUserServersRouter(deps: McpApiDeps): Router {
  const router = Router();

  // Can `userId` (this request) manage (edit/delete/refresh) the given server?
  // The owner always can. Additionally, a space-owned api_key server is managed
  // by any space member with edit rights (Space-as-Principal P2) — its static
  // token is master-encrypted, not tied to the creator. OAuth servers stay
  // owner-only (per-user tokens).
  async function canManageServer(
    server: { ownerId: string | null; spaceId: string | null; authKind: string },
    userId: string,
    req: Request,
  ): Promise<boolean> {
    if (server.ownerId === userId) return true;
    if (server.spaceId && server.authKind === 'api_key' && deps.canEditSpace) {
      return deps.canEditSpace(server.spaceId, req);
    }
    return false;
  }

  router.get('/', deps.requireAuth, async (req, res) => {
    const userId = deps.getUserId(req);
    if (!userId) {
      res.status(401).json({ error: 'unauthenticated' });
      return;
    }
    // Space-scoped listing: ?spaceId=<id> returns that space's servers owned
    // by the requesting user (visibility-checked).
    const spaceId = readSpaceId(req.query as Record<string, unknown>);
    if (typeof spaceId === 'string') {
      if (!deps.getSpace) {
        res.status(400).json({ error: 'space-scoped listing not available' });
        return;
      }
      const space = await deps.getSpace(spaceId, (req as Request & { user?: Express.User }).user);
      if (!space) {
        res.status(404).json({ error: 'unknown or inaccessible space' });
        return;
      }
      // Space-as-Principal P2: api_key servers are space-shared (every member who
      // can see the space — getSpace already gated that above — sees them). OAuth
      // servers remain private to their owner (per-user tokens).
      const scoped = deps.registry
        .listForSpace(spaceId)
        .filter((s) => s.authKind === 'api_key' || s.ownerId === userId)
        .map((s) => ({ ...s, toolCount: deps.toolCache.getForServer(s.id).length }));
      res.json({ servers: scoped });
      return;
    }
    const servers = deps.registry.listEnabledForOwner(userId);
    const enriched = servers.map((s) => ({
      ...s,
      toolCount: deps.toolCache.getForServer(s.id).length,
    }));
    res.json({ servers: enriched });
  });

  router.post('/', deps.requireAuth, async (req, res) => {
    const userId = deps.getUserId(req);
    if (!userId) {
      res.status(401).json({ error: 'unauthenticated' });
      return;
    }

    const body = req.body as Partial<{
      id: string;
      name: string;
      url: string;
      authKind: string;
      oauthClientId: string;
      oauthClientSecret: string;
      oauthScopes: string;
      staticToken: string;
      authHeaderName: string;
      enabled: boolean;
      space_id: string | null;
      spaceId: string | null;
    }>;

    if (!body.id || !ID_REGEX.test(body.id)) {
      res.status(400).json({ error: 'id must match [a-z0-9_-]{1,64}' });
      return;
    }
    if (!body.name || !body.url) {
      res.status(400).json({ error: 'missing required fields: name, url' });
      return;
    }

    // Optional per-space scoping. Authorize the space against the viewer.
    const spaceId = readSpaceId(body as Record<string, unknown>);
    if (typeof spaceId === 'string') {
      if (!deps.getSpace) {
        res.status(400).json({ error: 'space-scoped servers not available' });
        return;
      }
      const space = await deps.getSpace(spaceId, (req as Request & { user?: Express.User }).user);
      if (!space) {
        res.status(404).json({ error: 'unknown or inaccessible space' });
        return;
      }
    }

    // Editing re-POSTs the same id. Collide only with a server you cannot manage
    // (a global or another user's server); your own — or a space api_key server
    // you may edit (Space-as-Principal P2) — is an update.
    const existing = deps.registry.getDecrypted(body.id);
    if (existing && !(await canManageServer(existing, userId, req))) {
      res.status(409).json({ error: `server id '${body.id}' already exists` });
      return;
    }
    const isEdit = existing !== null;
    // authKind cannot change on edit (the form disables the selector).
    const authKind = (isEdit ? existing!.authKind : (body.authKind ?? 'oauth')) as 'oauth' | 'api_key';
    // Secrets are write-only: a blank value on edit means "keep the existing one".
    const staticToken = body.staticToken || (isEdit ? existing!.staticToken ?? undefined : undefined);
    const oauthClientSecret = body.oauthClientSecret || (isEdit ? existing!.oauthClientSecret || undefined : undefined);
    const oauthClientId = body.oauthClientId || (isEdit ? existing!.oauthClientId || undefined : undefined);

    if (authKind === 'api_key') {
      if (!staticToken) {
        res.status(400).json({ error: 'authKind api_key requires staticToken' });
        return;
      }
    } else if (authKind === 'oauth') {
      if (!oauthClientId || !oauthClientSecret) {
        res.status(400).json({ error: 'authKind oauth requires oauthClientId and oauthClientSecret' });
        return;
      }
    } else {
      res.status(400).json({ error: `unknown authKind: ${authKind}` });
      return;
    }

    // Adding a NEW api_key server into a space makes it a shared space credential,
    // so it requires space EDIT rights — mere visibility (getSpace above) is not
    // enough (else a read-only/public-space viewer could plant a shared server).
    // Edits of an existing server are already gated by canManageServer at the
    // collision check above. OAuth servers stay owner-private (no extra gate).
    if (!isEdit && typeof spaceId === 'string' && authKind === 'api_key') {
      if (!deps.canEditSpace || !(await deps.canEditSpace(spaceId, req))) {
        res.status(403).json({ error: 'forbidden: space edit rights required to add a shared server' });
        return;
      }
    }

    // spaceId === undefined on edit → keep existing space; on create → null (global).
    const effectiveSpaceId = spaceId === undefined ? (isEdit ? existing!.spaceId ?? null : null) : spaceId;
    deps.registry.upsert({
      id: body.id,
      name: body.name,
      url: body.url,
      authKind,
      // Preserve the original owner (created_by) on edit so a space member editing
      // a shared api_key server doesn't reassign ownership to themselves.
      ownerId: isEdit ? (existing!.ownerId ?? userId) : userId,
      spaceId: effectiveSpaceId,
      oauthClientId,
      oauthClientSecret,
      oauthScopes: body.oauthScopes ?? null,
      staticToken,
      authHeaderName: body.authHeaderName ?? null,
      enabled: body.enabled !== false,
      createdBy: userId,
    });

    // Auto list_tools for api_key servers (token available immediately).
    // For OAuth servers, defer until callback handler.
    if (authKind === 'api_key' && body.staticToken) {
      try {
        const server = deps.registry.getDecrypted(body.id);
        if (server && server.staticToken) {
          const { client, close } = await createMcpClient(
            server,
            server.staticToken,
            {
              insecureLocalTestMode: deps.insecureLocalTestMode,
              allowPrivateAddresses: deps.allowPrivateAddresses,
              callTimeoutMs: 30_000,
            },
          );
          try {
            const list = (await client.listTools()) as {
              tools: Array<{ name: string; description?: string; inputSchema?: unknown }>;
            };
            deps.toolCache.replaceForServer(body.id, list.tools);
            logger.info(
              `[mcp:api] auto list_tools server=${body.id} count=${list.tools.length}`,
            );
          } finally {
            await close();
          }
        }
      } catch (err) {
        logger.warn(
          `[mcp:api] auto list_tools failed (deferred to manual refresh) server=${body.id}: ${(err as Error).message}`,
        );
      }
    }

    res.json({ ok: true });
  });

  router.delete('/:id', deps.requireAuth, async (req, res) => {
    const userId = deps.getUserId(req);
    if (!userId) {
      res.status(401).json({ error: 'unauthenticated' });
      return;
    }
    const server = deps.registry.getDecrypted(req.params.id);
    if (!server) {
      res.status(404).json({ error: 'unknown server' });
      return;
    }
    if (!(await canManageServer(server, userId, req))) {
      res.status(403).json({ error: 'forbidden: you do not own this server' });
      return;
    }
    deps.registry.delete(req.params.id);
    res.json({ ok: true });
  });

  router.post('/:id/tools/refresh', deps.requireAuth, async (req, res) => {
    const userId = deps.getUserId(req);
    if (!userId) {
      res.status(401).json({ error: 'unauthenticated' });
      return;
    }
    const server = deps.registry.getDecrypted(req.params.id);
    if (!server) {
      res.status(404).json({ error: 'unknown server' });
      return;
    }
    if (!(await canManageServer(server, userId, req))) {
      res.status(403).json({ error: 'forbidden: you do not own this server' });
      return;
    }
    const accessToken = server.authKind === 'api_key' && server.staticToken
      ? server.staticToken
      : 'anonymous';
    try {
      const { client, close } = await createMcpClient(
        server,
        accessToken,
        {
          insecureLocalTestMode: deps.insecureLocalTestMode,
          callTimeoutMs: 30_000,
          allowPrivateAddresses: deps.allowPrivateAddresses,
        },
      );
      try {
        const list = (await client.listTools()) as {
          tools: Array<{ name: string; description?: string; inputSchema?: unknown }>;
        };
        deps.toolCache.replaceForServer(server.id, list.tools);
        res.json({ ok: true, count: list.tools.length });
      } finally {
        await close();
      }
    } catch (err) {
      logger.warn(
        `[mcp:api] user list_tools failed server=${server.id}: ${(err as Error).message}`,
      );
      res.status(502).json({ error: 'list_tools failed', detail: (err as Error).message });
    }
  });

  return router;
}
