"use strict";
/**
 * Account registry CRUD operations
 * Handles loading, saving, and syncing the accounts.json file
 */
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.discoverExistingAccounts = exports.setAccountTier = exports.touchAccount = exports.renameAccount = exports.removeAccount = exports.resumeAccount = exports.pauseAccount = exports.setDefaultAccount = exports.registerAccount = exports.hydrateRegistryFromTokenFiles = exports.syncRegistryWithTokenFiles = exports.saveAccountsRegistry = exports.loadAccountsRegistry = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const lockfile = __importStar(require("proper-lockfile"));
const auth_types_1 = require("../auth/auth-types");
const config_generator_1 = require("../config/config-generator");
const types_1 = require("./types");
const token_file_ops_1 = require("./token-file-ops");
const email_account_identity_1 = require("./email-account-identity");
/** Default registry structure */
function createDefaultRegistry() {
    return {
        version: 1,
        providers: {},
    };
}
function ensureProviderRegistry(registry, provider) {
    if (!registry.providers[provider]) {
        registry.providers[provider] = {
            default: 'default',
            accounts: {},
        };
    }
    return registry.providers[provider];
}
function resolveProviderFromTokenType(typeValue) {
    for (const [provider, typeValues] of Object.entries(auth_types_1.PROVIDER_TYPE_VALUES)) {
        if (typeValues.includes(typeValue)) {
            return provider;
        }
    }
    return undefined;
}
const EMAIL_FILE_NAME_PATTERN = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i;
function inferEmailFromTokenFileName(tokenFile, provider) {
    const baseName = tokenFile.replace(/\.json$/i, '');
    const providerPrefix = `${provider}-`;
    const candidate = baseName.startsWith(providerPrefix)
        ? baseName.slice(providerPrefix.length)
        : baseName;
    if (types_1.PROVIDERS_WITHOUT_EMAIL.includes(provider)) {
        const scopedCandidate = candidate.slice(candidate.indexOf('-') + 1);
        if (scopedCandidate &&
            scopedCandidate !== candidate &&
            EMAIL_FILE_NAME_PATTERN.test(scopedCandidate)) {
            return scopedCandidate;
        }
    }
    return EMAIL_FILE_NAME_PATTERN.test(candidate) ? candidate : undefined;
}
function describeRegistryPopulationIssue(issue) {
    const sourceDir = issue.paused ? 'auth-paused' : 'auth';
    return `${sourceDir}/${issue.tokenFile} (${issue.reason})`;
}
function getRegistryPopulationIssueReason(error) {
    if (error instanceof SyntaxError) {
        return 'invalid JSON';
    }
    if (error instanceof Error && error.message.trim()) {
        return error.message.trim();
    }
    return 'unreadable token file';
}
function buildProviderEmailCountKey(provider, email) {
    return `${provider}:${email.trim().toLowerCase()}`;
}
function readRecoverableTokenFiles(options = {}) {
    const tokens = [];
    const issues = [];
    for (const token of (0, token_file_ops_1.listRecoverableTokenFiles)(options)) {
        try {
            const content = fs.readFileSync(token.filePath, 'utf-8');
            const data = JSON.parse(content);
            if (typeof data.type !== 'string' || !data.type.trim()) {
                issues.push({
                    tokenFile: token.tokenFile,
                    paused: token.paused,
                    reason: 'invalid token type',
                });
                continue;
            }
            const provider = resolveProviderFromTokenType(data.type.toLowerCase());
            if (!provider) {
                continue;
            }
            const email = typeof data.email === 'string' && data.email.trim()
                ? data.email.trim()
                : inferEmailFromTokenFileName(token.tokenFile, provider);
            const projectId = typeof data.project_id === 'string' && data.project_id.trim()
                ? data.project_id.trim()
                : null;
            tokens.push({
                tokenFile: token.tokenFile,
                filePath: token.filePath,
                paused: token.paused,
                provider,
                email,
                projectId,
                stats: fs.statSync(token.filePath),
            });
        }
        catch (error) {
            issues.push({
                tokenFile: token.tokenFile,
                paused: token.paused,
                reason: getRegistryPopulationIssueReason(error),
            });
        }
    }
    return { tokens, issues };
}
function buildDuplicateEmailCounts(tokens) {
    const counts = new Map();
    for (const token of tokens) {
        if (!token.email) {
            continue;
        }
        const key = buildProviderEmailCountKey(token.provider, token.email);
        counts.set(key, (counts.get(key) ?? 0) + 1);
    }
    return counts;
}
function populateRegistryFromTokenFiles(registry, options = {}) {
    const { tokens, issues } = readRecoverableTokenFiles(options);
    const duplicateEmailCounts = buildDuplicateEmailCounts(tokens);
    for (const token of tokens) {
        const providerAccounts = ensureProviderRegistry(registry, token.provider);
        const existingEntry = Object.entries(providerAccounts.accounts).find(([, account]) => account.tokenFile === token.tokenFile);
        const existingAccountId = existingEntry?.[0];
        const existingAccount = existingEntry?.[1];
        const resolvedEmail = token.email ?? existingAccount?.email;
        const duplicateEmailCount = resolvedEmail
            ? (duplicateEmailCounts.get(buildProviderEmailCountKey(token.provider, resolvedEmail)) ?? 1)
            : 1;
        const desiredAccountId = types_1.PROVIDERS_WITHOUT_EMAIL.includes(token.provider) && !resolvedEmail
            ? (0, token_file_ops_1.deriveNoEmailProviderAccountId)(token.provider, token.tokenFile, providerAccounts.accounts)
            : !token.email && existingAccountId
                ? existingAccountId
                : (0, email_account_identity_1.buildEmailBackedAccountId)(token.provider, token.tokenFile, resolvedEmail, duplicateEmailCount);
        if (existingEntry && existingEntry[0] !== desiredAccountId) {
            if (!providerAccounts.accounts[desiredAccountId]) {
                providerAccounts.accounts[desiredAccountId] = existingEntry[1];
            }
            if (providerAccounts.default === existingEntry[0]) {
                providerAccounts.default = desiredAccountId;
            }
            delete providerAccounts.accounts[existingEntry[0]];
        }
        if (Object.keys(providerAccounts.accounts).length === 0) {
            providerAccounts.default = desiredAccountId;
        }
        const hydratedAccount = providerAccounts.accounts[desiredAccountId];
        const accountMeta = {
            email: resolvedEmail,
            nickname: hydratedAccount?.nickname ||
                (resolvedEmail
                    ? (0, email_account_identity_1.buildEmailBackedNickname)(token.provider, token.tokenFile, resolvedEmail, duplicateEmailCount)
                    : desiredAccountId),
            tokenFile: token.tokenFile,
            createdAt: hydratedAccount?.createdAt ||
                token.stats.birthtime?.toISOString() ||
                new Date().toISOString(),
            lastUsedAt: hydratedAccount?.lastUsedAt ||
                (token.stats.mtime || token.stats.birthtime || new Date()).toISOString(),
        };
        if (token.paused) {
            accountMeta.paused = true;
            accountMeta.pausedAt = hydratedAccount?.pausedAt || new Date().toISOString();
        }
        else {
            accountMeta.paused = undefined;
            accountMeta.pausedAt = undefined;
        }
        if (token.provider === 'agy') {
            accountMeta.projectId = token.projectId || hydratedAccount?.projectId;
        }
        providerAccounts.accounts[desiredAccountId] = accountMeta;
    }
    return issues;
}
function getCorruptedRegistryBackupPath(registryPath) {
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const backupBasePath = `${registryPath}.corrupt-${timestamp}`;
    let backupPath = `${backupBasePath}.bak`;
    let suffix = 1;
    while (fs.existsSync(backupPath)) {
        backupPath = `${backupBasePath}-${suffix}.bak`;
        suffix++;
    }
    return backupPath;
}
function backupCorruptedAccountsRegistry(registryPath) {
    if (!fs.existsSync(registryPath)) {
        return null;
    }
    const backupPath = getCorruptedRegistryBackupPath(registryPath);
    fs.renameSync(registryPath, backupPath);
    return backupPath;
}
function recoverAccountsRegistryFromCorruption(registryPath) {
    const backupPath = backupCorruptedAccountsRegistry(registryPath);
    const recovered = createDefaultRegistry();
    const issues = populateRegistryFromTokenFiles(recovered, { includePaused: true });
    writeAccountsRegistryToDisk(recovered);
    if (issues.length > 0) {
        console.error(`[!] Recovered corrupted account registry${backupPath ? `; backup saved to ${backupPath}` : ''}, but skipped ${issues.length} token file(s): ${issues
            .map(describeRegistryPopulationIssue)
            .join(', ')}`);
    }
    else if (backupPath) {
        console.error(`[i] Recovered corrupted account registry; backup saved to ${backupPath}`);
    }
    return recovered;
}
function withAccountsRegistryLock(callback) {
    const lockTarget = (0, config_generator_1.getCliproxyDir)();
    let release;
    if (!fs.existsSync(lockTarget)) {
        fs.mkdirSync(lockTarget, { recursive: true, mode: 0o700 });
    }
    try {
        release = lockfile.lockSync(lockTarget, { stale: 10000 });
        return callback();
    }
    finally {
        if (release) {
            try {
                release();
            }
            catch {
                // Best-effort release
            }
        }
    }
}
function readAccountsRegistryFromDisk() {
    const registryPath = (0, token_file_ops_1.getAccountsRegistryPath)();
    if (!fs.existsSync(registryPath)) {
        return createDefaultRegistry();
    }
    const content = fs.readFileSync(registryPath, 'utf-8');
    let data;
    try {
        data = JSON.parse(content);
    }
    catch {
        return recoverAccountsRegistryFromCorruption(registryPath);
    }
    if (!data || typeof data !== 'object' || Array.isArray(data)) {
        return recoverAccountsRegistryFromCorruption(registryPath);
    }
    const parsed = data;
    return {
        version: typeof parsed.version === 'number' ? parsed.version : 1,
        providers: parsed.providers && typeof parsed.providers === 'object'
            ? parsed.providers
            : {},
    };
}
function writeAccountsRegistryToDisk(registry) {
    const registryPath = (0, token_file_ops_1.getAccountsRegistryPath)();
    const dir = path.dirname(registryPath);
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
    }
    const tempPath = `${registryPath}.tmp.${process.pid}`;
    fs.writeFileSync(tempPath, JSON.stringify(registry, null, 2) + '\n', {
        mode: 0o600,
    });
    fs.renameSync(tempPath, registryPath);
}
function mutateAccountsRegistry(mutator) {
    return withAccountsRegistryLock(() => {
        const registry = readAccountsRegistryFromDisk();
        const initialSnapshot = JSON.stringify(registry);
        const result = mutator(registry);
        if (JSON.stringify(registry) !== initialSnapshot) {
            writeAccountsRegistryToDisk(registry);
        }
        return result;
    });
}
/**
 * Load accounts registry
 */
