/**
 * Google OAuth authentication for CLI
 *
 * Implements the localhost HTTP server callback pattern for Google SSO.
 * Opens the user's browser to Google's consent screen, captures the
 * authorization code via redirect to localhost, and exchanges it for tokens.
 */
import * as crypto from "crypto";
import * as http from "http";
import * as net from "net";
import { formatNetworkError } from "../../../utils/fetch.js";
/**
 * Google OAuth client credentials (Desktop app type).
 * For Desktop app type, the client secret is not truly secret -- this is
 * standard practice (same as firebase-tools, gcloud CLI, etc.).
 * Used for the localhost callback flow.
 */
export const GOOGLE_OAUTH_CLIENT_ID = "199991289749-otcibgl0kp53qq2tn46n08iutm62pq6h.apps.googleusercontent.com";
export const GOOGLE_OAUTH_CLIENT_SECRET = "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";
/**
 * Google OAuth Web Application client ID (for headless/no-localhost flow).
 * The client secret is kept server-side on noriskillsets.dev.
 * Used when --no-localhost flag is specified.
 */
export const GOOGLE_OAUTH_WEB_CLIENT_ID = "199991289749-qneu4fa6jn6q5ss256ogj6ldg90b0h6a.apps.googleusercontent.com";
/**
 * Validate that OAuth credentials have been configured for localhost flow.
 * Throws if still using placeholder values.
 *
 * @throws Error if credentials are still placeholders
 */
export const validateOAuthCredentials = () => {
    if (GOOGLE_OAUTH_CLIENT_ID.startsWith("PLACEHOLDER") ||
        GOOGLE_OAUTH_CLIENT_SECRET.startsWith("PLACEHOLDER")) {
        throw new Error("Google OAuth credentials are not configured. " +
            "Replace placeholder values in googleAuth.ts " +
            "with real values from the Google Cloud Console.");
    }
};
/**
 * Validate that Web OAuth client ID has been configured for headless flow.
 * Throws if still using placeholder value.
 *
 * @throws Error if web client ID is still a placeholder
 */
export const validateWebOAuthCredentials = () => {
    if (GOOGLE_OAUTH_WEB_CLIENT_ID.startsWith("PLACEHOLDER")) {
        throw new Error("Google OAuth Web Application client ID is not configured. " +
            "Replace placeholder value in googleAuth.ts " +
            "with the Web Application client ID from the Google Cloud Console.");
    }
};
/** Port range for the local OAuth callback server */
const START_PORT = 9876;
const MAX_PORT_ATTEMPTS = 10;
/** Timeout for waiting for the OAuth callback (ms) */
const AUTH_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
/** Default warning delay before timeout (ms) */
export const AUTH_WARNING_MS = 60 * 1000; // 1 minute
/** Google OAuth endpoints */
const GOOGLE_AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
const GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
/**
 * Find an available port starting from startPort
 *
 * @param args - Configuration arguments
 * @param args.startPort - Port to start checking from
 * @param args.maxAttempts - Maximum number of ports to try
 *
 * @throws Error if no port is available
 *
 * @returns An available port number
 */
