import { Router, type RequestHandler } from 'express';
import { randomBytes, createHash } from 'node:crypto';
import type Database from 'better-sqlite3';
import type { McpRegistry } from './registry.js';
import type { McpTokenManager } from './token-manager.js';
import { fetchDiscovery, exchangeCode } from './discovery.js';
import { isKeyConfigured } from './crypto.js';
import { redactSecrets } from './redact.js';
import { logger } from '../logger.js';

function base64url(buf: Buffer): string {
  return buf.toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
}

function pkcePair(): { verifier: string; challenge: string } {
  const verifier = base64url(randomBytes(32));
  const challenge = base64url(createHash('sha256').update(verifier).digest());
  return { verifier, challenge };
}

export interface OauthRouterDeps {
  db: Database.Database;
  registry: McpRegistry;
  tokenManager: McpTokenManager;
  pendingTtlMinutes: number;
  getCallbackBaseUrl: () => string;
  getAuthenticatedUserId: (req: import('express').Request) => string | null;
  resumeWaitingJobs: (userId: string, serverId: string) => void;
  insecureLocalTestMode?: boolean;
  /** Best-effort list_tools call after OAuth token is saved; non-fatal on failure. */
  listToolsAfterAuth?: (serverId: string, accessToken: string) => Promise<void>;
}

export function createMcpOauthRouter(deps: OauthRouterDeps): Router {
  const router = Router();

  const requireAuthUser: RequestHandler = (req, res, next) => {
    const uid = deps.getAuthenticatedUserId(req);
    if (!uid) {
      res.status(401).json({ error: 'Authentication required' });
      return;
    }
    (req as unknown as { mcpUserId: string }).mcpUserId = uid;
    next();
  };

  router.get('/:serverId/start', requireAuthUser, async (req, res) => {
    if (!isKeyConfigured()) {
      res.status(503).json({ error: 'MCP_ENCRYPTION_KEY not configured' });
      return;
    }
    const { serverId } = req.params;
    const userId = (req as unknown as { mcpUserId: string }).mcpUserId;
    const server = deps.registry.getDecrypted(serverId);
    if (!server || !server.enabled) {
      res.status(404).json({ error: 'Unknown MCP server' });
      return;
    }
    if (server.authKind !== 'oauth') {
      res.status(400).json({ error: 'this server does not use OAuth — no authorization needed' });
      return;
    }

    let authEndpoint = server.authorizationEndpoint;
    let tokenEndpoint = server.tokenEndpoint;
    if (!authEndpoint || !tokenEndpoint) {
      try {
        const meta = await fetchDiscovery(server.url, { insecureLocalTestMode: deps.insecureLocalTestMode });
        deps.registry.setDiscovery(serverId, meta);
        authEndpoint = meta.authorizationEndpoint;
        tokenEndpoint = meta.tokenEndpoint;
      } catch (err) {
        logger.error(`[mcp:oauth] discovery failed server=${serverId}: ${(err as Error).message}`);
        res.status(502).json({ error: 'Discovery failed' });
        return;
      }
    }

    const { verifier, challenge } = pkcePair();
    const stateNonce = base64url(randomBytes(24));

    // Clean expired pending rows
    deps.db.prepare(
      `DELETE FROM mcp_oauth_pending
       WHERE datetime(created_at, '+' || ? || ' minutes') < datetime('now')`,
    ).run(String(deps.pendingTtlMinutes));

    deps.db.prepare(
      `INSERT INTO mcp_oauth_pending (state, user_id, server_id, code_verifier) VALUES (?, ?, ?, ?)`,
    ).run(stateNonce, userId, serverId, verifier);

    const redirectUri = `${deps.getCallbackBaseUrl()}/auth/mcp/${encodeURIComponent(serverId)}/callback`;
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: server.oauthClientId,
      redirect_uri: redirectUri,
      state: stateNonce,
      code_challenge: challenge,
      code_challenge_method: 'S256',
    });
    if (server.oauthScopes) params.set('scope', server.oauthScopes);
    res.redirect(`${authEndpoint}?${params.toString()}`);
  });

  router.get('/:serverId/callback', requireAuthUser, async (req, res) => {
    const { serverId } = req.params;
    const userId = (req as unknown as { mcpUserId: string }).mcpUserId;
    const code = typeof req.query.code === 'string' ? req.query.code : '';
    const state = typeof req.query.state === 'string' ? req.query.state : '';
    if (!code || !state) {
      res.status(400).send('Missing code/state');
      return;
    }

    // single-use state
    const pending = deps.db.prepare(
      `DELETE FROM mcp_oauth_pending WHERE state = ? RETURNING user_id, server_id, code_verifier`,
    ).get(state) as { user_id: string; server_id: string; code_verifier: string } | undefined;
    if (!pending) {
      res.status(400).send('Invalid or expired state');
      return;
    }
    if (pending.user_id !== userId) {
      res.status(403).send('State/user mismatch');
      return;
    }
    if (pending.server_id !== serverId) {
      res.status(400).send('State/server mismatch');
      return;
    }

    const server = deps.registry.getDecrypted(serverId);
    if (!server || !server.tokenEndpoint) {
      res.status(500).send('Server token endpoint missing');
      return;
    }

    const redirectUri = `${deps.getCallbackBaseUrl()}/auth/mcp/${encodeURIComponent(serverId)}/callback`;
    try {
      const resp = await exchangeCode(
        {
          tokenEndpoint: server.tokenEndpoint,
          clientId: server.oauthClientId,
          clientSecret: server.oauthClientSecret,
          code,
          redirectUri,
          codeVerifier: pending.code_verifier,
        },
        { insecureLocalTestMode: deps.insecureLocalTestMode },
      );
      if (server.issuer && resp.iss && resp.iss !== server.issuer) {
        res.status(400).send('Issuer mismatch');
        return;
      }
      const expiresAt = resp.expires_in
        ? new Date(Date.now() + resp.expires_in * 1000).toISOString()
        : null;
      deps.tokenManager.saveTokens({
        userId,
        serverId,
        accessToken: resp.access_token,
        refreshToken: resp.refresh_token ?? null,
        expiresAt,
        scope: resp.scope ?? null,
      });
      // Auto list_tools so the LLM sees tools immediately (best-effort)
      if (deps.listToolsAfterAuth) {
        await deps.listToolsAfterAuth(serverId, resp.access_token).catch((err: unknown) => {
          logger.warn(`[mcp:oauth] auto list_tools failed server=${serverId}: ${err}`);
        });
      }
      deps.resumeWaitingJobs(userId, serverId);
      logger.info(
        `[mcp:oauth] connected user=${userId} server=${serverId} ${JSON.stringify(redactSecrets({ scope: resp.scope }))}`,
      );
      res.redirect('/settings#mcp');
    } catch (err) {
      logger.error(`[mcp:oauth] token exchange failed: ${(err as Error).message}`);
      res.status(502).send('Token exchange failed');
    }
  });

  return router;
}
