/**
 * Gemini CLI OAuth flow (Google Cloud Code Assist)
 * Standard Gemini models only (gemini-2.0-flash, gemini-2.5-*)
 *
 * NOTE: This module uses Node.js http.createServer for the OAuth callback.
 * It is only intended for CLI use, not browser environments.
 */
import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js";
import { generatePKCE } from "./pkce.js";
let _createServer = null;
let _httpImportPromise = null;
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
    _httpImportPromise = import("node:http").then((m) => {
        _createServer = m.createServer;
    });
}
const decode = (s) => atob(s);
const CLIENT_ID = decode("NjgxMjU1ODA5Mzk1LW9vOGZ0Mm9wcmRybnA5ZTNhcWY2YXYzaG1kaWIxMzVqLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29t");
const CLIENT_SECRET = decode("R09DU1BYLTR1SGdNUG0tMW83U2stZ2VWNkN1NWNsWEZzeGw=");
const REDIRECT_URI = "http://localhost:8085/oauth2callback";
const SCOPES = [
    "https://www.googleapis.com/auth/cloud-platform",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/userinfo.profile",
];
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
const TOKEN_URL = "https://oauth2.googleapis.com/token";
const CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
/**
 * Start a local HTTP server to receive the OAuth callback
 */
async function getNodeCreateServer() {
    if (_createServer)
        return _createServer;
    if (_httpImportPromise) {
        await _httpImportPromise;
    }
    if (_createServer)
        return _createServer;
    throw new Error("Gemini CLI OAuth is only available in Node.js environments");
}
async function startCallbackServer() {
    const createServer = await getNodeCreateServer();
    return new Promise((resolve, reject) => {
        let settleWait;
        const waitForCodePromise = new Promise((resolveWait) => {
            let settled = false;
            settleWait = (value) => {
                if (settled)
                    return;
                settled = true;
                resolveWait(value);
            };
        });
        const server = createServer((req, res) => {
            const url = new URL(req.url || "", `http://localhost:8085`);
            if (url.pathname === "/oauth2callback") {
                const code = url.searchParams.get("code");
                const state = url.searchParams.get("state");
                const error = url.searchParams.get("error");
                if (error) {
                    res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
                    res.end(oauthErrorHtml("Google authentication did not complete.", `Error: ${error}`));
                    return;
                }
                if (code && state) {
                    res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
                    res.end(oauthSuccessHtml("Google authentication completed. You can close this window."));
                    settleWait?.({ code, state });
                }
                else {
                    res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
                    res.end(oauthErrorHtml("Missing code or state parameter."));
                }
            }
            else {
                res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
                res.end(oauthErrorHtml("Callback route not found."));
            }
        });
        server.on("error", (err) => {
            reject(err);
        });
        server.listen(8085, "127.0.0.1", () => {
            resolve({
                server,
                cancelWait: () => {
                    settleWait?.(null);
                },
                waitForCode: () => waitForCodePromise,
            });
        });
    });
}
/**
 * Parse redirect URL to extract code and state
 */
function parseRedirectUrl(input) {
    const value = input.trim();
    if (!value)
        return {};
    try {
        const url = new URL(value);
        return {
            code: url.searchParams.get("code") ?? undefined,
            state: url.searchParams.get("state") ?? undefined,
        };
    }
    catch {
        // Not a URL, return empty
        return {};
    }
}
// Tier IDs as used by the Cloud Code API
const TIER_FREE = "free-tier";
const TIER_LEGACY = "legacy-tier";
const TIER_STANDARD = "standard-tier";
/**
 * Wait helper for onboarding retries
 */
