"use strict";
/**
 * Token Manager for CLIProxyAPI
 *
 * Handles OAuth token storage, retrieval, and validation.
 * Tokens are stored in ~/.ccs/cliproxy/auth/ directory.
 */
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.ensureTokenValid = exports.displayAuthStatus = exports.registerAccountFromToken = exports.clearAuth = exports.getAllAuthStatus = exports.getAuthStatus = exports.isAuthenticated = exports.findNewTokenSnapshotForAuthAttempt = exports.findNewTokenSnapshot = exports.listProviderTokenSnapshots = exports.isTokenFileForProvider = exports.getProviderTokenDir = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto_1 = require("crypto");
const profile_detector_1 = require("../../auth/profile-detector");
const config_generator_1 = require("../config/config-generator");
const account_manager_1 = require("../accounts/account-manager");
const token_file_ops_1 = require("../accounts/token-file-ops");
const email_account_identity_1 = require("../accounts/email-account-identity");
const provider_capabilities_1 = require("../provider-capabilities");
const auth_types_1 = require("./auth-types");
/**
 * Get token directory for provider
 */
function getProviderTokenDir(provider) {
    return (0, config_generator_1.getProviderAuthDir)(provider);
}
exports.getProviderTokenDir = getProviderTokenDir;
/**
 * Check if a JSON file contains a token for the given provider
 * Reads the file and checks the "type" field
 */
function isTokenFileForProvider(filePath, provider) {
    try {
        const content = fs.readFileSync(filePath, 'utf-8');
        const data = JSON.parse(content);
        const typeValue = (data.type || '').toLowerCase();
        const validTypes = auth_types_1.PROVIDER_TYPE_VALUES[provider] || [];
        return validTypes.includes(typeValue);
    }
    catch {
        return false;
    }
}
exports.isTokenFileForProvider = isTokenFileForProvider;
function buildTokenFingerprint(content) {
    return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
}
function listTokenCandidates(provider, tokenDir) {
    if (!fs.existsSync(tokenDir)) {
        return [];
    }
    const files = fs.readdirSync(tokenDir);
    const jsonFiles = files.filter((file) => file.endsWith('.json'));
    const existingAccounts = (0, account_manager_1.getProviderAccounts)(provider);
    const rawCandidates = jsonFiles.flatMap((file) => {
        const filePath = path.join(tokenDir, file);
        if (!isTokenFileForProvider(filePath, provider)) {
            return [];
        }
        const content = fs.readFileSync(filePath, 'utf-8');
        const data = JSON.parse(content);
        const email = data.email || undefined;
        const projectId = data.project_id || undefined;
        const stats = fs.statSync(filePath);
        return [
            {
                file,
                filePath,
                content,
                email,
                projectId,
                mtimeMs: stats.mtimeMs,
                alreadyRegistered: existingAccounts.some((account) => account.tokenFile === file),
            },
        ];
    });
    const duplicateEmailCounts = new Map();
    const duplicateEmailTokenSets = new Map();
    for (const account of existingAccounts) {
        if (!account.email)
            continue;
        const key = account.email.toLowerCase();
        const tokenSet = duplicateEmailTokenSets.get(key) ?? new Set();
        tokenSet.add(account.tokenFile);
        duplicateEmailTokenSets.set(key, tokenSet);
    }
    for (const candidate of rawCandidates) {
        if (!candidate.email)
            continue;
        const key = candidate.email.toLowerCase();
        const tokenSet = duplicateEmailTokenSets.get(key) ?? new Set();
        tokenSet.add(candidate.file);
        duplicateEmailTokenSets.set(key, tokenSet);
    }
    for (const [key, tokenSet] of duplicateEmailTokenSets) {
        duplicateEmailCounts.set(key, tokenSet.size);
    }
    return rawCandidates
        .map((rawCandidate) => {
        const duplicateEmailCount = rawCandidate.email
            ? (duplicateEmailCounts.get(rawCandidate.email.toLowerCase()) ?? 1)
            : 1;
        const accountId = rawCandidate.email
            ? (0, email_account_identity_1.buildEmailBackedAccountId)(provider, rawCandidate.file, rawCandidate.email, duplicateEmailCount)
            : (0, token_file_ops_1.extractAccountIdFromTokenFile)(rawCandidate.file, rawCandidate.email);
        return {
            ...rawCandidate,
            accountId,
            fingerprint: buildTokenFingerprint(rawCandidate.content),
        };
    })
        .sort((left, right) => right.mtimeMs - left.mtimeMs);
}
function listProviderTokenSnapshots(provider, tokenDir = getProviderTokenDir(provider)) {
    return listTokenCandidates(provider, tokenDir).map((candidate) => ({
        file: candidate.file,
        mtimeMs: candidate.mtimeMs,
        accountId: candidate.accountId,
        fingerprint: candidate.fingerprint,
    }));
}
exports.listProviderTokenSnapshots = listProviderTokenSnapshots;
function findNewTokenSnapshot(currentTokenFiles, knownTokenFiles, expectedAccountId) {
    const knownSnapshotsByFile = new Map(knownTokenFiles.map((snapshot) => [snapshot.file, snapshot]));
    return (currentTokenFiles.find((snapshot) => {
        const knownSnapshot = knownSnapshotsByFile.get(snapshot.file);
        if (!expectedAccountId) {
            return !knownSnapshot;
        }
        const matchesExpectedAccount = snapshot.file === expectedAccountId || snapshot.accountId === expectedAccountId;
        if (!matchesExpectedAccount) {
            return false;
        }
        if (!knownSnapshot) {
            return true;
        }
        return (snapshot.fingerprint !== knownSnapshot.fingerprint ||
            snapshot.mtimeMs !== knownSnapshot.mtimeMs);
    }) || null);
}
exports.findNewTokenSnapshot = findNewTokenSnapshot;
function findNewTokenSnapshotForAuthAttempt(provider, tokenDir, knownTokenFiles, expectedAccountId) {
    return findNewTokenSnapshot(listProviderTokenSnapshots(provider, tokenDir), knownTokenFiles, expectedAccountId);
}
exports.findNewTokenSnapshotForAuthAttempt = findNewTokenSnapshotForAuthAttempt;
/**
 * Check if provider has valid authentication
 * CLIProxyAPI stores OAuth tokens as JSON files in the auth directory.
 * Detection strategy:
 * 1. First check by filename prefix (fast path)
 * 2. If no match, check JSON content for "type" field (Gemini uses {email}-{projectID}.json without prefix)
 */
