/**
 * Gemini CLI Workspace Credential Helper
 *
 * Piggybacks on the Google Workspace extension for Gemini CLI
 * (https://github.com/gemini-cli-extensions/workspace) for Google OAuth
 * credential acquisition and refresh.
 *
 * **Capture** reads the extension's file-storage token file (written when
 * the user runs the extension with `GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE=true`)
 * and imports the tokens into Crewly's own encrypted store.
 *
 * **Refresh** POSTs the stored `refresh_token` to the extension's Cloud
 * Function `/refreshToken` endpoint — no client_secret needed on our side.
 *
 * After capture, Crewly owns the tokens; the extension's own state is not
 * depended on for day-to-day operation.
 *
 * @module services/credential/helpers/gemini-cli-workspace.helper
 */
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import crypto from 'crypto';
import { CredentialRevokedError, } from '../../../types/credential.types.js';
import { GEMINI_CLI_CLOUD_FUNCTION_URL, GEMINI_CLI_REFRESH_PATH, GOOGLE_OAUTH_CLIENT_ID, } from '../../../config/oauth.config.js';
import { fetchGoogleAccountEmail, } from '../../../utils/google-userinfo.utils.js';
// ============================================================================
// Constants — extension-specific file layout and refresh tuning
// (github.com/gemini-cli-extensions/workspace)
// ============================================================================
const DEFAULT_EXTENSION_PATH = path.join(os.homedir(), '.gemini', 'extensions', 'google-workspace');
const TOKEN_FILENAME = 'gemini-cli-workspace-token.json';
const MASTER_KEY_FILENAME = '.gemini-cli-workspace-master-key';
const MAIN_ACCOUNT_KEY = 'main-account';
const SALT_SUFFIX = '-gemini-cli-workspace';
/** Refresh if the current access token expires within this window. */
const DEFAULT_EXPIRY_BUFFER_MS = 60_000;
/** Minimum gap between refresh attempts for the same credential. */
const REFRESH_COOLDOWN_MS = 30_000;
// ============================================================================
// Helper
// ============================================================================
/**
 * Credential helper that reads tokens from the gemini-cli-workspace extension
 * and refreshes them via the extension's Cloud Function.
 */