function loadAccountsRegistry() {
    return withAccountsRegistryLock(() => readAccountsRegistryFromDisk());
}
exports.loadAccountsRegistry = loadAccountsRegistry;
/**
 * Save accounts registry
 */
function saveAccountsRegistry(registry) {
    withAccountsRegistryLock(() => {
        writeAccountsRegistryToDisk(registry);
    });
}
exports.saveAccountsRegistry = saveAccountsRegistry;
/**
 * Sync registry with actual token files
 * Removes stale entries where token file no longer exists
 * For paused accounts, checks both auth/ and paused/ directories
 * Called automatically when loading accounts
 */
function syncRegistryWithTokenFiles(registry) {
    const authDir = (0, config_generator_1.getAuthDir)();
    const pausedDir = (0, token_file_ops_1.getPausedDir)();
    let modified = false;
    for (const [_providerName, providerAccounts] of Object.entries(registry.providers)) {
        if (!providerAccounts)
            continue;
        const staleIds = [];
        for (const [accountId, meta] of Object.entries(providerAccounts.accounts)) {
            const tokenPath = path.join(authDir, meta.tokenFile);
            const pausedPath = path.join(pausedDir, meta.tokenFile);
            // For paused accounts, check paused dir; for active accounts, check auth dir
            const expectedPath = meta.paused ? pausedPath : tokenPath;
            // Also accept if file exists in either location (handles edge cases)
            const existsAnywhere = fs.existsSync(tokenPath) || fs.existsSync(pausedPath);
            if (!fs.existsSync(expectedPath) && !existsAnywhere) {
                staleIds.push(accountId);
            }
        }
        // Remove stale accounts
        for (const id of staleIds) {
            delete providerAccounts.accounts[id];
            modified = true;
            // Update default if deleted
            if (providerAccounts.default === id) {
                const remainingIds = Object.keys(providerAccounts.accounts);
                providerAccounts.default = remainingIds[0] || 'default';
            }
        }
    }
    return modified;
}
exports.syncRegistryWithTokenFiles = syncRegistryWithTokenFiles;
/**
 * Build an in-memory view that includes both stale-entry cleanup and any token
 * files not yet persisted into accounts.json. Used by read paths so duplicate
 * email accounts stay visible without forcing a disk write.
 */
