import { createHash, randomBytes } from 'node:crypto';
import { spawn } from 'node:child_process';
import { createServer } from 'node:http';
export const OPENAI_CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
export const OPENAI_AUTH_ISSUER = 'https://auth.openai.com';
export const OPENAI_CODEX_RESPONSES_ENDPOINT = 'https://chatgpt.com/backend-api/codex/responses';
export const OPENAI_OAUTH_CALLBACK_PATH = '/auth/callback';
export const DEFAULT_OPENAI_OAUTH_PORT = 1455;
/**
 * OpenAI account sign-in service. It owns OAuth URL construction, callback
 * handling, token exchange/refresh, credential creation, and browser launch.
 */
export class OpenAiOAuthService {
    static generatePkceCodes() {
        const verifier = OpenAiOAuthService.base64UrlEncode(randomBytes(32));
        const challenge = OpenAiOAuthService.base64UrlEncode(createHash('sha256').update(verifier).digest());
        return { verifier, challenge };
    }
    static generateState() {
        return OpenAiOAuthService.base64UrlEncode(randomBytes(32));
    }
    static buildAuthorizeUrl(args) {
        const params = new URLSearchParams({
            response_type: 'code',
            client_id: OPENAI_CODEX_CLIENT_ID,
            redirect_uri: args.redirectUri,
            scope: 'openid profile email offline_access',
            code_challenge: args.pkce.challenge,
            code_challenge_method: 'S256',
            id_token_add_organizations: 'true',
            codex_cli_simplified_flow: 'true',
            state: args.state,
            originator: args.originator ?? 'heddle',
        });
        return `${OPENAI_AUTH_ISSUER}/oauth/authorize?${params.toString()}`;
    }
    static async runBrowserLogin(options = {}) {
        const port = options.port ?? DEFAULT_OPENAI_OAUTH_PORT;
        const pkce = OpenAiOAuthService.generatePkceCodes();
        const state = OpenAiOAuthService.generateState();
        const callback = await OpenAiOAuthService.startCallbackServer({
            port,
            state,
            pkce,
            timeoutMs: options.timeoutMs,
            fetchImpl: options.fetchImpl,
        });
        const authUrl = OpenAiOAuthService.buildAuthorizeUrl({
            redirectUri: callback.redirectUri,
            pkce,
            state,
        });
        try {
            options.onAuthorizeUrl?.(authUrl);
            if (options.openBrowser !== false) {
                await (options.openUrl ?? OpenAiOAuthService.openUrlInBrowser)(authUrl);
            }
            const tokens = await callback.tokens;
            return OpenAiOAuthService.createCredential(tokens);
        }
        finally {
            await callback.close();
        }
    }
    static async refreshCredential(credential, options = {}) {
        const tokens = await OpenAiOAuthService.refreshToken({
            refreshToken: credential.refreshToken,
            fetchImpl: options.fetchImpl,
        });
        return {
            ...OpenAiOAuthService.createCredential(tokens),
            createdAt: credential.createdAt,
            accountId: OpenAiOAuthService.extractAccountId(tokens) ?? credential.accountId,
        };
    }
    static async exchangeCode(args) {
        const fetcher = args.fetchImpl ?? fetch;
        const response = await fetcher(`${OPENAI_AUTH_ISSUER}/oauth/token`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
                grant_type: 'authorization_code',
                code: args.code,
                redirect_uri: args.redirectUri,
                client_id: OPENAI_CODEX_CLIENT_ID,
                code_verifier: args.codeVerifier,
            }).toString(),
        });
        if (!response.ok) {
            throw new Error(`OpenAI OAuth token exchange failed: ${response.status}`);
        }
        return OpenAiOAuthService.normalizeTokenResponse(await response.json());
    }
    static async refreshToken(options) {
        const fetcher = options.fetchImpl ?? fetch;
        const response = await fetcher(`${OPENAI_AUTH_ISSUER}/oauth/token`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
                grant_type: 'refresh_token',
                refresh_token: options.refreshToken,
                client_id: OPENAI_CODEX_CLIENT_ID,
            }).toString(),
        });
        if (!response.ok) {
            throw new Error(`OpenAI OAuth token refresh failed: ${response.status}`);
        }
        return OpenAiOAuthService.normalizeTokenResponse(await response.json());
    }
    static createCredential(tokens, now = Date.now()) {
        const timestamp = new Date(now).toISOString();
        return {
            type: 'oauth',
            provider: 'openai',
            accessToken: tokens.access_token,
            refreshToken: tokens.refresh_token,
            expiresAt: now + (tokens.expires_in ?? 3600) * 1000,
            accountId: OpenAiOAuthService.extractAccountId(tokens),
            createdAt: timestamp,
            updatedAt: timestamp,
            label: 'ChatGPT/Codex OAuth',
        };
    }
    static parseJwtClaims(token) {
        const parts = token.split('.');
        if (parts.length !== 3) {
            return undefined;
        }
        try {
            return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
        }
        catch {
            return undefined;
        }
    }
    static extractAccountId(tokens) {
        const idClaims = tokens.id_token ? OpenAiOAuthService.parseJwtClaims(tokens.id_token) : undefined;
        const idAccount = idClaims ? OpenAiOAuthService.extractAccountIdFromClaims(idClaims) : undefined;
        if (idAccount) {
            return idAccount;
        }
        const accessClaims = OpenAiOAuthService.parseJwtClaims(tokens.access_token);
        return accessClaims ? OpenAiOAuthService.extractAccountIdFromClaims(accessClaims) : undefined;
    }
    static extractAccountIdFromClaims(claims) {
        return (claims.chatgpt_account_id
            ?? claims['https://api.openai.com/auth']?.chatgpt_account_id
            ?? claims.organizations?.find((organization) => organization.id)?.id);
    }
    static buildOpenUrlCommand(url, platform = process.platform) {
        if (platform === 'darwin') {
            return { command: 'open', args: [url] };
        }
        if (platform === 'win32') {
            const script = `Start-Process -FilePath '${OpenAiOAuthService.escapePowerShellSingleQuotedString(url)}'`;
            return {
                command: 'powershell.exe',
                args: ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')],
            };
        }
        return { command: 'xdg-open', args: [url] };
    }
    static async startCallbackServer(args) {
        let settled = false;
        let resolveTokens;
        let rejectTokens;
        const tokens = new Promise((resolve, reject) => {
            resolveTokens = resolve;
            rejectTokens = reject;
        });
        const timeout = setTimeout(() => {
            if (!settled) {
                settled = true;
                rejectTokens(new Error('OpenAI OAuth callback timed out'));
            }
        }, args.timeoutMs ?? 5 * 60 * 1000);
        const server = createServer((req, res) => {
            const url = new URL(req.url ?? '/', `http://localhost:${args.port}`);
            if (url.pathname !== OPENAI_OAUTH_CALLBACK_PATH) {
                res.writeHead(404);
                res.end('Not found');
                return;
            }
            const error = url.searchParams.get('error');
            if (error) {
                settled = true;
                const message = url.searchParams.get('error_description') ?? error;
                rejectTokens(new Error(message));
                res.writeHead(200, { 'Content-Type': 'text/html' });
                res.end(OpenAiOAuthService.renderCallbackHtml('Authorization failed', message));
                return;
            }
            const code = url.searchParams.get('code');
            const state = url.searchParams.get('state');
            if (!code) {
                settled = true;
                rejectTokens(new Error('OpenAI OAuth callback did not include a code'));
                res.writeHead(400, { 'Content-Type': 'text/html' });
                res.end(OpenAiOAuthService.renderCallbackHtml('Authorization failed', 'Missing authorization code.'));
                return;
            }
            if (state !== args.state) {
                settled = true;
                rejectTokens(new Error('OpenAI OAuth callback state did not match'));
                res.writeHead(400, { 'Content-Type': 'text/html' });
                res.end(OpenAiOAuthService.renderCallbackHtml('Authorization failed', 'Invalid OAuth state.'));
                return;
            }
            settled = true;
            const redirectUri = `http://localhost:${OpenAiOAuthService.getServerPort(server) ?? args.port}${OPENAI_OAUTH_CALLBACK_PATH}`;
            void OpenAiOAuthService.exchangeCode({
                code,
                redirectUri,
                codeVerifier: args.pkce.verifier,
                fetchImpl: args.fetchImpl,
            }).then(resolveTokens, rejectTokens);
            res.writeHead(200, { 'Content-Type': 'text/html' });
            res.end(OpenAiOAuthService.renderCallbackHtml('Authorization successful', 'You can close this window and return to Heddle.'));
        });
        await new Promise((resolve, reject) => {
            server.once('error', reject);
            server.listen(args.port, '127.0.0.1', () => {
                server.off('error', reject);
                resolve();
            });
        });
        const actualPort = OpenAiOAuthService.getServerPort(server) ?? args.port;
        const redirectUri = `http://localhost:${actualPort}${OPENAI_OAUTH_CALLBACK_PATH}`;
        return {
            redirectUri,
            tokens: tokens.finally(() => clearTimeout(timeout)),
            close: async () => {
                clearTimeout(timeout);
                await new Promise((resolve) => {
                    if (!server.listening) {
                        resolve();
                        return;
                    }
                    server.close(() => resolve());
                });
            },
        };
    }
    static normalizeTokenResponse(input) {
        if (!input || typeof input !== 'object' || Array.isArray(input)) {
            throw new Error('OpenAI OAuth token response was not an object');
        }
        const candidate = input;
        if (typeof candidate.access_token !== 'string' || typeof candidate.refresh_token !== 'string') {
            throw new Error('OpenAI OAuth token response did not include access and refresh tokens');
        }
        return {
            id_token: typeof candidate.id_token === 'string' ? candidate.id_token : undefined,
            access_token: candidate.access_token,
            refresh_token: candidate.refresh_token,
            expires_in: typeof candidate.expires_in === 'number' && Number.isFinite(candidate.expires_in) ?
                candidate.expires_in
                : undefined,
        };
    }
    static base64UrlEncode(value) {
        return value.toString('base64url');
    }
    static getServerPort(server) {
        const address = server.address();
        return typeof address === 'object' && address ? address.port : undefined;
    }
    static async openUrlInBrowser(url) {
        const { command, args } = OpenAiOAuthService.buildOpenUrlCommand(url, process.platform);
        const child = spawn(command, args, {
            detached: true,
            stdio: 'ignore',
        });
        child.unref();
    }
    static escapePowerShellSingleQuotedString(value) {
        return value.replaceAll("'", "''");
    }
    static renderCallbackHtml(title, message) {
        return `<!doctype html>
<html>
  <head><title>Heddle OpenAI OAuth</title></head>
  <body>
    <h1>${OpenAiOAuthService.escapeHtml(title)}</h1>
    <p>${OpenAiOAuthService.escapeHtml(message)}</p>
  </body>
</html>`;
    }
    static escapeHtml(value) {
        return value
            .replaceAll('&', '&amp;')
            .replaceAll('<', '&lt;')
            .replaceAll('>', '&gt;')
            .replaceAll('"', '&quot;')
            .replaceAll("'", '&#39;');
    }
}
//# sourceMappingURL=openai-oauth.js.map