import { randomBytes } from 'crypto';
import { logger } from '@librechat/data-schemas';
import { FetchLike } from '@modelcontextprotocol/sdk/shared/transport';
import { OAuthMetadataSchema } from '@modelcontextprotocol/sdk/shared/auth.js';
import {
  registerClient,
  startAuthorization,
  exchangeAuthorization,
  discoverAuthorizationServerMetadata,
  discoverOAuthProtectedResourceMetadata,
} from '@modelcontextprotocol/sdk/client/auth.js';
import { TokenExchangeMethodEnum, type MCPOptions } from 'librechat-data-provider';
import type { FlowStateManager } from '~/flow/manager';
import type {
  OAuthClientInformation,
  OAuthProtectedResourceMetadata,
  MCPOAuthFlowMetadata,
  MCPOAuthTokens,
  OAuthMetadata,
} from './types';
import { sanitizeUrlForLogging } from '~/mcp/utils';

/** Type for the OAuth metadata from the SDK */
type SDKOAuthMetadata = Parameters<typeof registerClient>[1]['metadata'];

export class MCPOAuthHandler {
  private static readonly FLOW_TYPE = 'mcp_oauth';
  private static readonly FLOW_TTL = 10 * 60 * 1000; // 10 minutes

  private static getForcedTokenEndpointAuthMethod(
    tokenExchangeMethod?: TokenExchangeMethodEnum,
  ): 'client_secret_basic' | 'client_secret_post' | undefined {
    if (tokenExchangeMethod === TokenExchangeMethodEnum.DefaultPost) {
      return 'client_secret_post';
    }
    if (tokenExchangeMethod === TokenExchangeMethodEnum.BasicAuthHeader) {
      return 'client_secret_basic';
    }
    return undefined;
  }

  private static resolveTokenEndpointAuthMethod(options: {
    tokenExchangeMethod?: TokenExchangeMethodEnum;
    tokenAuthMethods: string[];
    preferredMethod?: string;
  }): 'client_secret_basic' | 'client_secret_post' | undefined {
    const forcedMethod = this.getForcedTokenEndpointAuthMethod(options.tokenExchangeMethod);
    const preferredMethod = forcedMethod ?? options.preferredMethod;

    if (preferredMethod === 'client_secret_basic' || preferredMethod === 'client_secret_post') {
      return preferredMethod;
    }

    if (options.tokenAuthMethods.includes('client_secret_basic')) {
      return 'client_secret_basic';
    }
    if (options.tokenAuthMethods.includes('client_secret_post')) {
      return 'client_secret_post';
    }
    return undefined;
  }

  /**
   * Creates a fetch function with custom headers injected
   */
  private static createOAuthFetch(
    headers: Record<string, string>,
    clientInfo?: OAuthClientInformation,
  ): FetchLike {
    return async (url: string | URL, init?: RequestInit): Promise<Response> => {
      const newHeaders = new Headers(init?.headers ?? {});
      for (const [key, value] of Object.entries(headers)) {
        newHeaders.set(key, value);
      }

      const method = (init?.method ?? 'GET').toUpperCase();
      const initBody = init?.body;
      let params: URLSearchParams | undefined;

      if (initBody instanceof URLSearchParams) {
        params = initBody;
      } else if (typeof initBody === 'string') {
        const parsed = new URLSearchParams(initBody);
        if (parsed.has('grant_type')) {
          params = parsed;
        }
      }

      /**
       * FastMCP 2.14+/MCP SDK 1.24+ token endpoints can be strict about:
       * - Content-Type (must be application/x-www-form-urlencoded)
       * - where client_id/client_secret are supplied (default_post vs basic header)
       */
      if (method === 'POST' && params?.has('grant_type')) {
        newHeaders.set('Content-Type', 'application/x-www-form-urlencoded');

        if (clientInfo?.client_id) {
          let authMethod = clientInfo.token_endpoint_auth_method;

          if (!authMethod) {
            if (newHeaders.has('Authorization')) {
              authMethod = 'client_secret_basic';
            } else if (params.has('client_id') || params.has('client_secret')) {
              authMethod = 'client_secret_post';
            } else if (clientInfo.client_secret) {
              authMethod = 'client_secret_post';
            } else {
              authMethod = 'none';
            }
          }

          if (!clientInfo.client_secret || authMethod === 'none') {
            newHeaders.delete('Authorization');
            if (!params.has('client_id')) {
              params.set('client_id', clientInfo.client_id);
            }
          } else if (authMethod === 'client_secret_post') {
            newHeaders.delete('Authorization');
            if (!params.has('client_id')) {
              params.set('client_id', clientInfo.client_id);
            }
            if (!params.has('client_secret')) {
              params.set('client_secret', clientInfo.client_secret);
            }
          } else if (authMethod === 'client_secret_basic') {
            if (!newHeaders.has('Authorization')) {
              const clientAuth = Buffer.from(
                `${clientInfo.client_id}:${clientInfo.client_secret}`,
              ).toString('base64');
              newHeaders.set('Authorization', `Basic ${clientAuth}`);
            }
          }
        }

        return fetch(url, {
          ...init,
          body: params.toString(),
          headers: newHeaders,
        });
      }
      return fetch(url, {
        ...init,
        headers: newHeaders,
      });
    };
  }

