/**
 * @license
 * Copyright 2025 OElite Platform
 * SPDX-License-Identifier: Apache-2.0
 */
import * as http from 'node:http';
import * as url from 'node:url';
import * as crypto from 'node:crypto';
import * as net from 'node:net';
import open from 'open';
import { promises as fs } from 'node:fs';
import * as path from 'node:path';
import os from 'node:os';
import { EventEmitter } from 'node:events';
// OAuth Client ID for Claude Code (Anthropic)
const OAUTH_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
// OAuth Endpoints for Anthropic
const OAUTH_AUTHORIZE_URL = 'https://console.anthropic.com/oauth/authorize';
const OAUTH_TOKEN_URL = 'https://console.anthropic.com/v1/oauth/token';
const OAUTH_REDIRECT_URI = 'http://localhost';
// OAuth Scopes for Claude Code
const OAUTH_SCOPES = 'org:create_api_key user:profile user:inference';
const HTTP_REDIRECT = 301;
const SIGN_IN_SUCCESS_URL = 'https://console.anthropic.com/settings/keys';
const SIGN_IN_FAILURE_URL = 'https://console.anthropic.com/';
// Event emitter for OAuth flow
export const claudeCodeOAuth2Events = new EventEmitter();
export var ClaudeCodeOAuth2Event;
(function (ClaudeCodeOAuth2Event) {
    ClaudeCodeOAuth2Event["AuthUri"] = "authUri";
    ClaudeCodeOAuth2Event["AuthProgress"] = "authProgress";
    ClaudeCodeOAuth2Event["AuthCancel"] = "authCancel";
    ClaudeCodeOAuth2Event["AuthSuccess"] = "authSuccess";
    ClaudeCodeOAuth2Event["AuthError"] = "authError";
})(ClaudeCodeOAuth2Event || (ClaudeCodeOAuth2Event = {}));
/**
 * Get the path where Claude Code OAuth credentials are stored
 */
function getCredentialsPath() {
    const configDir = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
    return path.join(configDir, 'arc-cli', 'claude-code-oauth-creds.json');
}
/**
 * Cache credentials to disk
 */
async function cacheCredentials(credentials) {
    const credPath = getCredentialsPath();
    const credDir = path.dirname(credPath);
    try {
        // Create directory if it doesn't exist
        await fs.mkdir(credDir, { recursive: true, mode: 0o700 });
        // Write credentials with restrictive permissions
        await fs.writeFile(credPath, JSON.stringify(credentials, null, 2), {
            mode: 0o600,
        });
        console.log(`[Claude Code OAuth] Cached credentials to ${credPath}`);
    }
    catch (error) {
        console.error('[Claude Code OAuth] Failed to cache credentials:', error);
        throw error;
    }
}
/**
 * Fetch cached credentials from disk
 */
async function fetchCachedCredentials() {
    const credPath = getCredentialsPath();
    try {
        const data = await fs.readFile(credPath, 'utf-8');
        const credentials = JSON.parse(data);
        return credentials;
    }
    catch (_error) {
        console.debug('[Claude Code OAuth] No cached credentials found');
        return null;
    }
}
/**
 * Clear cached credentials
 */
export async function clearClaudeCodeCredentials() {
    const credPath = getCredentialsPath();
    try {
        await fs.unlink(credPath);
        console.log('[Claude Code OAuth] Cleared cached credentials');
    }
    catch (error) {
        console.debug('[Claude Code OAuth] No credentials to clear:', error);
    }
}
/**
 * Generate a random code verifier for PKCE
 */
function generateCodeVerifier() {
    const randomBytes = crypto.randomBytes(32);
    return randomBytes
        .toString('base64url')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '');
}
/**
 * Generate code challenge from code verifier for PKCE
 */
function generateCodeChallenge(codeVerifier) {
    const hash = crypto.createHash('sha256').update(codeVerifier).digest();
    return hash
        .toString('base64url')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '');
}
/**
 * Find an available port for the OAuth callback server
 */
async function getClaudeCodeAvailablePort() {
    return new Promise((resolve, reject) => {
        const server = net.createServer();
        server.listen(0, () => {
            const address = server.address();
            if (address && typeof address !== 'string') {
                const { port } = address;
                server.close(() => resolve(port));
            }
            else {
                server.close(() => reject(new Error('Failed to get available port address')));
            }
        });
        server.on('error', reject);
    });
}
/**
 * Exchange authorization code for tokens
 */
