import { auth, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
import { randomBytes } from 'node:crypto';
import { getOAuthProvider, registerOAuthProvider, unregisterOAuthProvider } from '../../ai/utils/oauth/index.js';
import { startOAuthCallbackServer } from '../auth/oauth-callback-server.js';
import { getHostedMcpOAuthConfig, getHostedMcpOAuthProviderId } from './hosted-oauth-id.js';
import { registerManagedMcpOAuthProvider, unregisterManagedMcpOAuthProvider } from './managed-oauth-providers.js';
import { resolveHostedMcpOAuth, resolveHostedMcpOAuthTimeoutMs } from './oauth-discovery.js';
import { hostedMcpFetch } from './proxy-fetch.js';
const DEFAULT_REDIRECT_URL = 'http://127.0.0.1:0/auth/callback';
const EXPIRY_BUFFER_MS = 30_000;
// The underlying OAuth provider registry is process-global, so hosted MCP
// providers need process-global ref counting as well.
const registeredHostedProviders = new Map();
function buildClientMetadata(serverName, config, redirectUrl) {
    const oauth = getHostedMcpOAuthConfig(config);
    return {
        redirect_uris: [redirectUrl],
        token_endpoint_auth_method: oauth.clientSecret ? 'client_secret_post' : 'none',
        grant_types: ['authorization_code', 'refresh_token'],
        response_types: ['code'],
        client_name: oauth.providerName ?? `ShortcutXL MCP ${serverName}`,
        ...(oauth.scope ? { scope: oauth.scope } : {})
    };
}
function resolveRedirectTemplate(config, mcpOAuthStore) {
    const oauth = getHostedMcpOAuthConfig(config);
    const settings = mcpOAuthStore?.getSettings();
    const configuredCallbackUrl = settings?.callbackUrl.trim();
    if (configuredCallbackUrl) {
        return new URL(configuredCallbackUrl).toString();
    }
    const baseUrl = oauth.redirectUrl || DEFAULT_REDIRECT_URL;
    const redirectUrl = new URL(baseUrl);
    if ((settings?.callbackPort ?? 0) > 0) {
        redirectUrl.port = String(settings?.callbackPort);
    }
    return redirectUrl.toString();
}
function parseAuthorizationCode(input, expectedState) {
    const value = input.trim();
    if (!value) {
        throw new Error('Login cancelled');
    }
    let url;
    try {
        url = new URL(value);
    }
    catch {
        // Ignore and fall back to treating the input as the code itself.
    }
    if (url) {
        const error = url.searchParams.get('error');
        const errorDescription = url.searchParams.get('error_description');
        if (error) {
            throw new Error(errorDescription ? `OAuth error: ${error} - ${errorDescription}` : `OAuth error: ${error}`);
        }
        const code = url.searchParams.get('code');
        const state = url.searchParams.get('state') ?? undefined;
        if (expectedState) {
            if (!state) {
                throw new Error('OAuth callback did not include state');
            }
            if (state !== expectedState) {
                throw new Error('OAuth callback state mismatch');
            }
        }
        if (code) {
            return code;
        }
    }
    if (value.includes('code=')) {
        const params = new URLSearchParams(value);
        const error = params.get('error');
        const errorDescription = params.get('error_description');
        if (error) {
            throw new Error(errorDescription ? `OAuth error: ${error} - ${errorDescription}` : `OAuth error: ${error}`);
        }
        const code = params.get('code');
        const state = params.get('state') ?? undefined;
        if (expectedState) {
            if (!state) {
                throw new Error('OAuth callback did not include state');
            }
            if (state !== expectedState) {
                throw new Error('OAuth callback state mismatch');
            }
        }
        if (code) {
            return code;
        }
    }
    if (value.includes('://') ||
        value.startsWith('?') ||
        value.startsWith('&') ||
        (value.includes('=') && !/^[A-Za-z0-9._~-]+$/.test(value))) {
        throw new Error('OAuth callback did not include an authorization code');
    }
    if (expectedState) {
        throw new Error('OAuth callback did not include state');
    }
    return value;
}
function toOAuthCredentials(tokens) {
    if (!tokens?.access_token || !tokens.refresh_token) {
        throw new Error('OAuth provider did not return usable access and refresh tokens');
    }
    return {
        access: tokens.access_token,
        refresh: tokens.refresh_token,
        expires: Date.now() + (tokens.expires_in ?? 3600) * 1000 - EXPIRY_BUFFER_MS
    };
}
function toOAuthTokens(credentials) {
    if (!credentials) {
        return undefined;
    }
    return {
        access_token: credentials.access,
        refresh_token: credentials.refresh,
        token_type: 'Bearer',
        expires_in: Math.max(1, Math.ceil((credentials.expires - Date.now()) / 1000))
    };
}
function createHostedMcpClientProvider(options) {
    const { authStorage, config, mcpOAuthStore, providerId, redirectUrl, serverName } = options;
    const oauth = getHostedMcpOAuthConfig(config);
    let tokens;
    let codeVerifier = '';
    let state;
    let discoveryState = mcpOAuthStore?.getDiscoveryState(providerId);
    const getClientInformation = () => {
        const stored = mcpOAuthStore?.getClientInfo(providerId);
        if (stored) {
            return {
                client_id: stored.clientId,
                ...(stored.clientSecret ? { client_secret: stored.clientSecret } : {})
            };
        }
        if (!oauth.clientId) {
            return undefined;
        }
        return {
            client_id: oauth.clientId,
            ...(oauth.clientSecret ? { client_secret: oauth.clientSecret } : {})
        };
    };
    return {
        redirectUrl,
        clientMetadata: buildClientMetadata(serverName, config, redirectUrl),
        clientInformation: getClientInformation,
        saveClientInformation: (nextInfo) => {
            mcpOAuthStore?.setClientInfo(providerId, {
                clientId: nextInfo.client_id,
                ...(nextInfo.client_secret ? { clientSecret: nextInfo.client_secret } : {})
            });
        },
        tokens: () => tokens,
        saveTokens: (nextTokens) => {
            tokens = nextTokens;
        },
        redirectToAuthorization: async (authorizationUrl) => {
            if (!options.interactiveAuth) {
                throw new UnauthorizedError('Interactive re-authentication required');
            }
            await options.interactiveAuth.onAuthorization(authorizationUrl);
        },
        saveCodeVerifier(nextVerifier) {
            codeVerifier = nextVerifier;
        },
        codeVerifier() {
            return codeVerifier;
        },
        state() {
            if (!state) {
                state = randomBytes(32).toString('base64url');
            }
            return state;
        },
        saveDiscoveryState(state) {
            discoveryState = state;
            mcpOAuthStore?.setDiscoveryState(providerId, state);
        },
        discoveryState() {
            return discoveryState ?? mcpOAuthStore?.getDiscoveryState(providerId);
        },
        validateResourceURL: async () => {
            const resolvedAuth = await resolveHostedMcpOAuth(serverName, config, mcpOAuthStore, {
                timeoutMs: resolveHostedMcpOAuthTimeoutMs(config)
            });
            return resolvedAuth.resource ? new URL(resolvedAuth.resource) : undefined;
        },
        invalidateCredentials(scope) {
            if (!authStorage) {
                return;
            }
            if (scope === 'all') {
                authStorage.logout(providerId);
                mcpOAuthStore?.clear(providerId);
                return;
            }
            if (scope === 'client') {
                mcpOAuthStore?.removeClientInfo(providerId);
                return;
            }
            if (scope === 'discovery') {
                mcpOAuthStore?.removeDiscoveryState(providerId);
                return;
            }
            if (scope === 'tokens') {
                authStorage.invalidateToken(providerId);
            }
        }
    };
}
export function isHostedMcpOAuthProviderId(providerId) {
    const provider = getOAuthProvider(providerId);
    return provider?.hostedMcpProvider === true;
}
export function createHostedMcpOAuthProvider(serverName, config, authStorage, mcpOAuthStore) {
    const oauth = getHostedMcpOAuthConfig(config);
    const providerId = getHostedMcpOAuthProviderId(serverName, config);
    return {
        id: providerId,
        name: oauth.providerName ?? `MCP: ${serverName}`,
        hostedMcpProvider: true,
        hostedMcpServerName: serverName,
        usesCallbackServer: true,
        redirectUrl: resolveRedirectTemplate(config, mcpOAuthStore),
        async login(callbacks) {
            const callbackServer = await startOAuthCallbackServer(resolveRedirectTemplate(config, mcpOAuthStore), callbacks.signal);
            const provider = createHostedMcpClientProvider({
                redirectUrl: callbackServer.redirectUrl,
                authStorage,
                mcpOAuthStore,
                providerId,
                serverName,
                config,
                interactiveAuth: {
                    onAuthorization: async (authorizationUrl) => {
                        callbacks.onAuth({
                            url: authorizationUrl.toString(),
                            instructions: 'Complete login in the browser.'
                        });
                    }
                }
            });
            try {
                const resolvedAuth = await resolveHostedMcpOAuth(serverName, config, mcpOAuthStore, {
                    timeoutMs: resolveHostedMcpOAuthTimeoutMs(config)
                });
                const result = await auth(provider, {
                    serverUrl: config.url,
                    scope: resolvedAuth.scope,
                    resourceMetadataUrl: resolvedAuth.resourceMetadataUrl,
                    fetchFn: hostedMcpFetch
                });
                if (result === 'REDIRECT') {
                    const authorizationCode = parseAuthorizationCode(await callbackServer.waitForCallback(), await provider.state?.());
                    await auth(provider, {
                        serverUrl: config.url,
                        authorizationCode,
                        scope: resolvedAuth.scope,
                        resourceMetadataUrl: resolvedAuth.resourceMetadataUrl,
                        fetchFn: hostedMcpFetch
                    });
                }
                return toOAuthCredentials(await provider.tokens());
            }
            finally {
                await callbackServer.close().catch(() => { });
            }
        },
        async refreshToken(credentials) {
            const resolvedAuth = await resolveHostedMcpOAuth(serverName, config, mcpOAuthStore, {
                timeoutMs: resolveHostedMcpOAuthTimeoutMs(config)
            });
            const provider = createHostedMcpClientProvider({
                redirectUrl: resolveRedirectTemplate(config, mcpOAuthStore),
                authStorage,
                mcpOAuthStore,
                providerId,
                serverName,
                config
            });
            const initialTokens = toOAuthTokens(credentials);
            if (!initialTokens) {
                throw new Error('OAuth provider did not return usable access and refresh tokens');
            }
            provider.saveTokens?.(initialTokens);
            await auth(provider, {
                serverUrl: config.url,
                scope: resolvedAuth.scope,
                resourceMetadataUrl: resolvedAuth.resourceMetadataUrl,
                fetchFn: hostedMcpFetch
            });
            return toOAuthCredentials(await provider.tokens());
        },
        getApiKey(credentials) {
            return credentials.access;
        }
    };
}
export function createHostedMcpRuntimeAuthProvider(serverName, config, authStorage, mcpOAuthStore) {
    const providerId = getHostedMcpOAuthProviderId(serverName, config);
    const provider = createHostedMcpClientProvider({
        redirectUrl: resolveRedirectTemplate(config, mcpOAuthStore),
        authStorage,
        mcpOAuthStore,
        providerId,
        serverName,
        config
    });
    provider.tokens = () => {
        const credentials = authStorage.get(providerId);
        return credentials?.type === 'oauth' ? toOAuthTokens(credentials) : undefined;
    };
    provider.saveTokens = (tokens) => {
        authStorage.set(providerId, {
            type: 'oauth',
            ...toOAuthCredentials(tokens)
        });
    };
    return provider;
}
export function registerHostedMcpOAuthProviders(servers, authStorage, mcpOAuthStore) {
    const providerIds = [];
    for (const [serverName, config] of Object.entries(servers)) {
        if ('command' in config || config.transport !== 'streamable_http') {
            continue;
        }
        const provider = createHostedMcpOAuthProvider(serverName, config, authStorage, mcpOAuthStore);
        const existing = registeredHostedProviders.get(provider.id);
        if (existing) {
            registeredHostedProviders.set(provider.id, {
                provider: existing.provider,
                refCount: existing.refCount + 1
            });
        }
        else {
            registerOAuthProvider(provider);
            registeredHostedProviders.set(provider.id, { provider, refCount: 1 });
        }
        registerManagedMcpOAuthProvider(provider.id);
        providerIds.push(provider.id);
    }
    return providerIds;
}
export function unregisterHostedMcpOAuthProviders(providerIds) {
    for (const providerId of providerIds) {
        const existing = registeredHostedProviders.get(providerId);
        if (!existing) {
            unregisterManagedMcpOAuthProvider(providerId);
            continue;
        }
        if (existing.refCount <= 1) {
            registeredHostedProviders.delete(providerId);
            unregisterOAuthProvider(providerId);
        }
        else {
            registeredHostedProviders.set(providerId, {
                provider: existing.provider,
                refCount: existing.refCount - 1
            });
        }
        unregisterManagedMcpOAuthProvider(providerId);
    }
}
//# sourceMappingURL=hosted-oauth-provider.js.map