/**
 * Internal Logger for CLI command tracing
 *
 * This logger captures all CLI execution details for debugging purposes.
 * It maintains two files per command execution:
 * 1. session.json - Command metadata, environment, and system info
 * 2. logs.jsonl - JSON Lines format log entries
 *
 * The logger automatically cleans up old logs, keeping only the most recent execution.
 */
import { appendFileSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync, readFileSync, } from 'node:fs';
import { join, resolve } from 'node:path';
import { homedir, platform, arch, cpus, totalmem } from 'node:os';
import { randomUUID } from 'node:crypto';
// Sensitive environment variable patterns to mask
const SENSITIVE_ENV_PATTERNS = [
    /KEY/i, // Any env var with KEY in the name
    /SECRET/i,
    /TOKEN/i,
    /PASSWORD/i,
    /^AWS_/i,
    /^GCP_/i,
    /^AZURE_/i,
    /^CLOUDFLARE_/i,
    /^DATABASE_URL$/i,
    /^DB_/i,
    /^QUILL_/i, // Code signing keys
    /^MACOS_/i, // macOS signing keys
    /_P12$/i, // Certificate files
    /BEARER/i,
    /CREDENTIALS?/i,
    /AUTH/i,
];
const MASKED_ARG_VALUE = '***MASKED***';
const SENSITIVE_ARG_PATTERNS = [
    /^--?api[-_]?key$/i,
    /^--?token$/i,
    /^--?secret$/i,
    /^--?password$/i,
    /^--?client[-_]?secret$/i,
    /^--?auth[-_]?value$/i,
];
/**
 * Mask sensitive values in environment variables
 */
function maskEnvironment() {
    const masked = {};
    for (const [key, value] of Object.entries(process.env)) {
        if (!value)
            continue;
        // Check if this env var matches sensitive patterns
        const isSensitive = SENSITIVE_ENV_PATTERNS.some((pattern) => pattern.test(key));
        if (isSensitive) {
            // Show only first and last 4 chars for keys/tokens, or just mask completely
            if (value.length > 12) {
                masked[key] = `${value.slice(0, 4)}...${value.slice(-4)}`;
            }
            else {
                masked[key] = '***MASKED***';
            }
        }
        else {
            masked[key] = value;
        }
    }
    return masked;
}
function isSensitiveArgFlag(arg) {
    return SENSITIVE_ARG_PATTERNS.some((pattern) => pattern.test(arg));
}
function sanitizeArgsForLogging(args) {
    const sanitized = [];
    let maskNextValue = false;
    for (let i = 0; i < args.length; i += 1) {
        const arg = args[i];
        if (maskNextValue) {
            if (!arg.startsWith('-')) {
                sanitized.push(MASKED_ARG_VALUE);
                maskNextValue = false;
                continue;
            }
            sanitized.push(MASKED_ARG_VALUE);
            maskNextValue = false;
            i -= 1;
            continue;
        }
        if (!arg.startsWith('-')) {
            sanitized.push(arg);
            continue;
        }
        const equalsIndex = arg.indexOf('=');
        if (equalsIndex > 0) {
            const flag = arg.slice(0, equalsIndex);
            if (isSensitiveArgFlag(flag)) {
                sanitized.push(`${flag}=${MASKED_ARG_VALUE}`);
                continue;
            }
        }
        sanitized.push(arg);
        if (isSensitiveArgFlag(arg)) {
            maskNextValue = true;
        }
    }
    return sanitized;
}
export function sanitizeCliCommandForLogging(command, args) {
    const commandTokens = command ? command.split(' ') : [];
    if (commandTokens.length >= 5 &&
        commandTokens[0] === 'coder' &&
        commandTokens[1] === 'config' &&
        commandTokens[2] === 'set' &&
        commandTokens[3] === 'apikey') {
        commandTokens[4] = MASKED_ARG_VALUE;
    }
    return {
        command: commandTokens.join(' '),
        args: sanitizeArgsForLogging(args),
    };
}
/**
 * Get the logs directory path
 */
