import { z } from 'zod';
import { TokenExchangeMethodEnum } from './types/agents';
import { extractEnvVariable } from './utils';

const BaseOptionsSchema = z.object({
  /** Display name for the MCP server - only letters, numbers, and spaces allowed */
  title: z
    .string()
    .regex(/^[a-zA-Z0-9 ]+$/, 'Title can only contain letters, numbers, and spaces')
    .optional(),
  /** Description of the MCP server */
  description: z.string().optional(),
  /**
   * Controls whether the MCP server is initialized during application startup.
   * - true (default): Server is initialized during app startup and included in app-level connections
   * - false: Skips initialization at startup and excludes from app-level connections - useful for servers
   *   requiring manual authentication (e.g., GitHub PAT tokens) that need to be configured through the UI after startup
   */
  startup: z.boolean().optional(),
  iconPath: z.string().optional(),
  timeout: z.number().optional(),
  initTimeout: z.number().optional(),
  /** Controls visibility in chat dropdown menu (MCPSelect) */
  chatMenu: z.boolean().optional(),
  /**
   * Controls server instruction behavior:
   * - undefined/not set: No instructions included (default)
   * - true: Use server-provided instructions
   * - string: Use custom instructions (overrides server-provided)
   */
  serverInstructions: z.union([z.boolean(), z.string()]).optional(),
  /**
   * Whether this server requires OAuth authentication
   * If not specified, will be auto-detected during construction
   */
  requiresOAuth: z.boolean().optional(),
  /**
   * OAuth configuration for SSE and Streamable HTTP transports
   * - Optional: OAuth can be auto-discovered on 401 responses
   * - Pre-configured values will skip discovery steps
   */
  oauth: z
    .object({
      /** OAuth authorization endpoint (optional - can be auto-discovered) */
      authorization_url: z.string().url().optional(),
      /** OAuth token endpoint (optional - can be auto-discovered) */
      token_url: z.string().url().optional(),
      /** OAuth client ID (optional - can use dynamic registration) */
      client_id: z.string().optional(),
      /** OAuth client secret (optional - can use dynamic registration) */
      client_secret: z.string().optional(),
      /** OAuth scopes to request */
      scope: z.string().optional(),
      /** OAuth redirect URI (defaults to /api/mcp/{serverName}/oauth/callback) */
      redirect_uri: z.string().url().optional(),
      /** Token exchange method */
      token_exchange_method: z.nativeEnum(TokenExchangeMethodEnum).optional(),
      /** Supported grant types (defaults to ['authorization_code', 'refresh_token']) */
      grant_types_supported: z.array(z.string()).optional(),
      /** Supported token endpoint authentication methods (defaults to ['client_secret_basic', 'client_secret_post']) */
      token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
      /** Supported response types (defaults to ['code']) */
      response_types_supported: z.array(z.string()).optional(),
      /** Supported code challenge methods (defaults to ['S256', 'plain']) */
      code_challenge_methods_supported: z.array(z.string()).optional(),
      /** Skip code challenge validation and force S256 (useful for providers like AWS Cognito that support S256 but don't advertise it) */
      skip_code_challenge_check: z.boolean().optional(),
      /** OAuth revocation endpoint (optional - can be auto-discovered) */
      revocation_endpoint: z.string().url().optional(),
      /** OAuth revocation endpoint authentication methods supported (optional - can be auto-discovered) */
      revocation_endpoint_auth_methods_supported: z.array(z.string()).optional(),
    })
    .optional(),
  /** Custom headers to send with OAuth requests (registration, discovery, token exchange, etc.) */
  oauth_headers: z.record(z.string(), z.string()).optional(),
  /**
   * API Key authentication configuration for SSE and Streamable HTTP transports
   * - source: 'admin' means the key is provided by admin and shared by all users
   * - source: 'user' means each user provides their own key via customUserVars
   */
  apiKey: z
    .object({
      /** API key value (only for admin-provided mode, stored encrypted) */
      key: z.string().optional(),
      /** Whether key is provided by admin or each user */
      source: z.enum(['admin', 'user']),
      /** How to format the authorization header */
      authorization_type: z.enum(['basic', 'bearer', 'custom']),
      /** Custom header name when authorization_type is 'custom' */
      custom_header: z.string().optional(),
    })
    .optional(),
  customUserVars: z
    .record(
      z.string(),
      z.object({
        title: z.string(),
        description: z.string(),
      }),
    )
    .optional(),
});