function isAuthenticated(provider) {
    const tokenDir = getProviderTokenDir(provider);
    if (!fs.existsSync(tokenDir)) {
        return false;
    }
    const validPrefixes = auth_types_1.PROVIDER_AUTH_PREFIXES[provider] || [];
    try {
        const files = fs.readdirSync(tokenDir);
        const jsonFiles = files.filter((f) => f.endsWith('.json') || f.endsWith('.token') || f === 'credentials');
        // Strategy 1: Check by filename prefix (fast path for antigravity, codex)
        const prefixMatch = jsonFiles.some((f) => {
            const lowerFile = f.toLowerCase();
            return validPrefixes.some((prefix) => lowerFile.startsWith(prefix));
        });
        if (prefixMatch)
            return true;
        // Strategy 2: Check JSON content for "type" field (needed for Gemini)
        for (const f of jsonFiles) {
            const filePath = path.join(tokenDir, f);
            if (isTokenFileForProvider(filePath, provider)) {
                return true;
            }
        }
        return false;
    }
    catch {
        return false;
    }
}
exports.isAuthenticated = isAuthenticated;
/**
 * Get detailed auth status for provider
 * Uses same detection strategy as isAuthenticated: prefix first, then content
 */
function getAuthStatus(provider) {
    const tokenDir = getProviderTokenDir(provider);
    let tokenFiles = [];
    let lastAuth;
    const validPrefixes = auth_types_1.PROVIDER_AUTH_PREFIXES[provider] || [];
    if (fs.existsSync(tokenDir)) {
        const files = fs.readdirSync(tokenDir);
        const jsonFiles = files.filter((f) => f.endsWith('.json') || f.endsWith('.token') || f === 'credentials');
        // Check each file: by prefix OR by content
        tokenFiles = jsonFiles.filter((f) => {
            const lowerFile = f.toLowerCase();
            // Strategy 1: prefix match
            if (validPrefixes.some((prefix) => lowerFile.startsWith(prefix))) {
                return true;
            }
            // Strategy 2: content match (for Gemini tokens without prefix)
            const filePath = path.join(tokenDir, f);
            return isTokenFileForProvider(filePath, provider);
        });
        // Get most recent modification time
        for (const file of tokenFiles) {
            const filePath = path.join(tokenDir, file);
            try {
                const stats = fs.statSync(filePath);
                if (!lastAuth || stats.mtime > lastAuth) {
                    lastAuth = stats.mtime;
                }
            }
            catch {
                // Skip if can't stat file
            }
        }
    }
    // Get registered accounts for multi-account support
    const accounts = (0, account_manager_1.getProviderAccounts)(provider);
    const defaultAccount = (0, account_manager_1.getDefaultAccount)(provider);
    return {
        provider,
        authenticated: tokenFiles.length > 0,
        tokenDir,
        tokenFiles,
        lastAuth,
        accounts,
        defaultAccount: defaultAccount?.id,
    };
}
exports.getAuthStatus = getAuthStatus;
/**
 * Get auth status for all providers
 */