  /**
   * Discovers OAuth metadata from the server
   */
  private static async discoverMetadata(
    serverUrl: string,
    oauthHeaders: Record<string, string>,
  ): Promise<{
    metadata: OAuthMetadata;
    resourceMetadata?: OAuthProtectedResourceMetadata;
    authServerUrl: URL;
  }> {
    logger.debug(
      `[MCPOAuth] discoverMetadata called with serverUrl: ${sanitizeUrlForLogging(serverUrl)}`,
    );

    let authServerUrl = new URL(serverUrl);
    let resourceMetadata: OAuthProtectedResourceMetadata | undefined;

    const fetchFn = this.createOAuthFetch(oauthHeaders);

    try {
      // Try to discover resource metadata first
      logger.debug(
        `[MCPOAuth] Attempting to discover protected resource metadata from ${serverUrl}`,
      );
      resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, {}, fetchFn);

      if (resourceMetadata?.authorization_servers?.length) {
        authServerUrl = new URL(resourceMetadata.authorization_servers[0]);
        logger.debug(
          `[MCPOAuth] Found authorization server from resource metadata: ${authServerUrl}`,
        );
      } else {
        logger.debug(`[MCPOAuth] No authorization servers found in resource metadata`);
      }
    } catch (error) {
      logger.debug('[MCPOAuth] Resource metadata discovery failed, continuing with server URL', {
        error,
      });
    }

    // Discover OAuth metadata
    logger.debug(
      `[MCPOAuth] Discovering OAuth metadata from ${sanitizeUrlForLogging(authServerUrl)}`,
    );
    let rawMetadata = await discoverAuthorizationServerMetadata(authServerUrl, {
      fetchFn,
    });

    // If discovery failed and we're using a path-based URL, try the base URL
    if (!rawMetadata && authServerUrl.pathname !== '/') {
      const baseUrl = new URL(authServerUrl.origin);
      logger.debug(
        `[MCPOAuth] Discovery failed with path, trying base URL: ${sanitizeUrlForLogging(baseUrl)}`,
      );
      rawMetadata = await discoverAuthorizationServerMetadata(baseUrl, {
        fetchFn,
      });
    }

    if (!rawMetadata) {
      /**
       * No metadata discovered - create fallback metadata using default OAuth endpoint paths.
       * This mirrors the MCP SDK's behavior where it falls back to /authorize, /token, /register
       * when metadata discovery fails (e.g., servers without .well-known endpoints).
       * See: https://github.com/modelcontextprotocol/sdk/blob/main/src/client/auth.ts
       */
      logger.warn(
        `[MCPOAuth] No OAuth metadata discovered from ${sanitizeUrlForLogging(authServerUrl)}, using legacy fallback endpoints`,
      );

      const fallbackMetadata: OAuthMetadata = {
        issuer: authServerUrl.toString(),
        authorization_endpoint: new URL('/authorize', authServerUrl).toString(),
        token_endpoint: new URL('/token', authServerUrl).toString(),
        registration_endpoint: new URL('/register', authServerUrl).toString(),
        response_types_supported: ['code'],
        grant_types_supported: ['authorization_code', 'refresh_token'],
        code_challenge_methods_supported: ['S256', 'plain'],
        token_endpoint_auth_methods_supported: [
          'client_secret_basic',
          'client_secret_post',
          'none',
        ],
      };

      logger.debug(`[MCPOAuth] Using fallback metadata:`, fallbackMetadata);
      return {
        metadata: fallbackMetadata,
        resourceMetadata,
        authServerUrl,
      };
    }

    logger.debug(`[MCPOAuth] OAuth metadata discovered successfully`);
    const metadata = await OAuthMetadataSchema.parseAsync(rawMetadata);

