"use strict";
/**
 * Quota Fetcher for Codex (ChatGPT) Accounts
 *
 * Fetches quota information from ChatGPT backend API.
 * Used for displaying rate limit windows and reset times.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getUnknownCodexWindowLabels = exports.buildCodexQuotaWindows = exports.readCodexAuthData = exports.fetchAllCodexQuotas = exports.fetchCodexQuota = exports.buildCodexCoreUsageSummary = void 0;
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const config_generator_1 = require("../config/config-generator");
const account_manager_1 = require("../accounts/account-manager");
const auth_utils_1 = require("../auth/auth-utils");
const email_account_identity_1 = require("../accounts/email-account-identity");
/** ChatGPT backend API base URL */
const CODEX_API_BASE = 'https://chatgpt.com/backend-api';
const CODEX_QUOTA_TIMEOUT_MS = 12000;
const CODEX_QUOTA_MAX_ATTEMPTS = 2;
const CODEX_ERROR_DETAIL_MAX_LENGTH = 240;
/**
 * User agent matching Codex CLI for API compatibility.
 * Update when Codex CLI releases new versions to maintain compatibility.
 */
const USER_AGENT = 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal';
function getCodexWindowKind(label) {
    const lower = (label || '').toLowerCase();
    const isCodeReview = lower.includes('code review') || lower.includes('code_review');
    const isPrimary = lower.includes('primary');
    const isSecondary = lower.includes('secondary');
    if (isCodeReview) {
        if (isPrimary)
            return 'code-review-5h';
        if (isSecondary)
            return 'code-review-weekly';
        return 'code-review';
    }
    if (isPrimary)
        return 'usage-5h';
    if (isSecondary)
        return 'usage-weekly';
    return 'unknown';
}
function getUnknownCodexWindowLabels(windows) {
    const unknownLabels = windows
        .filter((window) => {
        // Windows with explicit category metadata are always classified.
        if (window.category)
            return false;
        return getCodexWindowKind(window.label) === 'unknown';
    })
        .map((window) => window.label)
        .filter((label) => typeof label === 'string' && label.trim().length > 0);
    return Array.from(new Set(unknownLabels));
}
exports.getUnknownCodexWindowLabels = getUnknownCodexWindowLabels;
function shouldLogCodexWindowWarnings(verbose) {
    if (verbose)
        return true;
    const debugFlag = process.env['CCS_DEBUG'];
    return debugFlag === '1' || debugFlag === 'true';
}
/**
 * Build explicit 5h + weekly usage summary from raw Codex windows.
 * Prefers explicit `category`/`cadence` metadata when present.
 * Falls back to label sniffing for legacy cached windows.
 */