function getLogsDir() {
    return join(homedir(), '.config', 'agentuity', 'logs');
}
/**
 * Calculate the current 5-minute bucket number
 * Each bucket represents a 5-minute window (300000ms)
 */
function getCurrentBucket() {
    return Math.floor(Date.now() / 300000);
}
/**
 * Parse bucket number from directory name
 * Directory format: {bucket}-{uuid}
 * Returns null for legacy directories (uuid-only format)
 */
function parseBucketFromDirName(dirName) {
    const match = dirName.match(/^(\d+)-/);
    if (match && match[1]) {
        return parseInt(match[1], 10);
    }
    return null;
}
/**
 * Clean up old log directories, keeping only directories in the current bucket
 */
function cleanupOldLogs(currentBucket) {
    // Skip cleanup when inheriting a parent's session ID to avoid
    // deleting the parent's session directory (race condition).
    // This applies to forked deploy processes and any subprocess
    // that inherits AGENTUITY_INTERNAL_SESSION_ID.
    if (process.env.AGENTUITY_INTERNAL_SESSION_ID) {
        return;
    }
    const logsDir = getLogsDir();
    if (!existsSync(logsDir)) {
        return;
    }
    try {
        const entries = readdirSync(logsDir, { withFileTypes: true });
        const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
        // Remove directories with bucket < currentBucket (old buckets)
        // Also remove legacy directories (no bucket prefix) for backward compatibility
        for (const dir of dirs) {
            const bucket = parseBucketFromDirName(dir);
            // Delete if:
            // 1. Legacy directory (no bucket prefix) - clean up old format
            // 2. Bucket is older than current bucket
            if (bucket === null || bucket < currentBucket) {
                const dirPath = join(logsDir, dir);
                try {
                    rmSync(dirPath, { recursive: true, force: true });
                }
                catch (err) {
                    // Ignore errors during cleanup
                    console.debug(`Failed to remove old log directory ${dir}: ${err}`);
                }
            }
        }
    }
    catch (err) {
        // Ignore errors during cleanup
        console.debug(`Failed to cleanup old logs: ${err}`);
    }
}
/**
 * Internal logger for capturing all CLI command execution details
 */