async function exchangeAuthorizationCode(code, codeVerifier, redirectUri) {
    const tokenParams = new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: OAUTH_CLIENT_ID,
        code,
        code_verifier: codeVerifier,
        redirect_uri: redirectUri,
    });
    const response = await fetch(OAUTH_TOKEN_URL, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: tokenParams.toString(),
    });
    if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`Failed to exchange authorization code: ${response.status} ${errorText}`);
    }
    const data = await response.json();
    return {
        access_token: data.access_token,
        refresh_token: data.refresh_token,
        expires_at: data.expires_in
            ? Date.now() + data.expires_in * 1000
            : undefined,
        token_type: data.token_type || 'Bearer',
    };
}
/**
 * Refresh access token using refresh token
 */
async function refreshAccessToken(refreshToken) {
    const tokenParams = new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: OAUTH_CLIENT_ID,
        refresh_token: refreshToken,
    });
    const response = await fetch(OAUTH_TOKEN_URL, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: tokenParams.toString(),
    });
    if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`Failed to refresh access token: ${response.status} ${errorText}`);
    }
    const data = await response.json();
    return {
        access_token: data.access_token,
        refresh_token: data.refresh_token || refreshToken,
        expires_at: data.expires_in
            ? Date.now() + data.expires_in * 1000
            : undefined,
        token_type: data.token_type || 'Bearer',
    };
}
/**
 * Initiate browser-based OAuth flow for Claude Code
 */
export async function authWithClaudeCodeWeb() {
    const port = await getClaudeCodeAvailablePort();
    const redirectUri = `${OAUTH_REDIRECT_URI}:${port}/callback`;
    // Generate PKCE parameters
    const codeVerifier = generateCodeVerifier();
    const codeChallenge = generateCodeChallenge(codeVerifier);
    const state = crypto.randomBytes(32).toString('hex');
    // Construct authorization URL
    const authParams = new URLSearchParams({
        response_type: 'code',
        client_id: OAUTH_CLIENT_ID,
        redirect_uri: redirectUri,
        scope: OAUTH_SCOPES,
        state,
        code_challenge: codeChallenge,
        code_challenge_method: 'S256',
    });
    const authUrl = `${OAUTH_AUTHORIZE_URL}?${authParams.toString()}`;
    // Emit auth URI event for UI to display
    const authResponse = {
        verification_uri: authUrl,
        verification_uri_complete: authUrl,
        expires_in: 300, // 5 minutes
    };
    claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthUri, authResponse);
    // Open browser
    try {
        claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthProgress, 'Opening browser for authentication...');
        const childProcess = await open(authUrl);
        childProcess.on('error', (error) => {
            console.error(`Failed to open browser:`, error);
            claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthProgress, 'Failed to open browser. Please navigate to the URL manually.');
        });
    }
    catch (err) {
        console.error(`Failed to open browser:`, err);
        claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthProgress, 'Failed to open browser. Please navigate to the URL manually.');
    }
    const loginCompletePromise = new Promise((resolve, reject) => {
        const server = http.createServer(async (req, res) => {
            try {
                if (!req.url || req.url.indexOf('/callback') === -1) {
                    res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL });
                    res.end();
                    const error = new Error('OAuth callback not received. Unexpected request: ' + req.url);
                    claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthError, error);
                    reject(error);
                    return;
                }
                const qs = new url.URL(req.url, `http://localhost:${port}`)
                    .searchParams;
                if (qs.get('error')) {
                    res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL });
                    res.end();
                    const errorCode = qs.get('error');
                    const errorDescription = qs.get('error_description') || 'No additional details provided';
                    const error = new Error(`Anthropic OAuth error: ${errorCode}. ${errorDescription}`);
                    claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthError, error);
                    reject(error);
                }
                else if (qs.get('state') !== state) {
                    res.end('State mismatch. Possible CSRF attack');
                    const error = new Error('OAuth state mismatch. Possible CSRF attack or browser session issue.');
                    claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthError, error);
                    reject(error);
                }
                else if (qs.get('code')) {
                    try {
                        claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthProgress, 'Exchanging authorization code for tokens...');
                        const credentials = await exchangeAuthorizationCode(qs.get('code'), codeVerifier, redirectUri);
                        // Cache the credentials
                        await cacheCredentials(credentials);
                        res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_SUCCESS_URL });
                        res.end();
                        claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthSuccess, 'Authentication successful!');
                        resolve();
                    }
                    catch (error) {
                        res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL });
                        res.end();
                        const err = new Error(`Failed to exchange authorization code for tokens: ${error}`);
                        claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthError, err);
                        reject(err);
                    }
                }
                else {
                    const error = new Error('No authorization code received from Anthropic OAuth. Please try authenticating again.');
                    claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthError, error);
                    reject(error);
                }
            }
            catch (e) {
                const error = new Error(`Unexpected error during OAuth authentication: ${e}`);
                claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthError, error);
                reject(error);
            }
            finally {
                server.close();
            }
        });
        server.listen(port, 'localhost', () => {
            console.log(`[Claude Code OAuth] Callback server listening on localhost:${port}`);
        });
        server.on('error', (err) => {
            const error = new Error(`OAuth callback server error: ${err}`);
            claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthError, error);
            reject(error);
        });
    });
    return {
        authUrl,
        loginCompletePromise,
    };
}
/**
 * Get Claude Code OAuth access token
 * Automatically triggers OAuth flow if no cached credentials exist
 */
