"use strict";
/**
 * Token file operations and path management
 */
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.findAccountNameMatch = exports.hasAccountNameConflict = exports.validateNickname = exports.generateNickname = exports.deriveNoEmailProviderAccountId = exports.extractAccountIdFromTokenFile = exports.tokenFileExists = exports.deleteTokenFile = exports.moveTokenFromPaused = exports.moveTokenToPaused = exports.getAccountTokenPath = exports.listRecoverableTokenFiles = exports.getPausedDir = exports.getAccountsRegistryPath = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const config_generator_1 = require("../config/config-generator");
/**
 * Get path to accounts registry file
 */
function getAccountsRegistryPath() {
    return path.join((0, config_generator_1.getCliproxyDir)(), 'accounts.json');
}
exports.getAccountsRegistryPath = getAccountsRegistryPath;
/**
 * Get path to paused tokens directory
 * Paused tokens are moved here so CLIProxyAPI won't discover them
 *
 * Uses sibling directory (auth-paused/) instead of subdirectory (auth/paused/)
 * because CLIProxyAPI's watcher uses filepath.Walk() which recursively scans
 * all subdirectories of auth/. A sibling directory is completely outside
 * CLIProxyAPI's scan path, preventing token refresh loops.
 */
function getPausedDir() {
    return path.join((0, config_generator_1.getCliproxyDir)(), 'auth-paused');
}
exports.getPausedDir = getPausedDir;
/**
 * List token files that can be used to rebuild the accounts registry.
 * Active auth tokens win over paused duplicates with the same filename.
 */
function listRecoverableTokenFiles(options = {}) {
    const tokenFiles = [];
    const seen = new Set();
    const sources = [{ dir: (0, config_generator_1.getAuthDir)(), paused: false }];
    if (options.includePaused) {
        sources.push({ dir: getPausedDir(), paused: true });
    }
    for (const source of sources) {
        if (!fs.existsSync(source.dir)) {
            continue;
        }
        for (const tokenFile of fs.readdirSync(source.dir)) {
            if (!tokenFile.endsWith('.json') || seen.has(tokenFile)) {
                continue;
            }
            tokenFiles.push({
                tokenFile,
                filePath: path.join(source.dir, tokenFile),
                paused: source.paused,
            });
            seen.add(tokenFile);
        }
    }
    return tokenFiles;
}
exports.listRecoverableTokenFiles = listRecoverableTokenFiles;
/**
 * Get token file path for an account
 * Returns path in paused/ dir if account is paused, otherwise auth/
 */
function getAccountTokenPath(account) {
    const baseDir = account.paused ? getPausedDir() : (0, config_generator_1.getAuthDir)();
    return path.join(baseDir, account.tokenFile);
}
exports.getAccountTokenPath = getAccountTokenPath;
/**
 * Move token file to paused directory
 * Creates paused directory if needed, idempotent
 */
function moveTokenToPaused(tokenFile) {
    const authDir = (0, config_generator_1.getAuthDir)();
    const pausedDir = getPausedDir();
    const tokenPath = path.join(authDir, tokenFile);
    const pausedPath = path.join(pausedDir, tokenFile);
    // Skip if already in paused directory
    if (!fs.existsSync(tokenPath)) {
        return true;
    }
    try {
        // Create paused directory if it doesn't exist
        if (!fs.existsSync(pausedDir)) {
            fs.mkdirSync(pausedDir, { recursive: true, mode: 0o700 });
        }
        fs.renameSync(tokenPath, pausedPath);
        return true;
    }
    catch {
        // File operation failed, caller handles recovery
        return false;
    }
}
exports.moveTokenToPaused = moveTokenToPaused;
/**
 * Move token file from paused directory back to auth
 * Idempotent
 */
function moveTokenFromPaused(tokenFile) {
    const authDir = (0, config_generator_1.getAuthDir)();
    const pausedDir = getPausedDir();
    const tokenPath = path.join(authDir, tokenFile);
    const pausedPath = path.join(pausedDir, tokenFile);
    // Skip if already in auth directory
    if (!fs.existsSync(pausedPath)) {
        return true;
    }
    try {
        fs.renameSync(pausedPath, tokenPath);
        return true;
    }
    catch {
        // File operation failed, caller handles recovery
        return false;
    }
}
exports.moveTokenFromPaused = moveTokenFromPaused;
/**
 * Delete token file from both auth and paused directories
 * Idempotent
 */
function deleteTokenFile(tokenFile) {
    const tokenPath = path.join((0, config_generator_1.getAuthDir)(), tokenFile);
    const pausedPath = path.join(getPausedDir(), tokenFile);
    let success = true;
    // Delete from auth directory
    if (fs.existsSync(tokenPath)) {
        try {
            fs.unlinkSync(tokenPath);
        }
        catch {
            success = false;
        }
    }
    // Also delete from paused directory if it exists there
    if (fs.existsSync(pausedPath)) {
        try {
            fs.unlinkSync(pausedPath);
        }
        catch {
            success = false;
        }
    }
    return success;
}
exports.deleteTokenFile = deleteTokenFile;
/**
 * Check if token file exists in either auth or paused directory
 */