function getAllAuthStatus() {
    const providers = [...profile_detector_1.CLIPROXY_PROFILES];
    return providers.map(getAuthStatus);
}
exports.getAllAuthStatus = getAllAuthStatus;
/**
 * Clear authentication for provider
 * Only removes files belonging to the specified provider (by prefix or content)
 * Does NOT remove the shared auth directory or other providers' files
 */
function clearAuth(provider) {
    const tokenDir = getProviderTokenDir(provider);
    if (!fs.existsSync(tokenDir)) {
        return false;
    }
    const validPrefixes = auth_types_1.PROVIDER_AUTH_PREFIXES[provider] || [];
    const files = fs.readdirSync(tokenDir);
    let removedCount = 0;
    // Only remove files that belong to this provider
    for (const file of files) {
        const filePath = path.join(tokenDir, file);
        const lowerFile = file.toLowerCase();
        // Check by prefix first (fast path)
        const matchesByPrefix = validPrefixes.some((prefix) => lowerFile.startsWith(prefix));
        // If no prefix match, check by content (for Gemini tokens without prefix)
        const matchesByContent = !matchesByPrefix && isTokenFileForProvider(filePath, provider);
        if (matchesByPrefix || matchesByContent) {
            try {
                fs.unlinkSync(filePath);
                removedCount++;
            }
            catch {
                // Failed to remove - skip
            }
        }
    }
    // DO NOT remove the shared auth directory - other providers may still have tokens
    return removedCount > 0;
}
exports.clearAuth = clearAuth;
/**
 * Register account from newly created token file
 * Scans auth directory for new token and extracts email
 * @param provider - The CLIProxy provider
 * @param tokenDir - Directory containing token files
 * @param nickname - Optional nickname (uses auto-generated from email if not provided)
 */
function registerAccountFromToken(provider, tokenDir, nickname, verbose = false, expectedAccountId) {
    const { registerAccount } = require('../accounts/account-manager');
    let selectedCandidate = null;
    try {
        const candidates = listTokenCandidates(provider, tokenDir);
        const existingAccounts = (0, account_manager_1.getProviderAccounts)(provider);
        if (expectedAccountId) {
            selectedCandidate =
                candidates.find((candidate) => candidate.accountId === expectedAccountId) ||
                    candidates.find((candidate) => candidate.file === expectedAccountId) ||
                    candidates.find((candidate) => {
                        const existingAccount = existingAccounts.find((account) => account.id === expectedAccountId);
                        return !!existingAccount && existingAccount.tokenFile === candidate.file;
                    }) ||
                    null;
        }
        else {
            selectedCandidate = candidates[0] || null;
        }
        if (!selectedCandidate) {
            if (verbose && expectedAccountId) {
                console.error(`[auth] No token matched the expected account ${expectedAccountId}; refusing ambiguous registration`);
            }
            return null;
        }
        const account = registerAccount(provider, selectedCandidate.file, selectedCandidate.email, nickname, selectedCandidate.projectId);
        uploadTokenToRemoteAsync(selectedCandidate.filePath, verbose);
        return account;
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        if (verbose) {
            console.error(`[auth] Failed to register token-backed account: ${message}`);
        }
        if (selectedCandidate && !selectedCandidate.alreadyRegistered && !expectedAccountId) {
            (0, token_file_ops_1.deleteTokenFile)(selectedCandidate.file);
        }
        if (expectedAccountId && verbose) {
            console.error(`[auth] Reauthentication target ${expectedAccountId} did not resolve cleanly from the new token`);
        }
        return null;
    }
}
exports.registerAccountFromToken = registerAccountFromToken;
/**
 * Upload token to remote server asynchronously (fire and forget).
 * Only runs if remote mode is enabled. Logs success/failure via uploadTokenToRemote.
 * Does not block the OAuth flow - local token is always valid regardless of upload result.
 *
 * @param tokenPath - Path to the token file
 * @param verbose - Enable verbose logging for upload progress
 */
