import { readFile } from 'node:fs/promises';
import { homedir } from 'node:os';
const CLAUDE_CREDENTIALS_PATH = '~/.claude/.credentials.json';
const CLAUDE_REFRESH_ENDPOINT = 'https://platform.claude.com/v1/oauth/token';
const CLAUDE_USAGE_ENDPOINT = 'https://api.anthropic.com/api/oauth/usage';
const CLAUDE_OAUTH_BETA_HEADER = 'oauth-2025-04-20';
const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
const CODEX_CREDENTIALS_PATH = '~/.codex/auth.json';
const CODEX_REFRESH_ENDPOINT = 'https://auth.openai.com/oauth/token';
const CODEX_USAGE_ENDPOINT = 'https://chatgpt.com/backend-api/wham/usage';
const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
function isRecord(value) {
    return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function clampPercent(value) {
    if (value < 0)
        return 0;
    if (value > 100)
        return 100;
    return value;
}
function toFinitePercent(value) {
    if (value === null || value === undefined || value === '')
        return null;
    const percent = Number(value);
    if (!Number.isFinite(percent))
        return null;
    return clampPercent(percent);
}
function unixSecondsToIso(value) {
    if (value === null || value === undefined || value === '')
        return null;
    const seconds = Number(value);
    if (!Number.isFinite(seconds))
        return null;
    return new Date(seconds * 1000).toISOString();
}
function resolveCredentialsPath(homeDir, path) {
    if (homeDir && path.startsWith('~/'))
        return `${homeDir}${path.slice(1)}`;
    return path;
}
function resolveAccessToken(credentials) {
    if (!isRecord(credentials))
        return null;
    return typeof credentials.accessToken === 'string'
        ? credentials.accessToken
        : typeof credentials.access_token === 'string'
            ? credentials.access_token
            : typeof credentials.token === 'string'
                ? credentials.token
                : null;
}
function resolveRefreshToken(credentials) {
    if (!isRecord(credentials))
        return null;
    return typeof credentials.refreshToken === 'string'
        ? credentials.refreshToken
        : typeof credentials.refresh_token === 'string'
            ? credentials.refresh_token
            : null;
}
function resolveCodexTokenBundle(credentials) {
    if (!isRecord(credentials) || !isRecord(credentials.tokens)) {
        return { accessToken: null, refreshToken: null, accountId: null };
    }
    return {
        accessToken: typeof credentials.tokens.access_token === 'string'
            ? credentials.tokens.access_token
            : typeof credentials.tokens.accessToken === 'string'
                ? credentials.tokens.accessToken
                : null,
        refreshToken: typeof credentials.tokens.refresh_token === 'string'
            ? credentials.tokens.refresh_token
            : typeof credentials.tokens.refreshToken === 'string'
                ? credentials.tokens.refreshToken
                : null,
        accountId: typeof credentials.tokens.account_id === 'string' ? credentials.tokens.account_id : null
    };
}
function resolveExpiry(credentials) {
    if (!isRecord(credentials))
        return null;
    return typeof credentials.expiresAt === 'string' || typeof credentials.expiresAt === 'number'
        ? credentials.expiresAt
        : typeof credentials.expires_at === 'string' || typeof credentials.expires_at === 'number'
            ? credentials.expires_at
            : typeof credentials.expiry === 'string' || typeof credentials.expiry === 'number'
                ? credentials.expiry
                : null;
}
function isTokenExpired(credentials) {
    const expiry = resolveExpiry(credentials);
    if (expiry === null || expiry === undefined)
        return false;
    if (typeof expiry === 'number') {
        const millis = expiry > 1e12 ? expiry : expiry * 1000;
        return Date.now() >= millis;
    }
    const parsed = Date.parse(expiry);
    if (Number.isNaN(parsed))
        return false;
    return Date.now() >= parsed;
}
function mapHttpStatusToErrorCode(status) {
    if (status === 401 || status === 403)
        return 'auth_expired';
    if (status === 404)
        return 'schema_changed';
    if (status === 429)
        return 'rate_limited';
    if (status >= 500 && status < 600)
        return 'network_error';
    return 'network_error';
}
function resolveUsageWindow(payload, keys) {
    if (!isRecord(payload))
        return null;
    for (const key of keys) {
        const candidate = payload[key];
        if (isRecord(candidate)) {
            return {
                utilization: candidate.utilization,
                resets_at: candidate.resets_at
            };
        }
    }
    return null;
}
function normalizeClaudeUsage(payload) {
    const fiveHour = resolveUsageWindow(payload, ['five_hour', 'fiveHour', 'session', 'session_window']);
    const sevenDay = resolveUsageWindow(payload, ['seven_day', 'sevenDay', 'weekly', 'week', 'weekly_window']);
    const fiveHourUtilization = toFinitePercent(fiveHour?.utilization);
    const sevenDayUtilization = toFinitePercent(sevenDay?.utilization);
    return {
        data: {
            sessionRemainingPct: fiveHourUtilization !== null ? clampPercent(100 - fiveHourUtilization) : null,
            weeklyRemainingPct: sevenDayUtilization !== null ? clampPercent(100 - sevenDayUtilization) : null,
            sessionUsedPct: fiveHourUtilization,
            weeklyUsedPct: sevenDayUtilization,
            sessionResetsAtIso: typeof fiveHour?.resets_at === 'string' ? fiveHour.resets_at : null,
            weeklyResetsAtIso: typeof sevenDay?.resets_at === 'string' ? sevenDay.resets_at : null
        },
        hasSessionUsage: fiveHourUtilization !== null,
        hasWeeklyUsage: sevenDayUtilization !== null
    };
}
function normalizeCodexUsage(payload) {
    const rateLimit = isRecord(payload) && isRecord(payload.rate_limit) ? payload.rate_limit : null;
    const primaryWindow = rateLimit && isRecord(rateLimit.primary_window) ? rateLimit.primary_window : null;
    const secondaryWindow = rateLimit && isRecord(rateLimit.secondary_window) ? rateLimit.secondary_window : null;
    const primaryUsedPct = toFinitePercent(primaryWindow?.used_percent);
    const secondaryUsedPct = toFinitePercent(secondaryWindow?.used_percent);
    return {
        data: {
            sessionRemainingPct: primaryUsedPct !== null ? clampPercent(100 - primaryUsedPct) : null,
            weeklyRemainingPct: secondaryUsedPct !== null ? clampPercent(100 - secondaryUsedPct) : null,
            sessionUsedPct: primaryUsedPct,
            weeklyUsedPct: secondaryUsedPct,
            sessionResetsAtIso: unixSecondsToIso(primaryWindow?.reset_at),
            weeklyResetsAtIso: unixSecondsToIso(secondaryWindow?.reset_at)
        },
        hasSessionUsage: primaryWindow !== null && primaryUsedPct !== null,
        hasWeeklyUsage: secondaryWindow !== null && secondaryUsedPct !== null
    };
}
async function parseJsonResponse(response) {
    try {
        return await response.json();
    }
    catch {
        return null;
    }
}
async function fetchUsage(fetchImpl, url, headers) {
    const response = await fetchImpl(url, {
        method: 'GET',
        headers
    });
    if (!response.ok) {
        return {
            ok: false,
            code: mapHttpStatusToErrorCode(response.status),
            message: `Usage request failed with status ${response.status}`
        };
    }
    const payload = await parseJsonResponse(response);
    if (!payload) {
        return {
            ok: false,
            code: 'schema_changed',
            message: 'Usage endpoint returned invalid JSON'
        };
    }
    return { ok: true, data: payload };
}
function isTransientUsageError(code) {
    return code === 'rate_limited' || code === 'network_error';
}
async function fetchClaudeUsageWithRetry(fetchImpl, headers) {
    const first = await fetchUsage(fetchImpl, CLAUDE_USAGE_ENDPOINT, headers);
    if (first.ok)
        return first;
    if (!isTransientUsageError(first.code))
        return first;
    await new Promise((resolve) => setTimeout(resolve, 750));
    return fetchUsage(fetchImpl, CLAUDE_USAGE_ENDPOINT, headers);
}
async function refreshClaudeAccessToken(fetchImpl, refreshToken) {
    if (!refreshToken) {
        return { ok: false, code: 'auth_expired', message: 'OAuth refresh token is missing' };
    }
    const body = new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: CLAUDE_CLIENT_ID
    });
    const response = await fetchImpl(CLAUDE_REFRESH_ENDPOINT, {
        method: 'POST',
        headers: {
            'content-type': 'application/x-www-form-urlencoded'
        },
        body
    });
    if (!response.ok) {
        return {
            ok: false,
            code: mapHttpStatusToErrorCode(response.status),
            message: `Refresh request failed with status ${response.status}`
        };
    }
    const payload = await parseJsonResponse(response);
    if (!isRecord(payload)) {
        return { ok: false, code: 'schema_changed', message: 'Refresh endpoint returned invalid JSON' };
    }
    const accessToken = resolveAccessToken(payload);
    if (!accessToken) {
        return { ok: false, code: 'auth_expired', message: 'Refresh response did not include an access token' };
    }
    return { ok: true, accessToken };
}
async function refreshCodexAccessToken(fetchImpl, refreshToken) {
    if (!refreshToken) {
        return { ok: false, code: 'auth_expired', message: 'OAuth refresh token is missing' };
    }
    const body = new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: CODEX_CLIENT_ID,
        refresh_token: refreshToken
    });
    const response = await fetchImpl(CODEX_REFRESH_ENDPOINT, {
        method: 'POST',
        headers: {
            'content-type': 'application/x-www-form-urlencoded'
        },
        body
    });
    if (!response.ok) {
        return {
            ok: false,
            code: mapHttpStatusToErrorCode(response.status),
            message: `Refresh request failed with status ${response.status}`
        };
    }
    const payload = await parseJsonResponse(response);
    if (!isRecord(payload)) {
        return { ok: false, code: 'schema_changed', message: 'Refresh endpoint returned invalid JSON' };
    }
    const accessToken = typeof payload.access_token === 'string' ? payload.access_token : null;
    if (!accessToken) {
        return { ok: false, code: 'auth_expired', message: 'Refresh response did not include an access token' };
    }
    return { ok: true, accessToken };
}
async function readJsonFile(readTextFile, path) {
    try {
        const raw = await readTextFile(path);
        if (!raw)
            return { ok: false, code: 'missing_creds' };
        try {
            return { ok: true, data: JSON.parse(raw) };
        }
        catch {
            return { ok: false, code: 'parse_error' };
        }
    }
    catch (error) {
        if (isRecord(error) && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
            return { ok: false, code: 'missing_creds' };
        }
        return { ok: false, code: 'missing_creds' };
    }
}
function buildSnapshot(provider, label, code, data, updatedAtIso) {
    return {
        provider,
        label,
        code,
        data,
        updatedAtIso
    };
}
function latestIso(left, right) {
    if (left && right)
        return left > right ? left : right;
    return left ?? right;
}
export class ProviderUsageService {
    fetchImpl;
    readTextFile;
    homeDir;
    now;
    lastGoodData = new Map();
    constructor(options = {}) {
        this.fetchImpl = options.fetch ?? globalThis.fetch;
        this.readTextFile = options.readTextFile ?? ((path) => readFile(path, 'utf8'));
        this.homeDir = options.homeDir ?? process.env.HOME ?? homedir();
        this.now = options.now ?? (() => new Date().toISOString());
    }
    async getSummary() {
        const [codex, claude] = await Promise.all([this.getCodexUsage(), this.getClaudeUsage()]);
        return {
            providers: {
                codex,
                claude
            },
            lastUpdatedAtIso: latestIso(codex.updatedAtIso, claude.updatedAtIso)
        };
    }
    async getClaudeUsage() {
        const updatedAtIso = this.now();
        if (typeof this.fetchImpl !== 'function') {
            return buildSnapshot('claude', 'Claude', 'network_error', null, updatedAtIso);
        }
        const credentialsPath = resolveCredentialsPath(this.homeDir, CLAUDE_CREDENTIALS_PATH);
        const credentials = await readJsonFile(this.readTextFile, credentialsPath);
        if (!credentials.ok) {
            return buildSnapshot('claude', 'Claude', credentials.code, null, updatedAtIso);
        }
        if (!isRecord(credentials.data) || !isRecord(credentials.data.claudeAiOauth)) {
            return buildSnapshot('claude', 'Claude', 'missing_creds', null, updatedAtIso);
        }
        let accessToken = resolveAccessToken(credentials.data.claudeAiOauth);
        const refreshToken = resolveRefreshToken(credentials.data.claudeAiOauth);
        try {
            if (!accessToken || isTokenExpired(credentials.data.claudeAiOauth)) {
                const refreshed = await refreshClaudeAccessToken(this.fetchImpl, refreshToken);
                if (!refreshed.ok) {
                    return buildSnapshot('claude', 'Claude', refreshed.code, null, updatedAtIso);
                }
                accessToken = refreshed.accessToken;
            }
            let usage = await fetchClaudeUsageWithRetry(this.fetchImpl, {
                authorization: `Bearer ${accessToken}`,
                'anthropic-beta': CLAUDE_OAUTH_BETA_HEADER
            });
            if (!usage.ok && usage.code === 'auth_expired') {
                const refreshed = await refreshClaudeAccessToken(this.fetchImpl, refreshToken);
                if (!refreshed.ok) {
                    return buildSnapshot('claude', 'Claude', refreshed.code, null, updatedAtIso);
                }
                usage = await fetchClaudeUsageWithRetry(this.fetchImpl, {
                    authorization: `Bearer ${refreshed.accessToken}`,
                    'anthropic-beta': CLAUDE_OAUTH_BETA_HEADER
                });
            }
            if (!usage.ok) {
                return buildSnapshot('claude', 'Claude', usage.code, null, updatedAtIso);
            }
            const normalized = normalizeClaudeUsage(usage.data);
            if (!normalized.hasSessionUsage && !normalized.hasWeeklyUsage) {
                console.warn('Claude usage returned no recognized usage windows', {
                    updatedAtIso,
                    payload: usage.data
                });
                return buildSnapshot('claude', 'Claude', 'schema_changed', null, updatedAtIso);
            }
            const mergedData = this.mergeWithLastGoodData('claude', normalized.data);
            if (!normalized.hasSessionUsage || !normalized.hasWeeklyUsage) {
                console.warn('Claude usage returned partial usage data', {
                    updatedAtIso,
                    payload: usage.data,
                    normalized: mergedData
                });
                return buildSnapshot('claude', 'Claude', 'partial_data', mergedData, updatedAtIso);
            }
            this.lastGoodData.set('claude', normalized.data);
            return buildSnapshot('claude', 'Claude', 'ok', normalized.data, updatedAtIso);
        }
        catch {
            return buildSnapshot('claude', 'Claude', 'network_error', null, updatedAtIso);
        }
    }
    async getCodexUsage() {
        const updatedAtIso = this.now();
        if (typeof this.fetchImpl !== 'function') {
            return buildSnapshot('codex', 'Codex', 'network_error', null, updatedAtIso);
        }
        const credentialsPath = resolveCredentialsPath(this.homeDir, CODEX_CREDENTIALS_PATH);
        const credentials = await readJsonFile(this.readTextFile, credentialsPath);
        if (!credentials.ok) {
            return buildSnapshot('codex', 'Codex', credentials.code, null, updatedAtIso);
        }
        const tokenBundle = resolveCodexTokenBundle(credentials.data);
        let accessToken = tokenBundle.accessToken;
        const refreshToken = tokenBundle.refreshToken;
        const accountId = tokenBundle.accountId;
        try {
            if (!accessToken) {
                const refreshed = await refreshCodexAccessToken(this.fetchImpl, refreshToken);
                if (!refreshed.ok) {
                    return buildSnapshot('codex', 'Codex', refreshed.code, null, updatedAtIso);
                }
                accessToken = refreshed.accessToken;
            }
            let usage = await fetchUsage(this.fetchImpl, CODEX_USAGE_ENDPOINT, {
                authorization: `Bearer ${accessToken}`,
                ...(accountId ? { 'ChatGPT-Account-Id': accountId } : {})
            });
            if (!usage.ok && usage.code === 'auth_expired') {
                const refreshed = await refreshCodexAccessToken(this.fetchImpl, refreshToken);
                if (!refreshed.ok) {
                    return buildSnapshot('codex', 'Codex', refreshed.code, null, updatedAtIso);
                }
                usage = await fetchUsage(this.fetchImpl, CODEX_USAGE_ENDPOINT, {
                    authorization: `Bearer ${refreshed.accessToken}`,
                    ...(accountId ? { 'ChatGPT-Account-Id': accountId } : {})
                });
            }
            if (!usage.ok) {
                return buildSnapshot('codex', 'Codex', usage.code, null, updatedAtIso);
            }
            const normalized = normalizeCodexUsage(usage.data);
            if (!normalized.hasSessionUsage && !normalized.hasWeeklyUsage) {
                return buildSnapshot('codex', 'Codex', 'schema_changed', null, updatedAtIso);
            }
            const mergedData = this.mergeWithLastGoodData('codex', normalized.data);
            if (!normalized.hasSessionUsage || !normalized.hasWeeklyUsage) {
                return buildSnapshot('codex', 'Codex', 'partial_data', mergedData, updatedAtIso);
            }
            this.lastGoodData.set('codex', normalized.data);
            return buildSnapshot('codex', 'Codex', 'ok', normalized.data, updatedAtIso);
        }
        catch {
            return buildSnapshot('codex', 'Codex', 'network_error', null, updatedAtIso);
        }
    }
    mergeWithLastGoodData(provider, data) {
        const previous = this.lastGoodData.get(provider);
        if (!previous)
            return data;
        return {
            sessionRemainingPct: data.sessionRemainingPct ?? previous.sessionRemainingPct,
            weeklyRemainingPct: data.weeklyRemainingPct ?? previous.weeklyRemainingPct,
            sessionUsedPct: data.sessionUsedPct ?? previous.sessionUsedPct,
            weeklyUsedPct: data.weeklyUsedPct ?? previous.weeklyUsedPct,
            sessionResetsAtIso: data.sessionResetsAtIso ?? previous.sessionResetsAtIso,
            weeklyResetsAtIso: data.weeklyResetsAtIso ?? previous.weeklyResetsAtIso
        };
    }
}
//# sourceMappingURL=provider-usage.js.map