import { execFileSync, execSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { readAllClaudeAccounts, refreshAccount, writeBackCredentials, } from "./keychain.js";
import { resetExcludedBetas } from "./betas.js";
import { log } from "./logger.js";
const CREDENTIAL_CACHE_TTL_MS = 30_000;
const accountCacheMap = new Map();
let activeAccountSource = null;
let allAccounts = [];
export function initAccounts(accounts) {
    allAccounts = accounts;
}
export function getAccounts() {
    return allAccounts;
}
export function setActiveAccountSource(source) {
    const previous = activeAccountSource;
    activeAccountSource = source;
    accountCacheMap.delete(source);
    resetExcludedBetas();
    if (previous && previous !== source) {
        log("account_switch", { newSource: source, previousSource: previous });
    }
}
export function refreshAccountsList() {
    allAccounts = readAllClaudeAccounts();
    return allAccounts;
}
function getActiveAccount() {
    if (allAccounts.length === 0)
        return null;
    if (activeAccountSource) {
        const found = allAccounts.find((a) => a.source === activeAccountSource);
        if (found)
            return found;
    }
    return allAccounts[0];
}
function getAccountStateFile() {
    return join(homedir(), ".local", "share", "opencode", "claude-account-source.txt");
}
export function loadPersistedAccountSource() {
    try {
        const path = getAccountStateFile();
        if (existsSync(path)) {
            return readFileSync(path, "utf-8").trim() || null;
        }
    }
    catch {
        // ignore
    }
    return null;
}
export function saveAccountSource(source) {
    try {
        const path = getAccountStateFile();
        const dir = dirname(path);
        if (!existsSync(dir))
            mkdirSync(dir, { recursive: true });
        writeFileSync(path, source, "utf-8");
    }
    catch {
        // Non-fatal
    }
}
function getAuthJsonPaths() {
    const xdgPath = join(homedir(), ".local", "share", "opencode", "auth.json");
    if (process.platform === "win32") {
        const appData = process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
        const localAppDataPath = join(appData, "opencode", "auth.json");
        return [xdgPath, localAppDataPath];
    }
    return [xdgPath];
}
function syncToPath(authPath, creds) {
    let auth = {};
    if (existsSync(authPath)) {
        const raw = readFileSync(authPath, "utf-8").trim();
        if (raw) {
            try {
                auth = JSON.parse(raw);
            }
            catch {
                // Malformed file, start fresh
            }
        }
    }
    auth.anthropic = {
        type: "oauth",
        access: creds.accessToken,
        refresh: creds.refreshToken,
        expires: creds.expiresAt,
    };
    const dir = dirname(authPath);
    if (!existsSync(dir)) {
        mkdirSync(dir, { recursive: true, mode: 0o700 });
    }
    writeFileSync(authPath, JSON.stringify(auth, null, 2), {
        encoding: "utf-8",
        mode: 0o600,
    });
    if (process.platform !== "win32") {
        chmodSync(authPath, 0o600);
    }
}
export function syncAuthJson(creds) {
    for (const authPath of getAuthJsonPaths()) {
        try {
            syncToPath(authPath, creds);
            log("sync_auth_json", { path: authPath, success: true });
        }
        catch (err) {
            log("sync_auth_json", {
                path: authPath,
                success: false,
                error: err instanceof Error ? err.message : String(err),
            });
            throw err;
        }
    }
}
export const OAUTH_TOKEN_URL = "https://claude.ai/v1/oauth/token";
export const OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
/**
 * Parse a raw OAuth token response into ClaudeCredentials.
 * Returns null if the response is missing a valid access_token.
 * Defaults expires_in to 36000s (10h) to match observed Claude token lifetime.
 */
export function parseOAuthResponse(raw, currentRefreshToken, now = Date.now()) {
    let data;
    try {
        data = JSON.parse(raw);
    }
    catch {
        return null;
    }
    if (!data.access_token)
        return null;
    return {
        accessToken: data.access_token,
        refreshToken: data.refresh_token ?? currentRefreshToken,
        expiresAt: now + (data.expires_in ?? 36_000) * 1000,
    };
}
export function refreshViaOAuth(refreshToken) {
    // Use a Node subprocess to perform the HTTP request synchronously.
    // The refresh token is passed via stdin to avoid exposure in process args.
    const script = `
    process.stdin.resume();
    let input = '';
    process.stdin.on('data', c => input += c);
    process.stdin.on('end', () => {
      const body = new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: '${OAUTH_CLIENT_ID}',
        refresh_token: input.trim()
      });
      fetch('${OAUTH_TOKEN_URL}', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: body.toString()
      })
      .then(r => { if (!r.ok) throw new Error(String(r.status)); return r.json(); })
      .then(d => { process.stdout.write(JSON.stringify(d)); })
      .catch(e => { process.stdout.write(JSON.stringify({ error: String(e) })); process.exit(1); });
    });
  `;
    try {
        log("refresh_started", { source: "oauth" });
        const result = execFileSync(process.execPath, ["-e", script], {
            input: refreshToken,
            timeout: 15_000,
            encoding: "utf-8",
            stdio: ["pipe", "pipe", "ignore"],
        });
        const creds = parseOAuthResponse(result, refreshToken);
        if (!creds) {
            log("refresh_failed", {
                source: "oauth",
                error: "no access_token in response",
            });
            return null;
        }
        log("refresh_success", { source: "oauth" });
        return creds;
    }
    catch (err) {
        log("refresh_failed", {
            source: "oauth",
            error: err instanceof Error ? err.message : String(err),
        });
        return null;
    }
}
function refreshViaCli() {
    const maxAttempts = 2;
    for (let i = 0; i < maxAttempts; i++) {
        log("refresh_started", { source: "cli", attempt: i + 1 });
        try {
            execSync("claude -p . --model haiku", {
                timeout: 60_000,
                encoding: "utf-8",
                env: { ...process.env, TERM: "dumb" },
                stdio: "ignore",
                cwd: tmpdir(),
            });
            log("refresh_success", { source: "cli" });
            return;
        }
        catch (err) {
            log("refresh_failed", {
                source: "cli",
                attempt: i + 1,
                error: err instanceof Error ? err.message : String(err),
            });
            // Non-fatal: retry once, then give up
        }
    }
}
export function refreshIfNeeded(account) {
    const target = account ?? getActiveAccount();
    if (!target)
        return null;
    // Pick up external updates to .credentials.json (e.g. switch_claude_account
    // on Windows). Bounded by getCachedCredentials's 30s TTL: fires at most
    // ~2x/min under load. macOS keychain sources stay on the in-memory path;
    // their state is mutated only by our own writeBackCredentials, so no
    // external-update vector exists for them.
    if (target.source === "file") {
        const onDisk = refreshAccount(target.source);
        if (onDisk)
            target.credentials = onDisk;
    }
    const creds = target.credentials;
    if (creds.expiresAt > Date.now() + 60_000)
        return creds;
    log("refresh_needed", {
        source: target.source,
        expiresAt: creds.expiresAt,
        expiresIn: creds.expiresAt - Date.now(),
    });
    // Try direct OAuth refresh first (zero LLM tokens consumed)
    if (creds.refreshToken) {
        const oauthCreds = refreshViaOAuth(creds.refreshToken);
        if (oauthCreds && oauthCreds.expiresAt > Date.now() + 60_000) {
            target.credentials = oauthCreds;
            writeBackCredentials(target.source, oauthCreds);
            return oauthCreds;
        }
    }
    // Fall back to CLI-based refresh (consumes Haiku tokens)
    log("refresh_fallback_cli", { source: target.source });
    refreshViaCli();
    const refreshed = refreshAccount(target.source);
    if (refreshed && refreshed.expiresAt > Date.now() + 60_000) {
        target.credentials = refreshed;
        return refreshed;
    }
    log("refresh_exhausted", {
        source: target.source,
        hadCredentials: !!refreshed,
        expiresAt: refreshed?.expiresAt,
    });
    return null;
}
/**
 * Returns the active account's credentials for auth.json sync purposes.
 * Unlike getCachedCredentials(), this does NOT trigger a refresh.
 * It returns the account's current in-memory credentials if they're still valid.
 * Returns null if no account or credentials are expired.
 */
export function getCredentialsForSync() {
    const account = getActiveAccount();
    if (!account)
        return null;
    const creds = account.credentials;
    if (creds.expiresAt > Date.now() + 60_000) {
        return creds;
    }
    // Credentials are near expiry -- don't refresh here, let the per-request path handle it
    return null;
}
export function getCachedCredentials() {
    const account = getActiveAccount();
    if (!account)
        return null;
    const now = Date.now();
    const cached = accountCacheMap.get(account.source);
    if (cached &&
        now - cached.cachedAt < CREDENTIAL_CACHE_TTL_MS &&
        cached.creds.expiresAt > now + 60_000) {
        log("cache_hit", {
            source: account.source,
            ttlRemaining: CREDENTIAL_CACHE_TTL_MS - (now - cached.cachedAt),
        });
        return cached.creds;
    }
    log("cache_miss", {
        source: account.source,
        reason: cached ? "stale or expiring" : "empty",
    });
    const fresh = refreshIfNeeded(account);
    if (!fresh) {
        log("credentials_unavailable", { source: account.source });
        accountCacheMap.delete(account.source);
        return null;
    }
    accountCacheMap.set(account.source, { creds: fresh, cachedAt: now });
    return fresh;
}
//# sourceMappingURL=credentials.js.map