export const StdioOptionsSchema = BaseOptionsSchema.extend({
  type: z.literal('stdio').optional(),
  /**
   * The executable to run to start the server.
   */
  command: z.string(),
  /**
   * Command line arguments to pass to the executable.
   */
  args: z.array(z.string()),
  /**
   * The environment to use when spawning the process.
   *
   * If not specified, the result of getDefaultEnvironment() will be used.
   * Environment variables can be referenced using ${VAR_NAME} syntax.
   */
  env: z
    .record(z.string(), z.string())
    .optional()
    .transform((env) => {
      if (!env) {
        return env;
      }

      const processedEnv: Record<string, string> = {};
      for (const [key, value] of Object.entries(env)) {
        processedEnv[key] = extractEnvVariable(value);
      }
      return processedEnv;
    }),
  /**
   * How to handle stderr of the child process. This matches the semantics of Node's `child_process.spawn`.
   *
   * @type {import('node:child_process').IOType | import('node:stream').Stream | number}
   *
   * The default is "inherit", meaning messages to stderr will be printed to the parent process's stderr.
   */
  stderr: z.any().optional(),
});

export const WebSocketOptionsSchema = BaseOptionsSchema.extend({
  type: z.literal('websocket').optional(),
  url: z
    .string()
    .transform((val: string) => extractEnvVariable(val))
    .pipe(z.string().url())
    .refine(
      (val: string) => {
        const protocol = new URL(val).protocol;
        return protocol === 'ws:' || protocol === 'wss:';
      },
      {
        message: 'WebSocket URL must start with ws:// or wss://',
      },
    ),
});

export const SSEOptionsSchema = BaseOptionsSchema.extend({
  type: z.literal('sse').optional(),
  headers: z.record(z.string(), z.string()).optional(),
  url: z
    .string()
    .transform((val: string) => extractEnvVariable(val))
    .pipe(z.string().url())
    .refine(
      (val: string) => {
        const protocol = new URL(val).protocol;
        return protocol !== 'ws:' && protocol !== 'wss:';
      },
      {
        message: 'SSE URL must not start with ws:// or wss://',
      },
    ),
});

export const StreamableHTTPOptionsSchema = BaseOptionsSchema.extend({
  type: z.union([z.literal('streamable-http'), z.literal('http')]),
  headers: z.record(z.string(), z.string()).optional(),
  url: z
    .string()
    .transform((val: string) => extractEnvVariable(val))
    .pipe(z.string().url())
    .refine(
      (val: string) => {
        const protocol = new URL(val).protocol;
        return protocol !== 'ws:' && protocol !== 'wss:';
      },
      {
        message: 'Streamable HTTP URL must not start with ws:// or wss://',
      },
    ),
});

export const MCPOptionsSchema = z.union([
  StdioOptionsSchema,
  WebSocketOptionsSchema,
  SSEOptionsSchema,
  StreamableHTTPOptionsSchema,
]);

export const MCPServersSchema = z.record(z.string(), MCPOptionsSchema);

export type MCPOptions = z.infer<typeof MCPOptionsSchema>;

/**
 * Helper to omit server-managed fields that should not come from UI
 */
const omitServerManagedFields = <T extends z.ZodObject<z.ZodRawShape>>(schema: T) =>
  schema.omit({
    startup: true,
    timeout: true,
    initTimeout: true,
    chatMenu: true,
    serverInstructions: true,
    requiresOAuth: true,
    customUserVars: true,
    oauth_headers: true,
  });

/**
 * MCP Server configuration that comes from UI/API input only.
 * Omits server-managed fields like startup, timeout, customUserVars, etc.
 * Allows: title, description, url, iconPath, oauth (user credentials)
 *
 * SECURITY: Stdio transport is intentionally excluded from user input.
 * Stdio allows arbitrary command execution and should only be configured
 * by administrators via the YAML config file (librechat.yaml).
 * Only remote transports (SSE, HTTP, WebSocket) are allowed via the API.
 */
export const MCPServerUserInputSchema = z.union([
  omitServerManagedFields(WebSocketOptionsSchema),
  omitServerManagedFields(SSEOptionsSchema),
  omitServerManagedFields(StreamableHTTPOptionsSchema),
]);

export type MCPServerUserInput = z.infer<typeof MCPServerUserInputSchema>;