function buildCodexCoreUsageSummary(windows) {
    if (!windows || windows.length === 0) {
        return { fiveHour: null, weekly: null };
    }
    let fiveHourWindow = null;
    let weeklyWindow = null;
    const nonCodeReviewWindows = [];
    // Determine if any window carries category metadata. If so, prefer category-based
    // selection so 'additional' windows (e.g. Spark) do not pollute the main usage summary.
    const hasCategoryMetadata = windows.some((window) => Boolean(window.category));
    if (hasCategoryMetadata) {
        for (const window of windows) {
            if (window.category === 'usage') {
                if (window.cadence === '5h' && !fiveHourWindow)
                    fiveHourWindow = window;
                else if (window.cadence === 'weekly' && !weeklyWindow)
                    weeklyWindow = window;
                nonCodeReviewWindows.push(window);
            }
            // 'code-review' and 'additional' windows are intentionally excluded from the main
            // usage summary — they represent feature-specific quotas, not core usage.
        }
    }
    else {
        for (const window of windows) {
            const kind = getCodexWindowKind(window.label);
            if (kind === 'usage-5h') {
                if (!fiveHourWindow)
                    fiveHourWindow = window;
                nonCodeReviewWindows.push(window);
                continue;
            }
            if (kind === 'usage-weekly') {
                if (!weeklyWindow)
                    weeklyWindow = window;
                nonCodeReviewWindows.push(window);
                continue;
            }
            if (kind === 'unknown') {
                nonCodeReviewWindows.push(window);
            }
        }
    }
    if ((!fiveHourWindow || !weeklyWindow) && nonCodeReviewWindows.length > 0) {
        const withReset = nonCodeReviewWindows
            .filter((w) => typeof w.resetAfterSeconds === 'number' &&
            isFinite(w.resetAfterSeconds) &&
            w.resetAfterSeconds >= 0)
            .sort((a, b) => (a.resetAfterSeconds || 0) - (b.resetAfterSeconds || 0));
        if (!fiveHourWindow) {
            fiveHourWindow = withReset[0] || nonCodeReviewWindows[0] || null;
        }
        if (!weeklyWindow) {
            weeklyWindow =
                withReset.length > 1
                    ? withReset[withReset.length - 1]
                    : nonCodeReviewWindows.find((w) => w !== fiveHourWindow) || null;
        }
    }
    const mapWindow = (window) => {
        if (!window)
            return null;
        return {
            label: window.label,
            remainingPercent: window.remainingPercent,
            resetAfterSeconds: window.resetAfterSeconds,
            resetAt: window.resetAt,
        };
    };
    return {
        fiveHour: mapWindow(fiveHourWindow),
        weekly: mapWindow(weeklyWindow),
    };
}
exports.buildCodexCoreUsageSummary = buildCodexCoreUsageSummary;
/**
 * Read auth data from Codex auth file
 */
function readCodexAuthFile(filePath) {
    try {
        const content = fs.readFileSync(filePath, 'utf-8');
        const data = JSON.parse(content);
        if (!data.access_token) {
            return null;
        }
        return {
            accessToken: data.access_token,
            accountId: data.account_id || data.accountId || '',
            isExpired: (0, auth_utils_1.isTokenExpired)(data.expired),
            expiresAt: data.expired || null,
        };
    }
    catch {
        return null;
    }
}
function readCodexAuthData(accountId) {
    const authDirs = [(0, config_generator_1.getAuthDir)(), (0, account_manager_1.getPausedDir)()];
    const registryAccount = (0, account_manager_1.getAccount)('codex', accountId);
    const canonicalEmail = (0, email_account_identity_1.extractCanonicalEmailFromAccountId)(accountId);
    const hasExplicitVariant = canonicalEmail !== null && canonicalEmail !== accountId;
    if (registryAccount?.tokenFile) {
        for (const authDir of authDirs) {
            const filePath = path.join(authDir, registryAccount.tokenFile);
            if (!fs.existsSync(filePath)) {
                continue;
            }
            const authData = readCodexAuthFile(filePath);
            if (authData) {
                return authData;
            }
        }
    }
    const legacyEmail = canonicalEmail ?? accountId;
    const sanitizedId = (0, auth_utils_1.sanitizeEmail)(legacyEmail);
    const expectedFile = `codex-${sanitizedId}.json`;
    for (const authDir of authDirs) {
        if (!fs.existsSync(authDir))
            continue;
        const filePath = path.join(authDir, expectedFile);
        if (fs.existsSync(filePath)) {
            const authData = readCodexAuthFile(filePath);
            if (authData) {
                return authData;
            }
        }
        // Fallback is only safe for legacy email-only IDs. Variant-backed IDs must resolve
        // through the registry-backed token file so duplicate-email accounts stay deterministic.
        if (hasExplicitVariant) {
            continue;
        }
        // Fallback: scan directory for matching email in file content.
        const files = fs.readdirSync(authDir);
        for (const file of files) {
            if (file.startsWith('codex-') && file.endsWith('.json')) {
                const candidatePath = path.join(authDir, file);
                try {
                    const content = fs.readFileSync(candidatePath, 'utf-8');
                    const data = JSON.parse(content);
                    if (data.email === legacyEmail && data.access_token) {
                        return {
                            accessToken: data.access_token,
                            accountId: data.account_id || data.accountId || '',
                            isExpired: (0, auth_utils_1.isTokenExpired)(data.expired),
                            expiresAt: data.expired || null,
                        };
                    }
                }
                catch {
                    continue;
                }
            }
        }
    }
    return null;
}
exports.readCodexAuthData = readCodexAuthData;
/**
 * Build CodexQuotaWindow array from API response
 * Handles both snake_case and camelCase field names
 */