function uploadTokenToRemoteAsync(tokenPath, verbose) {
    // Dynamic import to avoid circular dependencies
    Promise.resolve().then(() => __importStar(require('../management/remote-token-uploader'))).then(({ uploadTokenToRemote, isRemoteUploadEnabled }) => {
        if (isRemoteUploadEnabled()) {
            // uploadTokenToRemote handles its own success/failure logging
            // On failure, show additional warning so users know local token is still valid
            uploadTokenToRemote(tokenPath, verbose)
                .then((success) => {
                if (!success) {
                    console.error('\n[!] Remote upload failed - token saved locally only. Run "ccs tokens upload" to retry.');
                }
            })
                .catch((err) => {
                // Unexpected error (not handled by uploadTokenToRemote)
                const message = err instanceof Error ? err.message : String(err);
                console.error(`[token-manager] Unexpected upload error: ${message}`);
                console.error('[!] Token saved locally. Run "ccs tokens upload" to sync to remote server.');
            });
        }
    })
        .catch((err) => {
        // Module load failed - log for debugging
        const message = err instanceof Error ? err.message : String(err);
        console.error(`[token-manager] Failed to load remote-token-uploader: ${message}`);
    });
}
/**
 * Display auth status for all providers
 */
function displayAuthStatus() {
    console.log('CLIProxy Authentication Status:');
    console.log('');
    const statuses = getAllAuthStatus();
    for (const status of statuses) {
        const oauthConfig = (0, auth_types_1.getOAuthConfig)(status.provider);
        const icon = status.authenticated ? '[OK]' : '[!]';
        const authStatus = status.authenticated ? 'Authenticated' : 'Not authenticated';
        const lastAuthStr = status.lastAuth ? ` (last: ${status.lastAuth.toLocaleDateString()})` : '';
        console.log(`${icon} ${oauthConfig.displayName}: ${authStatus}${lastAuthStr}`);
    }
    console.log('');
    console.log('To authenticate: ccs <provider> --auth');
    console.log('To logout:       ccs <provider> --logout');
}
exports.displayAuthStatus = displayAuthStatus;
/**
 * Ensure OAuth token is valid for provider, refreshing if expired or expiring soon.
 * This prevents UND_ERR_SOCKET errors caused by expired tokens during API calls.
 *
 * Refresh responsibility:
 * - gemini: CCS refreshes directly via Google OAuth
 * - codex, agy, kiro, ghcp, qwen, iflow: CLIProxyAPIPlus handles refresh
 *   automatically in background (e.g. kiro refreshes every 1 min).
 *   CCS only checks if token file exists (authentication state).
 * - claude: not yet implemented
 *
 * @param provider The CLIProxy provider
 * @param verbose Log progress if true
 * @returns Object with valid status and whether refresh occurred
 */
async function ensureTokenValid(provider, _verbose = false) {
    if ((0, provider_capabilities_1.getTokenRefreshOwnership)(provider) === 'ccs') {
        return {
            valid: false,
            refreshed: false,
            error: `CCS-managed token validation is not available for ${provider}`,
        };
    }
    // Runtime-managed providers refresh upstream. CCS only verifies auth material exists locally.
    return { valid: isAuthenticated(provider), refreshed: false };
}
exports.ensureTokenValid = ensureTokenValid;
//# sourceMappingURL=token-manager.js.map