function tokenFileExists(tokenFile) {
    const tokenPath = path.join((0, config_generator_1.getAuthDir)(), tokenFile);
    const pausedPath = path.join(getPausedDir(), tokenFile);
    return fs.existsSync(tokenPath) || fs.existsSync(pausedPath);
}
exports.tokenFileExists = tokenFileExists;
/**
 * Extract unique account ID from token filename when email is unavailable
 * For Kiro/GHCP OAuth, filenames are: <provider>-<oauth>-<profile_id>.json
 * Extracts: <oauth>-<profile_id> as unique identifier
 * @example kiro-github-ABC123.json → github-ABC123
 * @example ghcp-amazon-XYZ789.json → amazon-XYZ789
 * @example kiro-nomail.json → default (no OAuth structure)
 */
function extractAccountIdFromTokenFile(filename, email) {
    if (email)
        return email;
    // Pattern: <provider>-<oauth>-<profile_id>.json → extract <oauth>-<profile_id>
    // Requires at least 2 hyphens to distinguish from simple filenames like "kiro-nomail.json"
    const match = filename.match(/^[^-]+-([^-]+-[^.]+)\.json$/);
    if (match)
        return match[1];
    return 'default';
}
exports.extractAccountIdFromTokenFile = extractAccountIdFromTokenFile;
/**
 * Derive a collision-safe internal account ID for providers that may not expose email.
 * Reuses the existing entry when the token file is already known, otherwise prefers the
 * filename-derived ID before falling back to provider-scoped sequential IDs.
 */
function deriveNoEmailProviderAccountId(provider, tokenFile, existingAccounts) {
    const existingEntries = Object.entries(existingAccounts);
    const existingEntry = existingEntries.find(([, account]) => account.tokenFile === tokenFile);
    if (existingEntry) {
        return existingEntry[0];
    }
    const extractedId = extractAccountIdFromTokenFile(tokenFile);
    const lowerExtractedId = extractedId.toLowerCase();
    if (extractedId !== 'default' &&
        !existingEntries.some(([existingId, account]) => existingId.toLowerCase() === lowerExtractedId ||
            account.nickname?.toLowerCase() === lowerExtractedId)) {
        return extractedId;
    }
    let index = 1;
    while (existingEntries.some(([existingId, account]) => existingId.toLowerCase() === `${provider}-${index}` ||
        account.nickname?.toLowerCase() === `${provider}-${index}`)) {
        index++;
    }
    return `${provider}-${index}`;
}
exports.deriveNoEmailProviderAccountId = deriveNoEmailProviderAccountId;
/**
 * Generate nickname from email
 * Takes prefix before @ symbol, sanitizes whitespace
 * Validation: 1-50 chars, any non-whitespace (permissive per user preference)
 */
function generateNickname(email) {
    if (!email)
        return 'default';
    const prefix = email.split('@')[0];
    // Sanitize: remove whitespace, limit to 50 chars
    return prefix.replace(/\s+/g, '').slice(0, 50) || 'default';
}
exports.generateNickname = generateNickname;
/**
 * Validate nickname
 * Rules: 1-50 chars, no whitespace, URL-safe, no reserved patterns
 * @returns null if valid, error message if invalid
 */
function validateNickname(nickname) {
    if (!nickname || nickname.length === 0) {
        return 'Nickname is required';
    }
    if (nickname.length > 50) {
        return 'Nickname must be 50 characters or less';
    }
    if (/\s/.test(nickname)) {
        return 'Nickname cannot contain whitespace';
    }
    // Block URL-unsafe chars that break routing
    if (/[%\/&?#]/.test(nickname)) {
        return 'Nickname cannot contain special URL characters (%, /, &, ?, #)';
    }
    // Block reserved patterns used by auto-discovery (kiro-1, ghcp-2, etc.)
    if (/^(kiro|ghcp)-\d+$/i.test(nickname)) {
        return 'Nickname cannot match reserved pattern (kiro-N, ghcp-N)';
    }
    return null;
}
exports.validateNickname = validateNickname;
/**
 * Check whether a nickname would collide with any existing account ID or nickname.
 */
function hasAccountNameConflict(accounts, candidateName, excludeAccountId) {
    const normalizedCandidate = candidateName.toLowerCase();
    const normalizedExcludedId = excludeAccountId?.toLowerCase();
    return accounts.some((account) => {
        if (normalizedExcludedId && account.id.toLowerCase() === normalizedExcludedId) {
            return false;
        }
        return (account.id.toLowerCase() === normalizedCandidate ||
            account.nickname?.toLowerCase() === normalizedCandidate);
    });
}
exports.hasAccountNameConflict = hasAccountNameConflict;
/**
 * Find the existing account that already owns the supplied id/nickname.
 */
function findAccountNameMatch(accounts, candidateName) {
    const normalizedCandidate = candidateName.toLowerCase();
    return (accounts.find((account) => account.id.toLowerCase() === normalizedCandidate ||
        account.nickname?.toLowerCase() === normalizedCandidate) || null);
}
exports.findAccountNameMatch = findAccountNameMatch;
//# sourceMappingURL=token-file-ops.js.map