function hydrateRegistryFromTokenFiles(registry) {
    const removedStaleEntries = syncRegistryWithTokenFiles(registry);
    const populationIssues = populateRegistryFromTokenFiles(registry);
    return removedStaleEntries || populationIssues.length > 0;
}
exports.hydrateRegistryFromTokenFiles = hydrateRegistryFromTokenFiles;
/**
 * Register a new account
 * Called after successful OAuth to record the account
 *
 * For providers without email (kiro, ghcp):
 * - internal accountId is derived from token metadata
 * - nickname is optional metadata
 *
 * For providers with email:
 * - email is used as accountId
 * - nickname is auto-generated from email if not provided
 */
function registerAccount(provider, tokenFile, email, nickname, projectId) {
    return mutateAccountsRegistry((registry) => {
        syncRegistryWithTokenFiles(registry);
        if (!registry.providers[provider]) {
            registry.providers[provider] = {
                default: 'default',
                accounts: {},
            };
        }
        const providerAccounts = registry.providers[provider];
        if (!providerAccounts) {
            throw new Error('Failed to initialize provider accounts');
        }
        let accountId;
        let accountNickname;
        if (types_1.PROVIDERS_WITHOUT_EMAIL.includes(provider)) {
            accountId = email
                ? (0, token_file_ops_1.extractAccountIdFromTokenFile)(tokenFile, email)
                : (0, token_file_ops_1.deriveNoEmailProviderAccountId)(provider, tokenFile, providerAccounts.accounts);
            const existingAccount = providerAccounts.accounts[accountId];
            if (nickname) {
                const validationError = (0, token_file_ops_1.validateNickname)(nickname);
                if (validationError) {
                    throw new Error(validationError);
                }
                const existingAccounts = Object.entries(providerAccounts.accounts).map(([id, account]) => ({
                    id,
                    nickname: account.nickname,
                }));
                if ((0, token_file_ops_1.hasAccountNameConflict)(existingAccounts, nickname, accountId)) {
                    throw new Error(`An account with nickname "${nickname}" already exists for ${provider}. ` +
                        `Choose a different nickname.`);
                }
            }
            accountNickname =
                nickname || existingAccount?.nickname || (email ? (0, token_file_ops_1.generateNickname)(email) : accountId);
        }
        else {
            const sameEmailEntries = email
                ? Object.entries(providerAccounts.accounts).filter(([, account]) => account.email?.toLowerCase() === email.toLowerCase())
                : [];
            const duplicateEmailCount = email
                ? new Set([...sameEmailEntries.map(([, account]) => account.tokenFile), tokenFile]).size
                : 1;
            if (email && duplicateEmailCount > 1) {
                for (const [existingId, existingMeta] of sameEmailEntries) {
                    const migratedId = (0, email_account_identity_1.buildEmailBackedAccountId)(provider, existingMeta.tokenFile, email, duplicateEmailCount);
                    if (migratedId === existingId || providerAccounts.accounts[migratedId]) {
                        continue;
                    }
                    providerAccounts.accounts[migratedId] = existingMeta;
                    if (providerAccounts.default === existingId) {
                        providerAccounts.default = migratedId;
                    }
                    delete providerAccounts.accounts[existingId];
                }
            }
            accountId = (0, email_account_identity_1.buildEmailBackedAccountId)(provider, tokenFile, email, duplicateEmailCount);
            const existingAccount = providerAccounts.accounts[accountId];
            if (nickname) {
                const validationError = (0, token_file_ops_1.validateNickname)(nickname);
                if (validationError) {
                    throw new Error(validationError);
                }
                const existingAccounts = Object.entries(providerAccounts.accounts).map(([id, account]) => ({
                    id,
                    nickname: account.nickname,
                }));
                if ((0, token_file_ops_1.hasAccountNameConflict)(existingAccounts, nickname, accountId)) {
                    throw new Error(`An account with nickname "${nickname}" already exists for ${provider}. ` +
                        `Choose a different nickname.`);
                }
            }
            accountNickname =
                nickname ||
                    existingAccount?.nickname ||
                    (0, email_account_identity_1.buildEmailBackedNickname)(provider, tokenFile, email, duplicateEmailCount);
        }
        const isFirstAccount = Object.keys(providerAccounts.accounts).length === 0;
        const existingAccount = providerAccounts.accounts[accountId];
        const accountMeta = {
            email,
            nickname: accountNickname,
            tokenFile,
            createdAt: existingAccount?.createdAt || new Date().toISOString(),
            lastUsedAt: new Date().toISOString(),
        };
        if (provider === 'agy' && projectId) {
            accountMeta.projectId = projectId;
        }
        providerAccounts.accounts[accountId] = accountMeta;
        if (isFirstAccount) {
            providerAccounts.default = accountId;
        }
        return {
            id: accountId,
            provider,
            isDefault: accountId === providerAccounts.default,
            email,
            nickname: accountNickname,
            tokenFile,
            createdAt: providerAccounts.accounts[accountId].createdAt,
            lastUsedAt: providerAccounts.accounts[accountId].lastUsedAt,
            projectId: providerAccounts.accounts[accountId].projectId,
        };
    });
}
exports.registerAccount = registerAccount;
/**
 * Set default account for a provider
 */
