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;
}

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

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

  router.get('/', deps.requireAdmin, (_req, res) => {
    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;
    }>;

    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;
    }

    // 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);
    deps.registry.upsert({
      id: body.id,
      name: body.name,
      url: body.url,
      authKind,
      ownerId: null,
      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();

  router.get('/', deps.requireAuth, (req, res) => {
    const userId = deps.getUserId(req);
    if (!userId) {
      res.status(401).json({ error: 'unauthenticated' });
      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;
    }>;

    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;
    }

    // Editing re-POSTs the same id. Collide only with a server you do NOT own
    // (a global or another user's server); your own server is an update.
    const existing = deps.registry.getDecrypted(body.id);
    if (existing && existing.ownerId !== userId) {
      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;
    }

    deps.registry.upsert({
      id: body.id,
      name: body.name,
      url: body.url,
      authKind,
      ownerId: userId,
      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, (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 (server.ownerId !== userId) {
      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 (server.ownerId !== userId) {
      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;
}