    logger.debug(`[MCPOAuth] OAuth metadata parsed successfully`);
    return {
      metadata: metadata as unknown as OAuthMetadata,
      resourceMetadata,
      authServerUrl,
    };
  }

  /**
   * Registers an OAuth client dynamically
   */
  private static async registerOAuthClient(
    serverUrl: string,
    metadata: OAuthMetadata,
    oauthHeaders: Record<string, string>,
    resourceMetadata?: OAuthProtectedResourceMetadata,
    redirectUri?: string,
    tokenExchangeMethod?: TokenExchangeMethodEnum,
  ): Promise<OAuthClientInformation> {
    logger.debug(
      `[MCPOAuth] Starting client registration for ${sanitizeUrlForLogging(serverUrl)}, server metadata:`,
      {
        grant_types_supported: metadata.grant_types_supported,
        response_types_supported: metadata.response_types_supported,
        token_endpoint_auth_methods_supported: metadata.token_endpoint_auth_methods_supported,
        scopes_supported: metadata.scopes_supported,
      },
    );

    /** Client metadata based on what the server supports */
    const clientMetadata = {
      client_name: 'QognyX MCP Client',
      redirect_uris: [redirectUri || this.getDefaultRedirectUri()],
      grant_types: ['authorization_code'] as string[],
      response_types: ['code'] as string[],
      token_endpoint_auth_method: 'client_secret_basic',
      scope: undefined as string | undefined,
      logo_uri: undefined as string | undefined,
      tos_uri: undefined as string | undefined,
    };

    const supportedGrantTypes = metadata.grant_types_supported || ['authorization_code'];
    const requestedGrantTypes = ['authorization_code'];

    if (supportedGrantTypes.includes('refresh_token')) {
      requestedGrantTypes.push('refresh_token');
      logger.debug(
        `[MCPOAuth] Server ${serverUrl} supports \`refresh_token\` grant type, adding to request`,
      );
    } else {
      logger.debug(
        `[MCPOAuth] Server ${sanitizeUrlForLogging(serverUrl)} does not support \`refresh_token\` grant type`,
      );
    }
    clientMetadata.grant_types = requestedGrantTypes;

    clientMetadata.response_types = metadata.response_types_supported || ['code'];

    const forcedAuthMethod = this.getForcedTokenEndpointAuthMethod(tokenExchangeMethod);

    if (forcedAuthMethod) {
      clientMetadata.token_endpoint_auth_method = forcedAuthMethod;
    } else if (metadata.token_endpoint_auth_methods_supported) {
      // Prefer client_secret_basic if supported, otherwise use the first supported method
      if (metadata.token_endpoint_auth_methods_supported.includes('client_secret_basic')) {
        clientMetadata.token_endpoint_auth_method = 'client_secret_basic';
      } else if (metadata.token_endpoint_auth_methods_supported.includes('client_secret_post')) {
        clientMetadata.token_endpoint_auth_method = 'client_secret_post';
      } else if (metadata.token_endpoint_auth_methods_supported.includes('none')) {
        clientMetadata.token_endpoint_auth_method = 'none';
      } else {
        clientMetadata.token_endpoint_auth_method =
          metadata.token_endpoint_auth_methods_supported[0];
      }
    }

    const availableScopes = resourceMetadata?.scopes_supported || metadata.scopes_supported;
    if (availableScopes) {
      clientMetadata.scope = availableScopes.join(' ');
    }

    logger.debug(
      `[MCPOAuth] Registering client for ${sanitizeUrlForLogging(serverUrl)} with metadata:`,
      clientMetadata,
    );

    const clientInfo = await registerClient(serverUrl, {
      metadata: metadata as unknown as SDKOAuthMetadata,
      clientMetadata,
      fetchFn: this.createOAuthFetch(oauthHeaders),
    });

    if (forcedAuthMethod) {
      clientInfo.token_endpoint_auth_method = forcedAuthMethod;
    } else if (!clientInfo.token_endpoint_auth_method) {
      clientInfo.token_endpoint_auth_method = clientMetadata.token_endpoint_auth_method;
    }

    logger.debug(
      `[MCPOAuth] Client registered successfully for ${sanitizeUrlForLogging(serverUrl)}:`,
      {
        client_id: clientInfo.client_id,
        has_client_secret: !!clientInfo.client_secret,
        grant_types: clientInfo.grant_types,
        scope: clientInfo.scope,
      },
    );

    return clientInfo;
  }

  /**
   * Initiates the OAuth flow for an MCP server
   */
  static async initiateOAuthFlow(
    serverName: string,
    serverUrl: string,
    userId: string,
    oauthHeaders: Record<string, string>,
    config?: MCPOptions['oauth'],
  ): Promise<{ authorizationUrl: string; flowId: string; flowMetadata: MCPOAuthFlowMetadata }> {
    logger.debug(
      `[MCPOAuth] initiateOAuthFlow called for ${serverName} with URL: ${sanitizeUrlForLogging(serverUrl)}`,
    );

    const flowId = this.generateFlowId(userId, serverName);
    const state = this.generateState();

    logger.debug(`[MCPOAuth] Generated flowId: ${flowId}, state: ${state}`);

    try {
      // Check if we have pre-configured OAuth settings
      if (config?.authorization_url && config?.token_url && config?.client_id) {
        logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for ${serverName}`);

        const skipCodeChallengeCheck =
          config?.skip_code_challenge_check === true ||
          process.env.MCP_SKIP_CODE_CHALLENGE_CHECK === 'true';
        let codeChallengeMethodsSupported: string[];

        if (config?.code_challenge_methods_supported !== undefined) {
          codeChallengeMethodsSupported = config.code_challenge_methods_supported;
        } else if (skipCodeChallengeCheck) {
          codeChallengeMethodsSupported = ['S256', 'plain'];
          logger.debug(
            `[MCPOAuth] Code challenge check skip enabled, forcing S256 support for ${serverName}`,
          );
        } else {
          codeChallengeMethodsSupported = ['S256', 'plain'];
        }

        /** Metadata based on pre-configured settings */
        let tokenEndpointAuthMethod: string;
        if (!config.client_secret) {
          tokenEndpointAuthMethod = 'none';
        } else {
          // When token_exchange_method is undefined or not DefaultPost, default to using
          // client_secret_basic (Basic Auth header) for token endpoint authentication.
          tokenEndpointAuthMethod =
            this.getForcedTokenEndpointAuthMethod(config.token_exchange_method) ??
            'client_secret_basic';
        }

        let defaultTokenAuthMethods: string[];
        if (tokenEndpointAuthMethod === 'none') {
          defaultTokenAuthMethods = ['none'];
        } else if (tokenEndpointAuthMethod === 'client_secret_post') {
          defaultTokenAuthMethods = ['client_secret_post', 'client_secret_basic'];
        } else {
          defaultTokenAuthMethods = ['client_secret_basic', 'client_secret_post'];
        }

        const metadata: OAuthMetadata = {
          authorization_endpoint: config.authorization_url,
          token_endpoint: config.token_url,
          issuer: serverUrl,
          scopes_supported: config.scope?.split(' ') ?? [],
          grant_types_supported: config?.grant_types_supported ?? [
            'authorization_code',
            'refresh_token',
          ],
          token_endpoint_auth_methods_supported:
            config?.token_endpoint_auth_methods_supported ?? defaultTokenAuthMethods,
          response_types_supported: config?.response_types_supported ?? ['code'],
          code_challenge_methods_supported: codeChallengeMethodsSupported,
        };
        logger.debug(`[MCPOAuth] metadata for "${serverName}": ${JSON.stringify(metadata)}`);
        const clientInfo: OAuthClientInformation = {
          client_id: config.client_id,
          client_secret: config.client_secret,
          redirect_uris: [config.redirect_uri || this.getDefaultRedirectUri(serverName)],
          scope: config.scope,
          token_endpoint_auth_method: tokenEndpointAuthMethod,
        };

        logger.debug(`[MCPOAuth] Starting authorization with pre-configured settings`);
        const { authorizationUrl, codeVerifier } = await startAuthorization(serverUrl, {
          metadata: metadata as unknown as SDKOAuthMetadata,
          clientInformation: clientInfo,
          redirectUrl: clientInfo.redirect_uris?.[0] || this.getDefaultRedirectUri(serverName),
          scope: config.scope,
        });

        /** Add state parameter with flowId to the authorization URL */
        authorizationUrl.searchParams.set('state', flowId);
        logger.debug(`[MCPOAuth] Added state parameter to authorization URL`);

        const flowMetadata: MCPOAuthFlowMetadata = {
          serverName,
          userId,
          serverUrl,
          state,
          codeVerifier,
          clientInfo,
          metadata,
        };

        logger.debug(
          `[MCPOAuth] Authorization URL generated: ${sanitizeUrlForLogging(authorizationUrl.toString())}`,
        );
        return {
          authorizationUrl: authorizationUrl.toString(),
          flowId,
          flowMetadata,
        };
      }

      logger.debug(
        `[MCPOAuth] Starting auto-discovery of OAuth metadata from ${sanitizeUrlForLogging(serverUrl)}`,
      );
      const { metadata, resourceMetadata, authServerUrl } = await this.discoverMetadata(
        serverUrl,
        oauthHeaders,
      );

      logger.debug(
        `[MCPOAuth] OAuth metadata discovered, auth server URL: ${sanitizeUrlForLogging(authServerUrl)}`,
      );

      /** Dynamic client registration based on the discovered metadata */
      const redirectUri = config?.redirect_uri || this.getDefaultRedirectUri(serverName);
      logger.debug(`[MCPOAuth] Registering OAuth client with redirect URI: ${redirectUri}`);

      const clientInfo = await this.registerOAuthClient(
        authServerUrl.toString(),
        metadata,
        oauthHeaders,
        resourceMetadata,
        redirectUri,
        config?.token_exchange_method,
      );

      logger.debug(`[MCPOAuth] Client registered with ID: ${clientInfo.client_id}`);

      /** Authorization Scope */
      const scope =
        config?.scope ||
        resourceMetadata?.scopes_supported?.join(' ') ||
        metadata.scopes_supported?.join(' ');

      logger.debug(`[MCPOAuth] Starting authorization with scope: ${scope}`);

      let authorizationUrl: URL;
      let codeVerifier: string;

      try {
        logger.debug(`[MCPOAuth] Calling startAuthorization...`);
        const authResult = await startAuthorization(serverUrl, {
          metadata: metadata as unknown as SDKOAuthMetadata,
          clientInformation: clientInfo,
          redirectUrl: redirectUri,
          scope,
        });

        authorizationUrl = authResult.authorizationUrl;
        codeVerifier = authResult.codeVerifier;

        logger.debug(`[MCPOAuth] startAuthorization completed successfully`);
        logger.debug(
          `[MCPOAuth] Authorization URL: ${sanitizeUrlForLogging(authorizationUrl.toString())}`,
        );

        /** Add state parameter with flowId to the authorization URL */
        authorizationUrl.searchParams.set('state', flowId);
        logger.debug(`[MCPOAuth] Added state parameter to authorization URL`);

        if (resourceMetadata?.resource != null && resourceMetadata.resource) {
          authorizationUrl.searchParams.set('resource', resourceMetadata.resource);
          logger.debug(
            `[MCPOAuth] Added resource parameter to authorization URL: ${resourceMetadata.resource}`,
          );
        } else {
          logger.warn(
            `[MCPOAuth] Resource metadata missing 'resource' property for ${serverName}. ` +
              'This can cause issues with some Authorization Servers who expect a "resource" parameter.',
          );
        }
      } catch (error) {
        logger.error(`[MCPOAuth] startAuthorization failed:`, error);
        throw error;
      }

      const flowMetadata: MCPOAuthFlowMetadata = {
        serverName,
        userId,
        serverUrl,
        state,
        codeVerifier,
        clientInfo,
        metadata,
        resourceMetadata,
      };

      logger.debug(
        `[MCPOAuth] Authorization URL generated for ${serverName}: ${authorizationUrl.toString()}`,
      );

      const result = {
        authorizationUrl: authorizationUrl.toString(),
        flowId,
        flowMetadata,
      };

      logger.debug(
        `[MCPOAuth] Returning from initiateOAuthFlow with result ${flowId} for ${serverName}`,
        result,
      );
      return result;
    } catch (error) {
      logger.error('[MCPOAuth] Failed to initiate OAuth flow', { error, serverName, userId });
      throw error;
    }
  }

  /**
   * Completes the OAuth flow by exchanging the authorization code for tokens
   */
  static async completeOAuthFlow(
    flowId: string,
    authorizationCode: string,
    flowManager: FlowStateManager<MCPOAuthTokens>,
    oauthHeaders: Record<string, string>,
  ): Promise<MCPOAuthTokens> {
    try {
      /** Flow state which contains our metadata */
      const flowState = await flowManager.getFlowState(flowId, this.FLOW_TYPE);
      if (!flowState) {
        throw new Error('OAuth flow not found');
      }

      const flowMetadata = flowState.metadata as MCPOAuthFlowMetadata;
      if (!flowMetadata) {
        throw new Error('OAuth flow metadata not found');
      }

      const metadata = flowMetadata;
      if (!metadata.metadata || !metadata.clientInfo || !metadata.codeVerifier) {
        throw new Error('Invalid flow metadata');
      }

      let resource: URL | undefined;
      try {
        if (metadata.resourceMetadata?.resource != null && metadata.resourceMetadata.resource) {
          resource = new URL(metadata.resourceMetadata.resource);
          logger.debug(`[MCPOAuth] Resource URL for flow ${flowId}: ${resource.toString()}`);
        }
      } catch (error) {
        logger.warn(
          `[MCPOAuth] Invalid resource URL format for flow ${flowId}: '${metadata.resourceMetadata!.resource}'. ` +
            `Error: ${error instanceof Error ? error.message : 'Unknown error'}. Proceeding without resource parameter.`,
        );
        resource = undefined;
      }

      const tokens = await exchangeAuthorization(metadata.serverUrl, {
        redirectUri: metadata.clientInfo.redirect_uris?.[0] || this.getDefaultRedirectUri(),
        metadata: metadata.metadata as unknown as SDKOAuthMetadata,
        clientInformation: metadata.clientInfo,
        codeVerifier: metadata.codeVerifier,
        authorizationCode,
        resource,
        fetchFn: this.createOAuthFetch(oauthHeaders, metadata.clientInfo),
      });

      logger.debug('[MCPOAuth] Token exchange successful', {
        flowId,
        has_access_token: !!tokens.access_token,
        has_refresh_token: !!tokens.refresh_token,
        expires_in: tokens.expires_in,
        token_type: tokens.token_type,
        scope: tokens.scope,
      });

      const mcpTokens: MCPOAuthTokens = {
        ...tokens,
        obtained_at: Date.now(),
        expires_at: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined,
      };

      /** Now complete the flow with the tokens */
      await flowManager.completeFlow(flowId, this.FLOW_TYPE, mcpTokens);

      return mcpTokens;
    } catch (error) {
      logger.error('[MCPOAuth] Failed to complete OAuth flow', { error, flowId });
      await flowManager.failFlow(flowId, this.FLOW_TYPE, error as Error);
      throw error;
    }
  }

  /**
   * Gets the OAuth flow metadata
   */
  static async getFlowState(
    flowId: string,
    flowManager: FlowStateManager<MCPOAuthTokens>,
  ): Promise<MCPOAuthFlowMetadata | null> {
    const flowState = await flowManager.getFlowState(flowId, this.FLOW_TYPE);
    if (!flowState) {
      return null;
    }
    return flowState.metadata as MCPOAuthFlowMetadata;
  }

  /**
   * Generates a flow ID for the OAuth flow
   * @returns Consistent ID so concurrent requests share the same flow
   */
  public static generateFlowId(userId: string, serverName: string): string {
    return `${userId}:${serverName}`;
  }

  /**
   * Generates a secure state parameter
   */
  private static generateState(): string {
    return randomBytes(32).toString('base64url');
  }

  /**
   * Gets the default redirect URI for a server
   */
  private static getDefaultRedirectUri(serverName?: string): string {
    const baseUrl = process.env.DOMAIN_SERVER || 'http://localhost:3080';
    return serverName
      ? `${baseUrl}/api/mcp/${serverName}/oauth/callback`
      : `${baseUrl}/api/mcp/oauth/callback`;
  }

  /**
   * Processes and logs a token refresh response from an OAuth server.
   * Normalizes the response to MCPOAuthTokens format and logs debug info about refresh token rotation.
   */
  private static processRefreshResponse(
    tokens: Record<string, unknown>,
    serverName: string,
    source: string,
  ): MCPOAuthTokens {
    const hasNewRefreshToken = !!tokens.refresh_token;

    logger.debug(`[MCPOAuth] Token refresh response (${source})`, {
      serverName,
      has_new_access_token: !!tokens.access_token,
      has_new_refresh_token: hasNewRefreshToken,
      refresh_token_rotated: hasNewRefreshToken,
      expires_in: tokens.expires_in,
    });

    if (!hasNewRefreshToken) {
      logger.debug(
        `[MCPOAuth] OAuth server did not return new refresh_token for ${serverName} - existing refresh token remains valid (normal for non-rotating providers)`,
      );
    }

    return {
      ...tokens,
      obtained_at: Date.now(),
      expires_at:
        typeof tokens.expires_in === 'number' ? Date.now() + tokens.expires_in * 1000 : undefined,
    } as MCPOAuthTokens;
  }

  /**
   * Refreshes OAuth tokens using a refresh token
   */
  static async refreshOAuthTokens(
    refreshToken: string,
    metadata: { serverName: string; serverUrl?: string; clientInfo?: OAuthClientInformation },
    oauthHeaders: Record<string, string>,
    config?: MCPOptions['oauth'],
  ): Promise<MCPOAuthTokens> {
    logger.debug(`[MCPOAuth] Refreshing tokens for ${metadata.serverName}`);

    try {
      /** If we have stored client information from the original flow, use that first */
      if (metadata.clientInfo?.client_id) {
        logger.debug(
          `[MCPOAuth] Using stored client information for token refresh for ${metadata.serverName}`,
        );
        logger.debug(
          `[MCPOAuth] Client ID: ${metadata.clientInfo.client_id} for ${metadata.serverName}`,
        );
        logger.debug(
          `[MCPOAuth] Has client secret: ${!!metadata.clientInfo.client_secret} for ${metadata.serverName}`,
        );
        logger.debug(`[MCPOAuth] Stored client info for ${metadata.serverName}:`, {
          client_id: metadata.clientInfo.client_id,
          has_client_secret: !!metadata.clientInfo.client_secret,
          grant_types: metadata.clientInfo.grant_types,
          scope: metadata.clientInfo.scope,
        });

        /** Use the stored client information and metadata to determine the token URL */
        let tokenUrl: string;
        let authMethods: string[] | undefined;
        if (config?.token_url) {
          tokenUrl = config.token_url;
          authMethods = config.token_endpoint_auth_methods_supported;
        } else if (!metadata.serverUrl) {
          throw new Error('No token URL available for refresh');
        } else {
          /** Auto-discover OAuth configuration for refresh */
          const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl, {
            fetchFn: this.createOAuthFetch(oauthHeaders),
          });
          if (!oauthMetadata) {
            /**
             * No metadata discovered - use fallback /token endpoint.
             * This mirrors the MCP SDK's behavior for legacy servers without .well-known endpoints.
             */
            logger.warn(
              `[MCPOAuth] No OAuth metadata discovered for token refresh, using fallback /token endpoint`,
            );
            tokenUrl = new URL('/token', metadata.serverUrl).toString();
            authMethods = ['client_secret_basic', 'client_secret_post', 'none'];
          } else if (!oauthMetadata.token_endpoint) {
            throw new Error('No token endpoint found in OAuth metadata');
          } else {
            tokenUrl = oauthMetadata.token_endpoint;
            authMethods = oauthMetadata.token_endpoint_auth_methods_supported;
          }
        }

        const body = new URLSearchParams({
          grant_type: 'refresh_token',
          refresh_token: refreshToken,
        });

        /** Add scope if available */
        if (metadata.clientInfo.scope) {
          body.append('scope', metadata.clientInfo.scope);
        }

        const headers: HeadersInit = {
          Accept: 'application/json',
          'Content-Type': 'application/x-www-form-urlencoded',
          ...oauthHeaders,
        };

        /** Handle authentication based on server's advertised methods */
        if (metadata.clientInfo.client_secret) {
          /** Default to client_secret_basic if no methods specified (per RFC 8414) */
          const tokenAuthMethods = authMethods ?? ['client_secret_basic'];
          const authMethod = this.resolveTokenEndpointAuthMethod({
            tokenExchangeMethod: config?.token_exchange_method,
            tokenAuthMethods,
            preferredMethod: metadata.clientInfo.token_endpoint_auth_method,
          });

          if (authMethod === 'client_secret_basic') {
            /** Use Basic auth */
            logger.debug('[MCPOAuth] Using client_secret_basic authentication method');
            const clientAuth = Buffer.from(
              `${metadata.clientInfo.client_id}:${metadata.clientInfo.client_secret}`,
            ).toString('base64');
            headers['Authorization'] = `Basic ${clientAuth}`;
          } else if (authMethod === 'client_secret_post') {
            /** Use client_secret_post */
            logger.debug('[MCPOAuth] Using client_secret_post authentication method');
            body.append('client_id', metadata.clientInfo.client_id);
            body.append('client_secret', metadata.clientInfo.client_secret);
          } else {
            /** No recognized method, default to Basic auth per RFC */
            logger.debug('[MCPOAuth] No recognized auth method, defaulting to client_secret_basic');
            const clientAuth = Buffer.from(
              `${metadata.clientInfo.client_id}:${metadata.clientInfo.client_secret}`,
            ).toString('base64');
            headers['Authorization'] = `Basic ${clientAuth}`;
          }
        } else {
          /** For public clients, client_id must be in the body */
          logger.debug('[MCPOAuth] Using public client authentication (no secret)');
          body.append('client_id', metadata.clientInfo.client_id);
        }

        logger.debug(`[MCPOAuth] Refresh request to: ${sanitizeUrlForLogging(tokenUrl)}`, {
          body: body.toString(),
          headers,
        });

        const response = await fetch(tokenUrl, {
          method: 'POST',
          headers,
          body,
        });

        if (!response.ok) {
          const errorText = await response.text();
          throw new Error(
            `Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`,
          );
        }

        const tokens = await response.json();
        return this.processRefreshResponse(tokens, metadata.serverName, 'stored client info');
      }

      // Fallback: If we have pre-configured OAuth settings, use them
      if (config?.token_url && config?.client_id) {
        logger.debug(`[MCPOAuth] Using pre-configured OAuth settings for token refresh`);

        const tokenUrl = new URL(config.token_url);

        const body = new URLSearchParams({
          grant_type: 'refresh_token',
          refresh_token: refreshToken,
        });

        if (config.scope) {
          body.append('scope', config.scope);
        }

        const headers: HeadersInit = {
          Accept: 'application/json',
          'Content-Type': 'application/x-www-form-urlencoded',
          ...oauthHeaders,
        };

        /** Handle authentication based on configured methods */
        if (config.client_secret) {
          /** Default to client_secret_basic if no methods specified (per RFC 8414) */
          const tokenAuthMethods = config.token_endpoint_auth_methods_supported ?? [
            'client_secret_basic',
          ];
          const authMethod = this.resolveTokenEndpointAuthMethod({
            tokenExchangeMethod: config.token_exchange_method,
            tokenAuthMethods,
          });

          if (authMethod === 'client_secret_basic') {
            /** Use Basic auth */
            logger.debug(
              '[MCPOAuth] Using client_secret_basic authentication method (pre-configured)',
            );
            const clientAuth = Buffer.from(`${config.client_id}:${config.client_secret}`).toString(
              'base64',
            );
            headers['Authorization'] = `Basic ${clientAuth}`;
          } else if (authMethod === 'client_secret_post') {
            /** Use client_secret_post */
            logger.debug(
              '[MCPOAuth] Using client_secret_post authentication method (pre-configured)',
            );
            body.append('client_id', config.client_id);
            body.append('client_secret', config.client_secret);
          } else {
            /** No recognized method, default to Basic auth per RFC */
            logger.debug(
              '[MCPOAuth] No recognized auth method, defaulting to client_secret_basic (pre-configured)',
            );
            const clientAuth = Buffer.from(`${config.client_id}:${config.client_secret}`).toString(
              'base64',
            );
            headers['Authorization'] = `Basic ${clientAuth}`;
          }
        } else {
          /** For public clients, client_id must be in the body */
          logger.debug('[MCPOAuth] Using public client authentication (no secret, pre-configured)');
          body.append('client_id', config.client_id);
        }

        const response = await fetch(tokenUrl, {
          method: 'POST',
          headers,
          body,
        });

        if (!response.ok) {
          const errorText = await response.text();
          throw new Error(
            `Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`,
          );
        }

        const tokens = await response.json();
        return this.processRefreshResponse(tokens, metadata.serverName, 'pre-configured OAuth');
      }

      /** For auto-discovered OAuth, we need the server URL */
      if (!metadata.serverUrl) {
        throw new Error('Server URL required for auto-discovered OAuth token refresh');
      }

      /** Auto-discover OAuth configuration for refresh */
      const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl, {
        fetchFn: this.createOAuthFetch(oauthHeaders),
      });

      let tokenUrl: URL;
      if (!oauthMetadata?.token_endpoint) {
        /**
         * No metadata or token_endpoint discovered - use fallback /token endpoint.
         * This mirrors the MCP SDK's behavior for legacy servers without .well-known endpoints.
         */
        logger.warn(
          `[MCPOAuth] No OAuth metadata or token endpoint found, using fallback /token endpoint`,
        );
        tokenUrl = new URL('/token', metadata.serverUrl);
      } else {
        tokenUrl = new URL(oauthMetadata.token_endpoint);
      }

      const body = new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
      });

      const headers: HeadersInit = {
        Accept: 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded',
        ...oauthHeaders,
      };

      const response = await fetch(tokenUrl, {
        method: 'POST',
        headers,
        body,
      });

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(
          `Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`,
        );
      }

      const tokens = await response.json();
      return this.processRefreshResponse(tokens, metadata.serverName, 'auto-discovered OAuth');
    } catch (error) {
      logger.error(`[MCPOAuth] Failed to refresh tokens for ${metadata.serverName}`, error);
      throw error;
    }
  }

  /**
   * Revokes OAuth tokens at the authorization server (RFC 7009)
   */
  public static async revokeOAuthToken(
    serverName: string,
    token: string,
    tokenType: 'refresh' | 'access',
    metadata: {
      serverUrl: string;
      clientId: string;
      clientSecret: string;
      revocationEndpoint?: string;
      revocationEndpointAuthMethodsSupported?: string[];
    },
    oauthHeaders: Record<string, string> = {},
  ): Promise<void> {
    // build the revoke URL, falling back to the server URL + /revoke if no revocation endpoint is provided
    const revokeUrl: URL =
      metadata.revocationEndpoint != null
        ? new URL(metadata.revocationEndpoint)
        : new URL('/revoke', metadata.serverUrl);

    // detect auth method to use
    const authMethods = metadata.revocationEndpointAuthMethodsSupported ?? [
      'client_secret_basic', // RFC 8414 (https://datatracker.ietf.org/doc/html/rfc8414)
    ];
    const usesBasicAuth = authMethods.includes('client_secret_basic');
    const usesClientSecretPost = authMethods.includes('client_secret_post');

    // init the request headers
    const headers: Record<string, string> = {
      'Content-Type': 'application/x-www-form-urlencoded',
      ...oauthHeaders,
    };

    // init the request body
    const body = new URLSearchParams({ token });
    body.set('token_type_hint', tokenType === 'refresh' ? 'refresh_token' : 'access_token');

    // process auth method
    if (usesBasicAuth) {
      // encode the client id and secret and add to the headers
      const credentials = Buffer.from(`${metadata.clientId}:${metadata.clientSecret}`).toString(
        'base64',
      );
      headers['Authorization'] = `Basic ${credentials}`;
    } else if (usesClientSecretPost) {
      // add the client id and secret to the body
      body.set('client_secret', metadata.clientSecret);
      body.set('client_id', metadata.clientId);
    }

    // perform the revoke request
    logger.info(
      `[MCPOAuth] Revoking tokens for ${serverName} via ${sanitizeUrlForLogging(revokeUrl.toString())}`,
    );
    const response = await fetch(revokeUrl, {
      method: 'POST',
      body: body.toString(),
      headers,
    });

    if (!response.ok) {
      logger.error(`[MCPOAuth] Token revocation failed for ${serverName}: HTTP ${response.status}`);
      throw new Error(`Token revocation failed: HTTP ${response.status}`);
    }
  }
}