function setDefaultAccount(provider, accountId) {
    return mutateAccountsRegistry((registry) => {
        const providerAccounts = registry.providers[provider];
        if (!providerAccounts || !providerAccounts.accounts[accountId]) {
            return false;
        }
        providerAccounts.default = accountId;
        return true;
    });
}
exports.setDefaultAccount = setDefaultAccount;
/**
 * Pause an account (skip in quota rotation)
 * Moves token file to paused/ subdir so CLIProxyAPI won't discover it
 */
function pauseAccount(provider, accountId) {
    return mutateAccountsRegistry((registry) => {
        const providerAccounts = registry.providers[provider];
        if (!providerAccounts?.accounts[accountId]) {
            return false;
        }
        const accountMeta = providerAccounts.accounts[accountId];
        if (accountMeta.paused) {
            return true;
        }
        if (!(0, token_file_ops_1.moveTokenToPaused)(accountMeta.tokenFile)) {
            return false;
        }
        providerAccounts.accounts[accountId].paused = true;
        providerAccounts.accounts[accountId].pausedAt = new Date().toISOString();
        return true;
    });
}
exports.pauseAccount = pauseAccount;
/**
 * Resume a paused account
 * Moves token file back from paused/ to auth/ so CLIProxyAPI can discover it
 */