function buildCodexQuotaWindows(payload) {
    const windows = [];
    // Get rate limit object (handles both cases)
    const rateLimit = payload.rate_limit || payload.rateLimit;
    const codeReviewRateLimit = payload.code_review_rate_limit || payload.codeReviewRateLimit;
    const additionalRateLimits = payload.additional_rate_limits || payload.additionalRateLimits;
    // Helper to extract window data
    const addWindow = (label, windowData, meta) => {
        if (!windowData)
            return;
        // Clamp usedPercent to [0, 100] range
        const rawUsedPercent = windowData.used_percent ?? windowData.usedPercent ?? 0;
        const usedPercent = Math.max(0, Math.min(100, rawUsedPercent));
        const resetAfterSeconds = windowData.reset_after_seconds ?? windowData.resetAfterSeconds ?? null;
        // Calculate reset timestamp if we have seconds
        let resetAt = null;
        if (resetAfterSeconds !== null && resetAfterSeconds > 0) {
            resetAt = new Date(Date.now() + resetAfterSeconds * 1000).toISOString();
        }
        const window = {
            label,
            usedPercent,
            remainingPercent: Math.max(0, 100 - usedPercent),
            resetAfterSeconds,
            resetAt,
            category: meta.category,
            cadence: meta.cadence,
        };
        if (meta.featureLabel) {
            window.featureLabel = meta.featureLabel;
        }
        windows.push(window);
    };
    // Add main rate limit windows
    if (rateLimit) {
        addWindow('Primary', rateLimit.primary_window || rateLimit.primaryWindow, {
            category: 'usage',
            cadence: '5h',
        });
        addWindow('Secondary', rateLimit.secondary_window || rateLimit.secondaryWindow, {
            category: 'usage',
            cadence: 'weekly',
        });
    }
    // Add code review rate limit windows
    if (codeReviewRateLimit) {
        addWindow('Code Review (Primary)', codeReviewRateLimit.primary_window || codeReviewRateLimit.primaryWindow, { category: 'code-review', cadence: '5h', featureLabel: 'Code Review' });
        addWindow('Code Review (Secondary)', codeReviewRateLimit.secondary_window || codeReviewRateLimit.secondaryWindow, { category: 'code-review', cadence: 'weekly', featureLabel: 'Code Review' });
    }
    // Add additional rate limit windows (e.g. GPT-5.3 Codex Spark)
    if (Array.isArray(additionalRateLimits)) {
        for (const entry of additionalRateLimits) {
            if (!entry)
                continue;
            const entryRateLimit = entry.rate_limit || entry.rateLimit;
            if (!entryRateLimit)
                continue;
            const rawFeatureLabel = entry.limit_name ?? entry.limitName;
            const featureLabel = typeof rawFeatureLabel === 'string' && rawFeatureLabel.trim().length > 0
                ? rawFeatureLabel.trim()
                : 'Additional';
            addWindow(`${featureLabel} (Primary)`, entryRateLimit.primary_window || entryRateLimit.primaryWindow, { category: 'additional', cadence: '5h', featureLabel });
            addWindow(`${featureLabel} (Secondary)`, entryRateLimit.secondary_window || entryRateLimit.secondaryWindow, { category: 'additional', cadence: 'weekly', featureLabel });
        }
    }
    return windows;
}
exports.buildCodexQuotaWindows = buildCodexQuotaWindows;
function buildCodexFailureResult(accountId, options) {
    return {
        success: false,
        windows: [],
        planType: null,
        lastUpdated: Date.now(),
        accountId,
        error: options.error,
        httpStatus: options.httpStatus,
        errorCode: options.errorCode,
        errorDetail: options.errorDetail,
        actionHint: options.actionHint,
        retryable: options.retryable,
        needsReauth: options.needsReauth,
        isForbidden: options.isForbidden,
    };
}
function sanitizeCodexErrorDetail(bodyText) {
    const trimmed = bodyText.trim();
    if (!trimmed) {
        return undefined;
    }
    if (/^<!doctype html/i.test(trimmed) || /^<html/i.test(trimmed) || /^<[^>]+>/.test(trimmed)) {
        return '[HTML error response omitted]';
    }
    let sanitized = trimmed
        .replace(/"(access[_-]?token|refresh[_-]?token|authorization|cookie|set-cookie|api[_-]?key|session[_-]?token|token)"\s*:\s*"[^"]*"/gi, '"$1":"[redacted]"')
        .replace(/Bearer\s+[A-Za-z0-9._-]+/g, 'Bearer [redacted]')
        .replace(/\s+/g, ' ');
    if (sanitized.length > CODEX_ERROR_DETAIL_MAX_LENGTH) {
        sanitized = `${sanitized.slice(0, CODEX_ERROR_DETAIL_MAX_LENGTH - 14)}...[truncated]`;
    }
    return sanitized;
}
function parseCodexErrorBody(bodyText) {
    const trimmed = bodyText.trim();
    if (!trimmed) {
        return {};
    }
    const sanitizedDetail = sanitizeCodexErrorDetail(trimmed);
    try {
        const parsed = JSON.parse(trimmed);
        const topLevelMessage = typeof parsed.message === 'string'
            ? parsed.message
            : typeof parsed.detail === 'string'
                ? parsed.detail
                : undefined;
        const topLevelCode = typeof parsed.code === 'string' ? parsed.code : undefined;
        if (parsed.error && typeof parsed.error === 'object') {
            const error = parsed.error;
            const errorCode = typeof error.code === 'string' ? error.code : topLevelCode;
            const errorMessage = typeof error.message === 'string'
                ? error.message
                : typeof error.error === 'string'
                    ? error.error
                    : topLevelMessage;
            return {
                errorCode,
                errorDetail: sanitizedDetail,
                message: errorMessage,
            };
        }
        if (parsed.detail && typeof parsed.detail === 'object') {
            const detail = parsed.detail;
            return {
                errorCode: typeof detail.code === 'string'
                    ? detail.code
                    : typeof detail.type === 'string'
                        ? detail.type
                        : topLevelCode,
                errorDetail: sanitizedDetail,
                message: typeof detail.message === 'string'
                    ? detail.message
                    : typeof detail.error === 'string'
                        ? detail.error
                        : topLevelMessage,
            };
        }
        return {
            errorCode: topLevelCode,
            errorDetail: sanitizedDetail,
            message: topLevelMessage,
        };
    }
    catch {
        return {
            errorDetail: sanitizedDetail,
            message: trimmed,
        };
    }
}
function buildCodexHttpFailureResult(accountId, status, bodyText) {
    const parsed = parseCodexErrorBody(bodyText);
    if (status === 401) {
        return buildCodexFailureResult(accountId, {
            error: 'Token expired or invalid',
            httpStatus: 401,
            errorCode: parsed.errorCode || 'reauth_required',
            errorDetail: parsed.errorDetail,
            actionHint: 'Run ccs cliproxy auth codex to re-authenticate this account.',
            needsReauth: true,
            retryable: false,
        });
    }
    if (status === 402) {
        if (parsed.errorCode === 'deactivated_workspace') {
            return buildCodexFailureResult(accountId, {
                error: 'Workspace deactivated (HTTP 402)',
                httpStatus: 402,
                errorCode: parsed.errorCode,
                errorDetail: parsed.errorDetail,
                actionHint: 'Remove and re-add this account from an active ChatGPT workspace before retrying.',
                retryable: false,
            });
        }
        return buildCodexFailureResult(accountId, {
            error: parsed.message || 'Payment or workspace access required (HTTP 402)',
            httpStatus: 402,
            errorCode: parsed.errorCode || 'payment_required',
            errorDetail: parsed.errorDetail,
            actionHint: 'Confirm the ChatGPT workspace/subscription is active, then retry.',
            retryable: false,
        });
    }
    if (status === 403) {
        return buildCodexFailureResult(accountId, {
            error: 'Quota API access forbidden (HTTP 403)',
            httpStatus: 403,
            errorCode: parsed.errorCode || 'quota_api_forbidden',
            errorDetail: parsed.errorDetail,
            actionHint: 'This account cannot access the Codex quota endpoint.',
            isForbidden: true,
            retryable: false,
        });
    }
    if (status === 404) {
        return buildCodexFailureResult(accountId, {
            error: 'Codex quota endpoint not found (HTTP 404)',
            httpStatus: 404,
            errorCode: parsed.errorCode || 'quota_endpoint_not_found',
            errorDetail: parsed.errorDetail,
            actionHint: 'The upstream Codex quota endpoint changed or is unavailable.',
            retryable: false,
        });
    }
    if (status === 429) {
        return buildCodexFailureResult(accountId, {
            error: 'Rate limited - try again later',
            httpStatus: 429,
            errorCode: parsed.errorCode || 'rate_limited',
            errorDetail: parsed.errorDetail,
            actionHint: 'Retry after a short delay.',
            retryable: true,
        });
    }
    if (status >= 500) {
        return buildCodexFailureResult(accountId, {
            error: `Codex quota service unavailable (HTTP ${status})`,
            httpStatus: status,
            errorCode: parsed.errorCode || 'provider_unavailable',
            errorDetail: parsed.errorDetail,
            actionHint: 'Retry later. This looks like a temporary upstream problem.',
            retryable: true,
        });
    }
    return buildCodexFailureResult(accountId, {
        error: parsed.message || `Codex quota request failed (HTTP ${status})`,
        httpStatus: status,
        errorCode: parsed.errorCode || 'unknown_upstream_error',
        errorDetail: parsed.errorDetail,
        actionHint: 'Inspect the upstream response details and retry if appropriate.',
        retryable: false,
    });
}
/**
 * Fetch quota for a single Codex account
 *
 * @param accountId - Account identifier (email)
 * @param verbose - Show detailed diagnostics
 * @returns Quota result with windows and percentages
 */