export class GeminiCliWorkspaceHelper {
    name = 'gemini-cli-workspace';
    extensionPath;
    cloudFunctionUrl;
    refreshPath;
    clientId;
    expiryBufferMs;
    fetchFn;
    store;
    /** Per-credential refresh in-flight promises (serializes concurrent refreshes). */
    refreshInFlight = new Map();
    /** Per-credential last refresh attempt timestamp (for cooldown). */
    lastRefreshAttempt = new Map();
    constructor(store, config) {
        this.store = store;
        this.extensionPath = config?.extensionPath ?? DEFAULT_EXTENSION_PATH;
        this.cloudFunctionUrl =
            config?.cloudFunctionUrl ?? GEMINI_CLI_CLOUD_FUNCTION_URL;
        this.refreshPath = config?.refreshPath ?? GEMINI_CLI_REFRESH_PATH;
        this.clientId = config?.clientId ?? GOOGLE_OAUTH_CLIENT_ID;
        this.expiryBufferMs = config?.expiryBufferMs ?? DEFAULT_EXPIRY_BUFFER_MS;
        this.fetchFn =
            config?.fetch ??
                ((url, init) => fetch(url, init));
    }
    // ------------------------------------------------------------------
    //  Capture
    // ------------------------------------------------------------------
    /**
     * Read the extension's current token file and return its contents as a
     * `GoogleOAuthPayload` ready to persist in Crewly's store.
     *
     * The user must have completed extension login with
     * `GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE=true` before calling this.
     */
    async captureFromFile() {
        const tokenPath = path.join(this.extensionPath, TOKEN_FILENAME);
        const masterKeyPath = path.join(this.extensionPath, MASTER_KEY_FILENAME);
        // Surface a user-friendly error if login hasn't happened yet
        try {
            await fs.access(tokenPath);
        }
        catch {
            throw new Error(`Gemini CLI Workspace token file not found at ${tokenPath}. ` +
                `Please run 'GEMINI_CLI_WORKSPACE_FORCE_FILE_STORAGE=true gemini' ` +
                `and complete Google sign-in first, then retry.`);
        }
        try {
            await fs.access(masterKeyPath);
        }
        catch {
            throw new Error(`Gemini CLI Workspace master key file not found at ${masterKeyPath}. ` +
                `The extension should create this on first login.`);
        }
        const encryptedText = await fs.readFile(tokenPath, 'utf8');
        const masterKey = await fs.readFile(masterKeyPath);
        // Derive the extension's encryption key (scrypt with hostname-username salt).
        // Must match `file-token-storage.ts` exactly.
        const salt = `${os.hostname()}-${os.userInfo().username}${SALT_SUFFIX}`;
        const encryptionKey = crypto.scryptSync(masterKey, salt, 32);
        const decrypted = this.decryptExtensionFormat(encryptedText, encryptionKey);
        const parsed = JSON.parse(decrypted);
        const entry = parsed[MAIN_ACCOUNT_KEY];
        if (!entry) {
            throw new Error(`Extension token file does not contain a '${MAIN_ACCOUNT_KEY}' entry. ` +
                `Did login complete successfully?`);
        }
        const { accessToken, refreshToken, tokenType, scope, expiresAt } = entry.token;
        if (!accessToken || !refreshToken) {
            throw new Error(`Extension token is missing accessToken or refreshToken — login may be incomplete.`);
        }
        const scopes = typeof scope === 'string' ? scope.split(' ').filter(Boolean) : [];
        const accountEmail = await fetchGoogleAccountEmail(accessToken, this.fetchFn);
        return {
            type: 'google-oauth',
            accessToken,
            refreshToken,
            tokenType: tokenType ?? 'Bearer',
            expiresAt: expiresAt ?? Date.now() + 3600_000,
            scopes,
            accountEmail,
            clientId: this.clientId,
        };
    }
    /**
     * Remove the extension's token file. Call after `captureFromFile()` to
     * prepare for the next account's login (the extension otherwise returns
     * cached credentials if scopes match, skipping the login prompt).
     * Leaves the master.key file untouched so existing ciphertexts remain
     * decryptable if needed.
     */
    async clearExtensionFile() {
        const tokenPath = path.join(this.extensionPath, TOKEN_FILENAME);
        try {
            await fs.unlink(tokenPath);
        }
        catch (err) {
            if (err.code === 'ENOENT')
                return;
            throw err;
        }
    }
    // ------------------------------------------------------------------
    //  Refresh / getAccessToken
    // ------------------------------------------------------------------
    /**
     * Return a valid `GoogleOAuthPayload`, refreshing via the extension's
     * cloud function if the access token is within the expiry buffer.
     *
     * Concurrent calls for the same credential share a single refresh.
     *
     * @throws CredentialRevokedError if the refresh token is no longer valid
     */
    async getAccessToken(entry, payload) {
        // Fast path: still valid
        if (payload.expiresAt - Date.now() > this.expiryBufferMs) {
            return payload;
        }
        // Coalesce concurrent refreshes for the same credential
        const existing = this.refreshInFlight.get(entry.id);
        if (existing)
            return existing;
        // Cooldown — avoid hammering the refresh endpoint
        const last = this.lastRefreshAttempt.get(entry.id) ?? 0;
        if (Date.now() - last < REFRESH_COOLDOWN_MS) {
            // Within cooldown — return the (possibly expired) payload; caller will
            // get a fresh attempt after the cooldown window.
            return payload;
        }
        const promise = this.doRefresh(entry, payload);
        this.refreshInFlight.set(entry.id, promise);
        this.lastRefreshAttempt.set(entry.id, Date.now());
        try {
            return await promise;
        }
        finally {
            this.refreshInFlight.delete(entry.id);
        }
    }
    async doRefresh(entry, payload) {
        const url = this.cloudFunctionUrl.replace(/\/$/, '') + this.refreshPath;
        const response = await this.fetchFn(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ refresh_token: payload.refreshToken }),
        });
        if (!response.ok) {
            const body = await response.text().catch(() => '');
            // Google responds 400 with body containing "invalid_grant" when the
            // refresh token is revoked/expired beyond recovery.
            if (response.status === 400 &&
                /invalid_grant/i.test(body)) {
                await this.store.updateCredential(entry.id, { status: 'revoked' });
                throw new CredentialRevokedError(entry.id, entry.name, `Log in to the Gemini CLI Workspace extension again and re-import this credential in Crewly.`);
            }
            throw new Error(`Token refresh failed (${response.status}): ${body.slice(0, 500)}`);
        }
        const data = (await response.json());
        if (!data.access_token) {
            throw new Error('Token refresh response missing access_token');
        }
        const expiresAt = Date.now() + (data.expires_in ?? 3600) * 1000;
        const updated = {
            ...payload,
            accessToken: data.access_token,
            expiresAt,
            // Google typically does NOT issue a new refresh_token on refresh,
            // but honor it if present
            refreshToken: data.refresh_token ?? payload.refreshToken,
            scopes: typeof data.scope === 'string'
                ? data.scope.split(' ').filter(Boolean)
                : payload.scopes,
        };
        await this.store.setPayload(entry.id, updated);
        await this.store.updateCredential(entry.id, {
            expiresAt,
            lastUsedAt: new Date().toISOString(),
        });
        return updated;
    }
    // ------------------------------------------------------------------
    //  Internals
    // ------------------------------------------------------------------
    /**
     * Decrypt the extension's `{iv_hex}:{authTag_hex}:{ciphertext_hex}`
     * format (AES-256-GCM).
     */
    decryptExtensionFormat(encryptedText, key) {
        const parts = encryptedText.split(':');
        if (parts.length !== 3) {
            throw new Error('Invalid extension token format (expected iv:tag:ciphertext)');
        }
        const iv = Buffer.from(parts[0], 'hex');
        const authTag = Buffer.from(parts[1], 'hex');
        const ciphertextHex = parts[2];
        const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
        decipher.setAuthTag(authTag);
        let decrypted = decipher.update(ciphertextHex, 'hex', 'utf8');
        decrypted += decipher.final('utf8');
        return decrypted;
    }
}
// ============================================================================
// Test helpers (not exported from public barrel — intended for tests only)
// ============================================================================
/**
 * Encrypt a plaintext string with the extension's file format. Used in
 * tests to generate fake token files.
 */
export function encryptExtensionFormatForTesting(plaintext, key) {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
    let encrypted = cipher.update(plaintext, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    const authTag = cipher.getAuthTag();
    return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
}
/**
 * Derive the extension's encryption key using the real scrypt + salt.
 * Test-only export.
 */
export function deriveExtensionKeyForTesting(masterKey) {
    const salt = `${os.hostname()}-${os.userInfo().username}${SALT_SUFFIX}`;
    return crypto.scryptSync(masterKey, salt, 32);
}
//# sourceMappingURL=gemini-cli-workspace.helper.js.map