function resumeAccount(provider, accountId) {
    return mutateAccountsRegistry((registry) => {
        const providerAccounts = registry.providers[provider];
        if (!providerAccounts?.accounts[accountId]) {
            return false;
        }
        const accountMeta = providerAccounts.accounts[accountId];
        if (!accountMeta.paused) {
            return true;
        }
        if (!(0, token_file_ops_1.moveTokenFromPaused)(accountMeta.tokenFile)) {
            return false;
        }
        providerAccounts.accounts[accountId].paused = false;
        providerAccounts.accounts[accountId].pausedAt = undefined;
        return true;
    });
}
exports.resumeAccount = resumeAccount;
/**
 * Remove an account
 */
function removeAccount(provider, accountId) {
    return mutateAccountsRegistry((registry) => {
        const providerAccounts = registry.providers[provider];
        if (!providerAccounts || !providerAccounts.accounts[accountId]) {
            return false;
        }
        const tokenFile = providerAccounts.accounts[accountId].tokenFile;
        if (!(0, token_file_ops_1.deleteTokenFile)(tokenFile)) {
            return false;
        }
        delete providerAccounts.accounts[accountId];
        const remainingAccounts = Object.keys(providerAccounts.accounts);
        if (providerAccounts.default === accountId && remainingAccounts.length > 0) {
            providerAccounts.default = remainingAccounts[0];
        }
        return true;
    });
}
exports.removeAccount = removeAccount;
/**
 * Rename an account's nickname
 */