function wait(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
 * Get default tier from allowed tiers
 */
function getDefaultTier(allowedTiers) {
    if (!allowedTiers || allowedTiers.length === 0)
        return { id: TIER_LEGACY };
    const defaultTier = allowedTiers.find((t) => t.isDefault);
    return defaultTier ?? { id: TIER_LEGACY };
}
function isVpcScAffectedUser(payload) {
    if (!payload || typeof payload !== "object")
        return false;
    if (!("error" in payload))
        return false;
    const error = payload.error;
    if (!error?.details || !Array.isArray(error.details))
        return false;
    return error.details.some((detail) => detail.reason === "SECURITY_POLICY_VIOLATED");
}
/**
 * Poll a long-running operation until completion
 */
async function pollOperation(operationName, headers, onProgress) {
    let attempt = 0;
    while (true) {
        if (attempt > 0) {
            onProgress?.(`Waiting for project provisioning (attempt ${attempt + 1})...`);
            await wait(5000);
        }
        const response = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal/${operationName}`, {
            method: "GET",
            headers,
        });
        if (!response.ok) {
            throw new Error(`Failed to poll operation: ${response.status} ${response.statusText}`);
        }
        const data = (await response.json());
        if (data.done) {
            return data;
        }
        attempt += 1;
    }
}
/**
 * Discover or provision a Google Cloud project for the user
 */
async function discoverProject(accessToken, onProgress) {
    // Check for user-provided project ID via environment variable
    const envProjectId = process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID;
    const headers = {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
        "User-Agent": "google-api-nodejs-client/9.15.1",
        "X-Goog-Api-Client": "gl-node/22.17.0",
    };
    // Try to load existing project via loadCodeAssist
    onProgress?.("Checking for existing Cloud Code Assist project...");
    const loadResponse = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`, {
        method: "POST",
        headers,
        body: JSON.stringify({
            cloudaicompanionProject: envProjectId,
            metadata: {
                ideType: "IDE_UNSPECIFIED",
                platform: "PLATFORM_UNSPECIFIED",
                pluginType: "GEMINI",
                duetProject: envProjectId,
            },
        }),
    });
    let data;
    if (!loadResponse.ok) {
        let errorPayload;
        try {
            errorPayload = await loadResponse.clone().json();
        }
        catch {
            errorPayload = undefined;
        }
        if (isVpcScAffectedUser(errorPayload)) {
            data = { currentTier: { id: TIER_STANDARD } };
        }
        else {
            const errorText = await loadResponse.text();
            throw new Error(`loadCodeAssist failed: ${loadResponse.status} ${loadResponse.statusText}: ${errorText}`);
        }
    }
    else {
        data = (await loadResponse.json());
    }
    // If user already has a current tier and project, use it
    if (data.currentTier) {
        if (data.cloudaicompanionProject) {
            return data.cloudaicompanionProject;
        }
        // User has a tier but no managed project - they need to provide one via env var
        if (envProjectId) {
            return envProjectId;
        }
        throw new Error("This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " +
            "See https://goo.gle/gemini-cli-auth-docs#workspace-gca");
    }
    // User needs to be onboarded - get the default tier
    const tier = getDefaultTier(data.allowedTiers);
    const tierId = tier?.id ?? TIER_FREE;
    if (tierId !== TIER_FREE && !envProjectId) {
        throw new Error("This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " +
            "See https://goo.gle/gemini-cli-auth-docs#workspace-gca");
    }
    onProgress?.("Provisioning Cloud Code Assist project (this may take a moment)...");
    // Build onboard request - for free tier, don't include project ID (Google provisions one)
    // For other tiers, include the user's project ID if available
    const onboardBody = {
        tierId,
        metadata: {
            ideType: "IDE_UNSPECIFIED",
            platform: "PLATFORM_UNSPECIFIED",
            pluginType: "GEMINI",
        },
    };
    if (tierId !== TIER_FREE && envProjectId) {
        onboardBody.cloudaicompanionProject = envProjectId;
        onboardBody.metadata.duetProject = envProjectId;
    }
    // Start onboarding - this returns a long-running operation
    const onboardResponse = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal:onboardUser`, {
        method: "POST",
        headers,
        body: JSON.stringify(onboardBody),
    });
    if (!onboardResponse.ok) {
        const errorText = await onboardResponse.text();
        throw new Error(`onboardUser failed: ${onboardResponse.status} ${onboardResponse.statusText}: ${errorText}`);
    }
    let lroData = (await onboardResponse.json());
    // If the operation isn't done yet, poll until completion
    if (!lroData.done && lroData.name) {
        lroData = await pollOperation(lroData.name, headers, onProgress);
    }
    // Try to get project ID from the response
    const projectId = lroData.response?.cloudaicompanionProject?.id;
    if (projectId) {
        return projectId;
    }
    // If no project ID from onboarding, fall back to env var
    if (envProjectId) {
        return envProjectId;
    }
    throw new Error("Could not discover or provision a Google Cloud project. " +
        "Try setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " +
        "See https://goo.gle/gemini-cli-auth-docs#workspace-gca");
}
/**
 * Get user email from the access token
 */
async function getUserEmail(accessToken) {
    try {
        const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
            headers: {
                Authorization: `Bearer ${accessToken}`,
            },
        });
        if (response.ok) {
            const data = (await response.json());
            return data.email;
        }
    }
    catch {
        // Ignore errors, email is optional
    }
    return undefined;
}
/**
 * Refresh Google Cloud Code Assist token
 */
export async function refreshGoogleCloudToken(refreshToken, projectId) {
    const response = await fetch(TOKEN_URL, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
            client_id: CLIENT_ID,
            client_secret: CLIENT_SECRET,
            refresh_token: refreshToken,
            grant_type: "refresh_token",
        }),
    });
    if (!response.ok) {
        const error = await response.text();
        throw new Error(`Google Cloud token refresh failed: ${error}`);
    }
    const data = (await response.json());
    return {
        refresh: data.refresh_token || refreshToken,
        access: data.access_token,
        expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
        projectId,
    };
}
/**
 * Login with Gemini CLI (Google Cloud Code Assist) OAuth
 *
 * @param onAuth - Callback with URL and optional instructions
 * @param onProgress - Optional progress callback
 * @param onManualCodeInput - Optional promise that resolves with user-pasted redirect URL.
 *                            Races with browser callback - whichever completes first wins.
 */
export async function loginGeminiCli(onAuth, onProgress, onManualCodeInput) {
    const { verifier, challenge } = await generatePKCE();
    // Start local server for callback
    onProgress?.("Starting local server for OAuth callback...");
    const server = await startCallbackServer();
    let code;
    try {
        // Build authorization URL
        const authParams = new URLSearchParams({
            client_id: CLIENT_ID,
            response_type: "code",
            redirect_uri: REDIRECT_URI,
            scope: SCOPES.join(" "),
            code_challenge: challenge,
            code_challenge_method: "S256",
            state: verifier,
            access_type: "offline",
            prompt: "consent",
        });
        const authUrl = `${AUTH_URL}?${authParams.toString()}`;
        // Notify caller with URL to open
        onAuth({
            url: authUrl,
            instructions: "Complete the sign-in in your browser.",
        });
        // Wait for the callback, racing with manual input if provided
        onProgress?.("Waiting for OAuth callback...");
        if (onManualCodeInput) {
            // Race between browser callback and manual input
            let manualInput;
            let manualError;
            const manualPromise = onManualCodeInput()
                .then((input) => {
                manualInput = input;
                server.cancelWait();
            })
                .catch((err) => {
                manualError = err instanceof Error ? err : new Error(String(err));
                server.cancelWait();
            });
            const result = await server.waitForCode();
            // If manual input was cancelled, throw that error
            if (manualError) {
                throw manualError;
            }
            if (result?.code) {
                // Browser callback won - verify state
                if (result.state !== verifier) {
                    throw new Error("OAuth state mismatch - possible CSRF attack");
                }
                code = result.code;
            }
            else if (manualInput) {
                // Manual input won
                const parsed = parseRedirectUrl(manualInput);
                if (parsed.state && parsed.state !== verifier) {
                    throw new Error("OAuth state mismatch - possible CSRF attack");
                }
                code = parsed.code;
            }
            // If still no code, wait for manual promise and try that
            if (!code) {
                await manualPromise;
                if (manualError) {
                    throw manualError;
                }
                if (manualInput) {
                    const parsed = parseRedirectUrl(manualInput);
                    if (parsed.state && parsed.state !== verifier) {
                        throw new Error("OAuth state mismatch - possible CSRF attack");
                    }
                    code = parsed.code;
                }
            }
        }
        else {
            // Original flow: just wait for callback
            const result = await server.waitForCode();
            if (result?.code) {
                if (result.state !== verifier) {
                    throw new Error("OAuth state mismatch - possible CSRF attack");
                }
                code = result.code;
            }
        }
        if (!code) {
            throw new Error("No authorization code received");
        }
        // Exchange code for tokens
        onProgress?.("Exchanging authorization code for tokens...");
        const tokenResponse = await fetch(TOKEN_URL, {
            method: "POST",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
            },
            body: new URLSearchParams({
                client_id: CLIENT_ID,
                client_secret: CLIENT_SECRET,
                code,
                grant_type: "authorization_code",
                redirect_uri: REDIRECT_URI,
                code_verifier: verifier,
            }),
        });
        if (!tokenResponse.ok) {
            const error = await tokenResponse.text();
            throw new Error(`Token exchange failed: ${error}`);
        }
        const tokenData = (await tokenResponse.json());
        if (!tokenData.refresh_token) {
            throw new Error("No refresh token received. Please try again.");
        }
        // Get user email
        onProgress?.("Getting user info...");
        const email = await getUserEmail(tokenData.access_token);
        // Discover project
        const projectId = await discoverProject(tokenData.access_token, onProgress);
        // Calculate expiry time (current time + expires_in seconds - 5 min buffer)
        const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;
        const credentials = {
            refresh: tokenData.refresh_token,
            access: tokenData.access_token,
            expires: expiresAt,
            projectId,
            email,
        };
        return credentials;
    }
    finally {
        server.server.close();
    }
}
export const geminiCliOAuthProvider = {
    id: "google-gemini-cli",
    name: "Google Cloud Code Assist (Gemini CLI)",
    usesCallbackServer: true,
    async login(callbacks) {
        return loginGeminiCli(callbacks.onAuth, callbacks.onProgress, callbacks.onManualCodeInput);
    },
    async refreshToken(credentials) {
        const creds = credentials;
        if (!creds.projectId) {
            throw new Error("Google Cloud credentials missing projectId");
        }
        return refreshGoogleCloudToken(creds.refresh, creds.projectId);
    },
    getApiKey(credentials) {
        const creds = credentials;
        return JSON.stringify({ token: creds.access, projectId: creds.projectId });
    },
};
//# sourceMappingURL=google-gemini-cli.js.map