export const findAvailablePort = async (args) => {
    const startPort = args.startPort ?? START_PORT;
    const maxAttempts = args.maxAttempts ?? MAX_PORT_ATTEMPTS;
    for (let i = 0; i < maxAttempts; i++) {
        const port = startPort + i;
        const available = await new Promise((resolve) => {
            const server = net.createServer();
            server.once("error", () => resolve(false));
            server.listen(port, () => {
                server.close(() => resolve(true));
            });
        });
        if (available) {
            return port;
        }
    }
    throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`);
};
/**
 * Build the Google OAuth authorization URL
 *
 * @param args - Configuration arguments
 * @param args.clientId - Google OAuth client ID
 * @param args.redirectUri - Redirect URI (http://localhost:{port})
 * @param args.state - CSRF protection nonce
 *
 * @returns The full Google OAuth authorization URL
 */
export const getGoogleAuthUrl = (args) => {
    const { clientId, redirectUri, state } = args;
    const params = new URLSearchParams({
        client_id: clientId,
        redirect_uri: redirectUri,
        response_type: "code",
        scope: "openid email profile",
        state,
        access_type: "offline",
        prompt: "consent",
    });
    return `${GOOGLE_AUTH_ENDPOINT}?${params.toString()}`;
};
/** HTML page shown in the browser after authentication completes */
const SUCCESS_HTML = `<!DOCTYPE html>
<html><head><title>Authentication Successful</title></head>
<body style="font-family:sans-serif;text-align:center;padding:40px">
<h2>Authentication successful!</h2>
<p>You can close this tab and return to your terminal.</p>
</body></html>`;
const ERROR_HTML = `<!DOCTYPE html>
<html><head><title>Authentication Failed</title></head>
<body style="font-family:sans-serif;text-align:center;padding:40px">
<h2>Authentication failed</h2>
<p>Please return to your terminal for more information.</p>
</body></html>`;
/**
 * Start a temporary HTTP server to capture the OAuth callback
 *
 * @param args - Configuration arguments
 * @param args.port - Port to listen on
 * @param args.expectedState - Expected state parameter for CSRF validation
 * @param args.timeoutMs - Timeout in milliseconds
 * @param args.warningMs - Time before timeout to call onTimeoutWarning (optional)
 * @param args.onTimeoutWarning - Callback to invoke when warning threshold reached (optional)
 *
 * @throws Error on timeout, user cancellation, or CSRF mismatch
 *
 * @returns Promise resolving with the authorization code and server reference
 */
export const startAuthServer = (args) => {
    const { port, expectedState, timeoutMs, warningMs, onTimeoutWarning } = args;
    const timeout = timeoutMs ?? AUTH_TIMEOUT_MS;
    return new Promise((resolve, reject) => {
        let timeoutHandle = null;
        let warningHandle = null;
        let settled = false;
        const cleanup = () => {
            if (timeoutHandle != null) {
                clearTimeout(timeoutHandle);
                timeoutHandle = null;
            }
            if (warningHandle != null) {
                clearTimeout(warningHandle);
                warningHandle = null;
            }
        };
        const safeReject = (err) => {
            if (settled)
                return;
            settled = true;
            cleanup();
            server.close();
            reject(err);
        };
        const server = http.createServer((req, res) => {
            if (settled) {
                res.writeHead(200, { "Content-Type": "text/html" });
                res.end(ERROR_HTML);
                return;
            }
            const url = new URL(req.url ?? "/", `http://localhost:${port}`);
            // Check for error from Google (e.g., user denied consent)
            const error = url.searchParams.get("error");
            if (error != null) {
                res.writeHead(200, { "Content-Type": "text/html" });
                res.end(ERROR_HTML);
                safeReject(new Error(`Authentication denied: ${error}`));
                return;
            }
            // Verify CSRF state parameter
            const state = url.searchParams.get("state");
            if (state !== expectedState) {
                res.writeHead(200, { "Content-Type": "text/html" });
                res.end(ERROR_HTML);
                safeReject(new Error("State parameter mismatch -- possible CSRF attack."));
                return;
            }
            // Extract authorization code
            const code = url.searchParams.get("code");
            if (code == null) {
                res.writeHead(200, { "Content-Type": "text/html" });
                res.end(ERROR_HTML);
                safeReject(new Error("No authorization code received from Google."));
                return;
            }
            // Success -- caller is responsible for closing the server
            settled = true;
            cleanup();
            res.writeHead(200, { "Content-Type": "text/html" });
            res.end(SUCCESS_HTML);
            resolve({ code, server });
        });
        server.listen(port, () => {
            // Set up warning callback if provided
            if (warningMs != null && onTimeoutWarning != null) {
                warningHandle = setTimeout(() => {
                    if (!settled) {
                        onTimeoutWarning();
                    }
                }, warningMs);
            }
            // Set up timeout
            timeoutHandle = setTimeout(() => {
                safeReject(new Error("Authentication timed out. No response received from the browser."));
            }, timeout);
        });
    });
};
/**
 * Exchange an authorization code for Google tokens
 *
 * @param args - Configuration arguments
 * @param args.code - Authorization code from OAuth callback
 * @param args.clientId - Google OAuth client ID
 * @param args.clientSecret - Google OAuth client secret
 * @param args.redirectUri - Redirect URI used in the auth request
 *
 * @throws Error if token exchange fails
 *
 * @returns Google tokens including id_token
 */
export const exchangeCodeForTokens = async (args) => {
    const { code, clientId, clientSecret, redirectUri } = args;
    const body = new URLSearchParams({
        code,
        client_id: clientId,
        client_secret: clientSecret,
        redirect_uri: redirectUri,
        grant_type: "authorization_code",
    }).toString();
    let response;
    try {
        response = await fetch(GOOGLE_TOKEN_ENDPOINT, {
            method: "POST",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
            },
            body,
        });
    }
    catch (err) {
        // Network errors from fetch - wrap with helpful message
        if (err instanceof Error) {
            const networkError = formatNetworkError({
                error: err,
                url: GOOGLE_TOKEN_ENDPOINT,
            });
            throw new Error(`Google token exchange failed: ${networkError.message}`);
        }
        throw err;
    }
    const data = (await response.json());
    if (!response.ok) {
        const errorMsg = data.error_description ??
            data.error ??
            "Token exchange failed";
        throw new Error(`Google token exchange failed: ${errorMsg}`);
    }
    return {
        idToken: data.id_token,
        accessToken: data.access_token,
    };
};
/**
 * Generate a cryptographic nonce for CSRF protection
 *
 * @returns A random hex string
 */
export const generateState = () => {
    return crypto.randomBytes(16).toString("hex");
};
/**
 * Detect if running in a headless or SSH environment
 *
 * Checks for common SSH environment variables that indicate the session
 * is running over SSH, where a local browser cannot be opened.
 *
 * @returns true if running in an SSH or headless environment
 */
export const isHeadlessEnvironment = () => {
    return (process.env.SSH_TTY != null ||
        process.env.SSH_CONNECTION != null ||
        process.env.SSH_CLIENT != null);
};
//# sourceMappingURL=googleAuth.js.map