export class InternalLogger {
    cliVersion;
    cliName;
    sessionId;
    sessionDir;
    sessionFile;
    logsFile;
    bucket;
    initialized = false;
    disabled = false;
    constructor(cliVersion, cliName) {
        this.cliVersion = cliVersion;
        this.cliName = cliName;
        // Calculate current 5-minute bucket
        this.bucket = getCurrentBucket();
        // When a parent session ID is set in the environment, use it to ensure
        // all CLI invocations (parent and any subprocesses) write to the same log file.
        // This prevents race conditions where child processes delete parent's logs.
        const parentSessionId = process.env.AGENTUITY_INTERNAL_SESSION_ID;
        if (parentSessionId) {
            this.sessionId = parentSessionId;
            // Parent session ID already includes bucket prefix, use as-is for directory name
            this.sessionDir = join(getLogsDir(), parentSessionId);
        }
        else {
            // Generate new session ID with bucket prefix: {bucket}-{uuid}
            const uuid = randomUUID();
            this.sessionId = `${this.bucket}-${uuid}`;
            this.sessionDir = join(getLogsDir(), this.sessionId);
        }
        this.sessionFile = join(this.sessionDir, 'session.json');
        this.logsFile = join(this.sessionDir, 'logs.jsonl');
    }
    /**
     * Initialize the internal logger with command metadata
     * @param command - The command being executed
     * @param args - Command line arguments
     * @param userId - Optional user ID (set later via setUserId if not provided)
     * @param projectDir - Optional project directory from --dir flag (defaults to process.cwd())
     */
    init(command, args, userId, projectDir) {
        if (this.disabled)
            return;
        try {
            // Create logs directory (may already exist if we're a child process)
            mkdirSync(this.sessionDir, { recursive: true, mode: 0o700 });
            // Clean up old logs (directories with bucket < current bucket)
            // This is skipped for child processes to avoid deleting parent's session
            cleanupOldLogs(this.bucket);
            // When inheriting a parent's session ID, skip session.json creation
            // (parent already created it) but enable logging
            if (process.env.AGENTUITY_INTERNAL_SESSION_ID) {
                this.initialized = true;
                return;
            }
            // Determine project directory: use provided projectDir, or fall back to cwd
            let workingDir = projectDir || process.cwd();
            // Handle home directory expansion (~/path -> /home/user/path)
            if (workingDir.startsWith('~/')) {
                workingDir = join(homedir(), workingDir.slice(2));
            }
            // Resolve to absolute path
            workingDir = resolve(workingDir);
            // Check for agentuity.json in the determined directory
            let projectId;
            let orgId;
            try {
                const agentuityJsonPath = join(workingDir, 'agentuity.json');
                if (existsSync(agentuityJsonPath)) {
                    const agentuityJson = JSON.parse(readFileSync(agentuityJsonPath, 'utf-8'));
                    projectId = agentuityJson.projectId;
                    orgId = agentuityJson.orgId;
                }
            }
            catch {
                // Ignore errors reading agentuity.json
            }
            // Use workingDir as cwd in session metadata
            const cwd = workingDir;
            const sanitizedInvocation = sanitizeCliCommandForLogging(command, args);
            // Gather session metadata
            const sessionMetadata = {
                sessionId: this.sessionId,
                bucket: this.bucket,
                pid: process.pid,
                ppid: process.ppid,
                command: sanitizedInvocation.command,
                args: sanitizedInvocation.args,
                timestamp: new Date().toISOString(),
                cli: {
                    version: this.cliVersion,
                    name: this.cliName,
                },
                system: {
                    platform: platform(),
                    arch: arch(),
                    cpus: cpus().length,
                    memory: totalmem(),
                    bunPath: process.execPath || '',
                    bunVersion: Bun.version || process.version,
                },
                environment: maskEnvironment(),
                cwd,
                ...(userId && { userId }),
                ...(projectId && { projectId }),
                ...(orgId && { orgId }),
            };
            // Write session metadata
            writeFileSync(this.sessionFile, JSON.stringify(sessionMetadata, null, 2));
            this.initialized = true;
        }
        catch (err) {
            // If we fail to initialize, disable the logger
            console.debug(`Failed to initialize internal logger: ${err}`);
            this.disabled = true;
        }
    }
    /**
     * Write a log entry to the logs file
     */
    writeLog(level, message, args) {
        if (!this.initialized || this.disabled)
            return;
        try {
            // Format the message
            let formattedMessage;
            if (typeof message === 'string') {
                // Simple sprintf-style formatting for %s and %d
                formattedMessage = message;
                let argIndex = 0;
                formattedMessage = formattedMessage.replace(/%[sd]/g, () => {
                    if (argIndex < args.length) {
                        return String(args[argIndex++]);
                    }
                    return '';
                });
                // Append any remaining args
                if (argIndex < args.length) {
                    formattedMessage += ' ' + args.slice(argIndex).map(String).join(' ');
                }
            }
            else {
                formattedMessage = [message, ...args].map(String).join(' ');
            }
            // Strip ANSI color codes since this is going to JSON
            if (typeof Bun !== 'undefined' && typeof Bun.stripANSI === 'function') {
                formattedMessage = Bun.stripANSI(formattedMessage);
            }
            // Extract context from args (look for objects)
            const context = {};
            for (const arg of args) {
                if (arg && typeof arg === 'object' && !Array.isArray(arg)) {
                    Object.assign(context, arg);
                }
            }
            const entry = {
                timestamp: new Date().toISOString(),
                level,
                message: formattedMessage,
                ...(Object.keys(context).length > 0 && { context }),
            };
            appendFileSync(this.logsFile, JSON.stringify(entry) + '\n');
        }
        catch (err) {
            // If write fails, disable the logger to prevent repeated errors
            console.debug(`Failed to write log entry: ${err}`);
            this.disabled = true;
        }
    }
    trace(message, ...args) {
        this.writeLog('trace', message, args);
    }
    debug(message, ...args) {
        this.writeLog('debug', message, args);
    }
    info(message, ...args) {
        this.writeLog('info', message, args);
    }
    warn(message, ...args) {
        this.writeLog('warn', message, args);
    }
    error(message, ...args) {
        this.writeLog('error', message, args);
    }
    fatal(message, ...args) {
        this.writeLog('error', message, args);
        process.exit(1);
    }
    child(_opts) {
        // Return the same logger - we don't need separate child loggers for internal logging
        return this;
    }
    /**
     * Get the session ID for this logger
     */
    getSessionId() {
        return this.sessionId;
    }
    /**
     * Get the session directory path
     */
    getSessionDir() {
        return this.sessionDir;
    }
    /**
     * Check if the logger is disabled
     */
    isDisabled() {
        return this.disabled;
    }
    /**
     * Update the session with user ID after authentication
     */
    setUserId(userId) {
        if (!this.initialized || this.disabled)
            return;
        try {
            // Read existing session data
            const existingData = JSON.parse(readFileSync(this.sessionFile, 'utf-8'));
            existingData.userId = userId;
            // Write updated session data
            writeFileSync(this.sessionFile, JSON.stringify(existingData, null, 2));
        }
        catch (err) {
            // Ignore errors - this is a best-effort update
            console.debug(`Failed to update userId in session: ${err}`);
        }
    }
    /**
     * Update the session with detected agent name
     */
    setDetectedAgent(agent) {
        if (!this.initialized || this.disabled)
            return;
        try {
            // Read existing session data
            const existingData = JSON.parse(readFileSync(this.sessionFile, 'utf-8'));
            existingData.detectedAgent = agent;
            // Write updated session data
            writeFileSync(this.sessionFile, JSON.stringify(existingData, null, 2));
        }
        catch (err) {
            // Ignore errors - this is a best-effort update
            console.debug(`Failed to update detectedAgent in session: ${err}`);
        }
    }
    /**
     * Disable the internal logger (prevents init and logging)
     */
    disable() {
        this.disabled = true;
    }
}
/**
 * Create a new internal logger instance
 */