async function fetchCodexQuota(accountId, verbose = false) {
    if (verbose)
        console.error(`[i] Fetching Codex quota for ${accountId}...`);
    const authData = readCodexAuthData(accountId);
    if (!authData) {
        const error = 'Auth file not found for Codex account';
        if (verbose)
            console.error(`[!] Error: ${error}`);
        return buildCodexFailureResult(accountId, {
            error,
            errorCode: 'auth_file_missing',
            actionHint: 'Remove the stale account or authenticate again with ccs cliproxy auth codex.',
            retryable: false,
        });
    }
    if (authData.isExpired) {
        const error = 'Token expired - re-authenticate with ccs cliproxy auth codex';
        if (verbose)
            console.error(`[!] Error: ${error}`);
        return buildCodexFailureResult(accountId, {
            error,
            errorCode: 'token_expired',
            actionHint: 'Run ccs cliproxy auth codex to refresh the token for this account.',
            needsReauth: true,
            retryable: false,
        });
    }
    if (!authData.accountId) {
        const error = 'Missing ChatGPT-Account-Id in auth file';
        if (verbose)
            console.error(`[!] Error: ${error}`);
        return buildCodexFailureResult(accountId, {
            error,
            errorCode: 'missing_account_id',
            actionHint: 'Remove and re-add this Codex account to refresh workspace metadata.',
            retryable: false,
        });
    }
    const url = `${CODEX_API_BASE}/wham/usage`;
    let lastErrorMsg = 'Unknown error';
    for (let attempt = 1; attempt <= CODEX_QUOTA_MAX_ATTEMPTS; attempt++) {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), CODEX_QUOTA_TIMEOUT_MS);
        try {
            const response = await fetch(url, {
                method: 'GET',
                signal: controller.signal,
                headers: {
                    Authorization: `Bearer ${authData.accessToken}`,
                    'ChatGPT-Account-Id': authData.accountId,
                    'User-Agent': USER_AGENT,
                },
            });
            clearTimeout(timeoutId);
            if (verbose)
                console.error(`[i] Codex API status: ${response.status} (attempt ${attempt})`);
            if (!response.ok) {
                const bodyText = await response.text();
                return buildCodexHttpFailureResult(accountId, response.status, bodyText);
            }
            const data = (await response.json());
            const windows = buildCodexQuotaWindows(data);
            const unknownWindowLabels = getUnknownCodexWindowLabels(windows);
            if (unknownWindowLabels.length > 0 && shouldLogCodexWindowWarnings(verbose)) {
                console.error(`[!] Codex quota detected unknown window labels: ${unknownWindowLabels.join(', ')}`);
                console.error('    Window classification may need an update for upstream API changes.');
            }
            const coreUsage = buildCodexCoreUsageSummary(windows);
            // Extract plan type
            const planTypeRaw = data.plan_type || data.planType;
            let planType = null;
            if (planTypeRaw) {
                const normalized = planTypeRaw.toLowerCase();
                if (normalized === 'free')
                    planType = 'free';
                else if (normalized === 'plus')
                    planType = 'plus';
                else if (normalized === 'pro')
                    planType = 'pro';
                else if (normalized === 'team')
                    planType = 'team';
            }
            if (verbose)
                console.error(`[i] Codex windows found: ${windows.length}`);
            return {
                success: true,
                windows,
                coreUsage,
                planType,
                lastUpdated: Date.now(),
                accountId,
            };
        }
        catch (err) {
            clearTimeout(timeoutId);
            const isAbortError = err instanceof Error && err.name === 'AbortError';
            lastErrorMsg = isAbortError
                ? 'Request timeout'
                : err instanceof Error
                    ? err.message
                    : 'Unknown error';
            if (verbose) {
                console.error(`[!] Codex quota error (attempt ${attempt}): ${lastErrorMsg}`);
            }
            // Retry timeout once; other failures return immediately.
            if (isAbortError && attempt < CODEX_QUOTA_MAX_ATTEMPTS) {
                continue;
            }
            return {
                success: false,
                windows: [],
                planType: null,
                lastUpdated: Date.now(),
                error: lastErrorMsg,
                accountId,
                errorCode: isAbortError ? 'network_timeout' : 'network_error',
                actionHint: isAbortError
                    ? 'Retry later. The Codex quota endpoint timed out.'
                    : 'Retry later or inspect network connectivity.',
                retryable: true,
            };
        }
    }
    return {
        success: false,
        windows: [],
        planType: null,
        lastUpdated: Date.now(),
        error: lastErrorMsg,
        accountId,
        errorCode: 'unknown_error',
        retryable: true,
    };
}
exports.fetchCodexQuota = fetchCodexQuota;
/**
 * Fetch quota for all Codex accounts
 *
 * @param verbose - Show detailed diagnostics
 * @returns Array of account quotas
 */
async function fetchAllCodexQuotas(verbose = false) {
    const accounts = (0, account_manager_1.getProviderAccounts)('codex');
    if (accounts.length === 0) {
        return [];
    }
    const results = await Promise.all(accounts.map(async (account) => ({
        account: account.id,
        quota: await fetchCodexQuota(account.id, verbose),
    })));
    return results;
}
exports.fetchAllCodexQuotas = fetchAllCodexQuotas;
//# sourceMappingURL=quota-fetcher-codex.js.map