export async function getClaudeCodeOAuthToken(config) {
    // Try to load cached credentials
    let credentials = await fetchCachedCredentials();
    if (credentials) {
        // Check if token is expired (with 5 minute buffer)
        const now = Date.now();
        const expiresAt = credentials.expires_at || 0;
        if (now < expiresAt - 300000) {
            // Token is still valid (5min buffer)
            console.log('[Claude Code OAuth] Using cached credentials');
            return credentials.access_token;
        }
        // Token is expired, try to refresh
        if (credentials.refresh_token) {
            try {
                console.log('[Claude Code OAuth] Refreshing expired token');
                claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthProgress, 'Refreshing expired token...');
                credentials = await refreshAccessToken(credentials.refresh_token);
                await cacheCredentials(credentials);
                console.log('[Claude Code OAuth] Token refreshed successfully');
                return credentials.access_token;
            }
            catch (error) {
                console.debug('[Claude Code OAuth] Token refresh failed:', error);
                // Clear invalid credentials
                await clearClaudeCodeCredentials();
            }
        }
    }
    // No valid cached credentials, start OAuth flow
    console.log('[Claude Code OAuth] No valid credentials found, starting auth flow');
    if (config && config.isBrowserLaunchSuppressed()) {
        throw new Error('Claude Code OAuth requires browser authentication. Please run without NO_BROWSER=true');
    }
    let isCancelled = false;
    // Set up cancellation listener
    const cancelHandler = () => {
        isCancelled = true;
    };
    claudeCodeOAuth2Events.once(ClaudeCodeOAuth2Event.AuthCancel, cancelHandler);
    try {
        const webLogin = await authWithClaudeCodeWeb();
        // Add 5-minute timeout
        const authTimeout = 5 * 60 * 1000;
        const timeoutPromise = new Promise((_, reject) => {
            setTimeout(() => {
                if (!isCancelled) {
                    const error = new Error('Authentication timed out after 5 minutes. Please try again.');
                    claudeCodeOAuth2Events.emit(ClaudeCodeOAuth2Event.AuthError, error);
                    reject(error);
                }
            }, authTimeout);
        });
        const cancelPromise = new Promise((_, reject) => {
            const checkCancelled = setInterval(() => {
                if (isCancelled) {
                    clearInterval(checkCancelled);
                    reject(new Error('Claude Code OAuth authentication was cancelled by user'));
                }
            }, 100);
        });
        await Promise.race([
            webLogin.loginCompletePromise,
            timeoutPromise,
            cancelPromise,
        ]);
        if (isCancelled) {
            throw new Error('Claude Code OAuth authentication was cancelled by user');
        }
        console.log('[Claude Code OAuth] Authentication successful!');
        // Fetch and return the new token
        credentials = await fetchCachedCredentials();
        if (!credentials) {
            throw new Error('Failed to load credentials after successful authentication');
        }
        return credentials.access_token;
    }
    finally {
        claudeCodeOAuth2Events.off(ClaudeCodeOAuth2Event.AuthCancel, cancelHandler);
    }
}
//# sourceMappingURL=claudeCodeOAuth2.js.map