export function createInternalLogger(cliVersion, cliName) {
    return new InternalLogger(cliVersion, cliName);
}
/**
 * Get all log session directories in the current time window
 * Returns directories from current bucket AND previous bucket (to handle boundary cases)
 */
export function getLogSessionsInCurrentWindow() {
    const logsDir = getLogsDir();
    if (!existsSync(logsDir)) {
        return [];
    }
    try {
        const currentBucket = getCurrentBucket();
        const previousBucket = currentBucket - 1;
        const entries = readdirSync(logsDir, { withFileTypes: true });
        const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
        // Filter to directories in current or previous bucket
        const validDirs = dirs.filter((dir) => {
            const bucket = parseBucketFromDirName(dir);
            // Include current bucket, previous bucket, and legacy directories (for backward compat)
            return bucket === currentBucket || bucket === previousBucket || bucket === null;
        });
        // Sort by bucket (descending) then by name to get most recent first
        validDirs.sort((a, b) => {
            const bucketA = parseBucketFromDirName(a) ?? 0;
            const bucketB = parseBucketFromDirName(b) ?? 0;
            if (bucketA !== bucketB) {
                return bucketB - bucketA; // Higher bucket first
            }
            return b.localeCompare(a); // Then by name descending
        });
        return validDirs.map((dir) => join(logsDir, dir));
    }
    catch {
        return [];
    }
}
/**
 * Get the latest log session directory (if any)
 * For backward compatibility, returns the first session in the current window
 */
export function getLatestLogSession() {
    const sessions = getLogSessionsInCurrentWindow();
    return sessions[0] ?? null;
}
/**
 * Get the logs directory path (exported for external use)
 */
export function getLogsDirPath() {
    return getLogsDir();
}
//# sourceMappingURL=internal-logger.js.map