function renameAccount(provider, accountId, newNickname) {
    const validationError = (0, token_file_ops_1.validateNickname)(newNickname);
    if (validationError) {
        throw new Error(validationError);
    }
    return mutateAccountsRegistry((registry) => {
        const providerAccounts = registry.providers[provider];
        if (!providerAccounts?.accounts[accountId]) {
            return false;
        }
        const existingAccounts = Object.entries(providerAccounts.accounts).map(([id, account]) => ({
            id,
            nickname: account.nickname,
        }));
        if ((0, token_file_ops_1.hasAccountNameConflict)(existingAccounts, newNickname, accountId)) {
            throw new Error(`Nickname "${newNickname}" is already used by another account`);
        }
        providerAccounts.accounts[accountId].nickname = newNickname;
        return true;
    });
}
exports.renameAccount = renameAccount;
/**
 * Update last used timestamp for an account
 */
function touchAccount(provider, accountId) {
    mutateAccountsRegistry((registry) => {
        const providerAccounts = registry.providers[provider];
        if (providerAccounts?.accounts[accountId]) {
            providerAccounts.accounts[accountId].lastUsedAt = new Date().toISOString();
        }
    });
}
exports.touchAccount = touchAccount;
/**
 * Update account tier
 */
function setAccountTier(provider, accountId, tier) {
    return mutateAccountsRegistry((registry) => {
        const providerAccounts = registry.providers[provider];
        if (!providerAccounts?.accounts[accountId]) {
            return false;
        }
        providerAccounts.accounts[accountId].tier = tier;
        return true;
    });
}
exports.setAccountTier = setAccountTier;
/**
 * Auto-discover accounts from existing token files
 * Called during migration or first run to populate accounts registry
 *
 * For kiro/ghcp providers without email, generates unique accountId from:
 * 1. OAuth provider + profile ID from filename (e.g., github-ABC123)
 * 2. Fallback: provider + index (e.g., kiro-1, kiro-2)
 */
function discoverExistingAccounts() {
    if (!fs.existsSync((0, config_generator_1.getAuthDir)())) {
        return;
    }
    mutateAccountsRegistry((registry) => {
        syncRegistryWithTokenFiles(registry);
        populateRegistryFromTokenFiles(registry);
    });
}
exports.discoverExistingAccounts = discoverExistingAccounts;
//# sourceMappingURL=registry.js.map