import { safeJsonStringify } from "../../../../util/log-sanitizer.js";
import { getLogger } from "../../../../util/logger.js";
import { OperationQueue } from "../../../../util/operation-queue.js";
import { scanContentForSecrets, SecretRedactor, } from "../../../security/index.js";
/** Default policy: redact secrets, save artifacts (cloud behavior) */
const DEFAULT_POLICY = {
    secretRedactionEnabled: true,
    artifactSavingEnabled: true,
};
const SECTION_WIDTH = 72;
const SECTION_CHAR = "-";
const MAX_IO_BYTES = 1 * 1024;
const MAX_SYSTEM_PROMPT_BYTES = 1 * 1024;
const MAX_MESSAGE_BYTES = 3 * 1024;
function formatSection(title) {
    const prefix = `${SECTION_CHAR.repeat(6)} ${title} `;
    const remaining = Math.max(0, SECTION_WIDTH - prefix.length);
    return prefix + SECTION_CHAR.repeat(remaining);
}
function truncateString(s, maxBytes) {
    if (s.length <= maxBytes) {
        return { text: s, truncated: false, omittedBytes: 0 };
    }
    const omittedBytes = s.length - maxBytes;
    return {
        text: s.slice(0, maxBytes) + `\n... [truncated ${omittedBytes} bytes]`,
        truncated: true,
        omittedBytes,
    };
}
function isEmptyJsonObject(value) {
    if (value === null || value === undefined)
        return true;
    if (typeof value === "object" && Object.keys(value).length === 0)
        return true;
    return false;
}
function formatIoBlock(label, value) {
    if (isEmptyJsonObject(value)) {
        return "";
    }
    const jsonStr = safeJsonStringify(value, 2);
    const { text } = truncateString(jsonStr, MAX_IO_BYTES);
    const indented = text
        .split("\n")
        .map((line) => `    ${line}`)
        .join("\n");
    return `  ${label}:\n${indented}\n`;
}
function formatToolCallPart(part) {
    const toolName = part.toolName ?? part.toolCallId ?? "unknown";
    let result = `TOOL ${toolName}:`;
    const args = part.args ?? part.input;
    const inBlock = formatIoBlock("IN", args);
    if (inBlock) {
        result += "\n" + inBlock.trimEnd();
    }
    return result;
}
function formatToolResultPart(part) {
    const toolName = part.toolName ?? part.toolCallId ?? "unknown";
    let result = `TOOL ${toolName}:`;
    const output = part.result ?? part.output;
    const outBlock = formatIoBlock("OUT", output);
    if (outBlock) {
        result += "\n" + outBlock.trimEnd();
    }
    return result;
}
function extractMessageText(content) {
    if (typeof content === "string") {
        return content;
    }
    if (Array.isArray(content)) {
        return content
            .map((part) => {
            if (part.type === "text" && typeof part.text === "string") {
                return part.text;
            }
            if (part.type === "reasoning") {
                const text = "text" in part ? part.text : "";
                return text ? `REASONING:\n${text}` : "";
            }
            if (part.type === "tool-call") {
                return formatToolCallPart(part);
            }
            if (part.type === "tool-result") {
                return formatToolResultPart(part);
            }
            if (part.type === "image") {
                return "[image]";
            }
            if (part.type === "file") {
                return "[file]";
            }
            return `[${part.type ?? "unknown"}]`;
        })
            .filter(Boolean)
            .join("\n");
    }
    return "";
}
function formatMessageRole(role) {
    return role.toUpperCase();
}
/**
 * Observer that captures detailed logs of LLM interactions.
 *
 * @example
 * ```typescript
 * const logRef = { content: "" };
 * const observer = new LoggingObserver({ logRef });
 * // Access logs during streaming: console.log(logRef.content)
 * ```
 */
export class LoggingObserver {
    logRef;
    policy;
    sessionStartTime;
    currentTextBuffer = "";
    currentReasoningBuffer = "";
    reasoningLoggedViaChunks = false;
    /** Queue for async operations (tool redaction, request logging) to preserve ordering */
    operationQueue = new OperationQueue();
    constructor(options = {}) {
        this.logRef = options.logRef ?? { content: "" };
        this.policy = options.policy ?? DEFAULT_POLICY;
    }
    /**
     * Redact secrets from content if policy enables it.
     */
    async redactContent(content) {
        if (!this.policy.secretRedactionEnabled) {
            return content;
        }
        // Scan content for secrets
        const scanResult = await scanContentForSecrets(content);
        if (!scanResult.hasSecrets) {
            return content;
        }
        // Create redactor for this content
        const redactor = new SecretRedactor(scanResult.findings);
        return redactor.redact(content);
    }
    attach(events, config) {
        if (!config.logging.enabled) {
            return;
        }
        events.on("session:start", (session, request) => {
            this.onSessionStart(session, config);
            if (request) {
                // Enqueue rather than await -- don't block the event bus.
                // onRequestStart runs Trufflehog scans on system prompt/messages;
                // the queue ensures it completes before any tool formatting.
                void this.operationQueue.enqueue(() => this.onRequestStart(request));
            }
        });
        events.on("step:start", (step, _session) => {
            this.onStepStart(step);
        });
        events.on("chunk", (chunk, _session) => {
            this.onChunk(chunk);
        });
        events.on("step:finish", (step, _session) => {
            this.onStepComplete(step);
        });
        events.on("retry", (error, attempt, delayMs, _session) => {
            this.onRetry(error, attempt, delayMs);
        });
        events.on("fallback", (from, to, reason) => {
            this.onFallback(from, to, reason);
        });
        events.on("stream:finish", (result, _session) => {
            this.onStreamFinish(result);
        });
        events.on("session:complete", (session) => {
            void this.onSessionComplete(session, config);
        });
        events.on("session:error", (error, session) => {
            void this.onSessionError(error, session, config);
        });
        events.on("session:abort", (session) => {
            void this.onSessionAbort(session, config);
        });
    }
    onSessionStart(session, config) {
        this.sessionStartTime = Date.now();
        const conversationId = config.logging.conversationId ?? session.id;
        const timestamp = new Date().toISOString();
        this.logRef.content += formatSection("LLM CONVERSATION START") + "\n";
        this.logRef.content += `Session ID: ${conversationId}\n`;
        this.logRef.content += `Timestamp: ${timestamp}\n`;
        this.logRef.content += `Provider: ${session.provider.name}\n`;
        this.logRef.content += `Model: ${session.provider.model}\n`;
        const metadata = config.logging.metadata;
        if (metadata && Object.keys(metadata).length > 0) {
            for (const [k, v] of Object.entries(metadata)) {
                if (typeof v === "object") {
                    this.logRef.content += `${k}: ${safeJsonStringify(v)}\n`;
                }
                else {
                    this.logRef.content += `${k}: ${v}\n`;
                }
            }
        }
        this.logRef.content += "\n";
        this.currentTextBuffer = "";
        this.currentReasoningBuffer = "";
        this.reasoningLoggedViaChunks = false;
    }
    async onRequestStart(payload) {
        this.logRef.content += formatSection("REQUEST CONTEXT") + "\n";
        // Log system prompt (redact secrets before logging)
        if (payload.system) {
            const systemBytes = payload.system.length;
            // Redact BEFORE truncation to ensure secrets are caught
            const redactedSystem = await this.redactContent(payload.system);
            const { text: systemText, truncated } = truncateString(redactedSystem, MAX_SYSTEM_PROMPT_BYTES);
            this.logRef.content += `System Prompt (${systemBytes.toLocaleString()} bytes):\n`;
            this.logRef.content += systemText + "\n";
            if (truncated) {
                this.logRef.content += `(showing first ${MAX_SYSTEM_PROMPT_BYTES.toLocaleString()} bytes)\n`;
            }
            this.logRef.content += "\n";
        }
        // Log all messages (redact secrets before logging)
        if (payload.messages.length > 0) {
            this.logRef.content += `Messages (${payload.messages.length} total):\n`;
            for (const msg of payload.messages) {
                const role = formatMessageRole(msg.role);
                const text = extractMessageText(msg.content);
                // Redact BEFORE truncation
                const redactedText = await this.redactContent(text);
                const { text: truncatedText } = truncateString(redactedText, MAX_MESSAGE_BYTES);
                this.logRef.content += `${role}:\n`;
                if (truncatedText.trim()) {
                    this.logRef.content += truncatedText + "\n";
                }
            }
            this.logRef.content += "\n";
        }
        this.logRef.content += formatSection("TRANSCRIPT") + "\n";
    }
    onStepStart(step) {
        if (step.activeTools && step.activeTools.length > 0) {
            this.logRef.content += `STEP ${step.stepNumber + 1} Active Tools (${step.activeTools.length}):\n`;
            this.logRef.content += `${step.activeTools.join(", ")}\n\n`;
        }
    }
    onChunk(chunk) {
        switch (chunk.type) {
            case "text-delta":
                this.currentTextBuffer += chunk.text;
                break;
            case "text-end":
                this.flushTextBuffer();
                break;
            case "reasoning-delta":
                this.currentReasoningBuffer += chunk.text;
                break;
            case "reasoning-end":
                this.flushReasoningBuffer();
                break;
            case "tool-call":
                this.flushTextBuffer();
                this.flushReasoningBuffer();
                // Enqueue async operation to preserve ordering and prevent races.
                // OperationQueue isolates failures so one bad format doesn't break the chain.
                void this.operationQueue.enqueue(async () => {
                    try {
                        await this.formatToolCall(chunk);
                    }
                    catch (err) {
                        getLogger().error(`Failed to format tool call in log: ${err instanceof Error ? err.message : String(err)}`);
                    }
                });
                break;
            case "tool-result":
                // Enqueue async operation to preserve ordering and prevent races.
                void this.operationQueue.enqueue(async () => {
                    try {
                        await this.formatToolResult(chunk);
                    }
                    catch (err) {
                        getLogger().error(`Failed to format tool result in log: ${err instanceof Error ? err.message : String(err)}`);
                    }
                });
                break;
            case "error": {
                this.flushTextBuffer();
                this.flushReasoningBuffer();
                const errorMsg = chunk.error instanceof Error
                    ? chunk.error.message
                    : String(chunk.error);
                this.logRef.content += `ERROR:\n${errorMsg}\n\n`;
                break;
            }
            // Ignore other chunk types (text-start, reasoning-start, finish, etc.)
        }
    }
    flushTextBuffer() {
        if (this.currentTextBuffer.trim()) {
            this.logRef.content += `ASSISTANT:\n${this.currentTextBuffer.trim()}\n\n`;
        }
        this.currentTextBuffer = "";
    }
    flushReasoningBuffer() {
        if (this.currentReasoningBuffer.trim()) {
            this.logRef.content += `REASONING:\n${this.currentReasoningBuffer.trim()}\n\n`;
            this.reasoningLoggedViaChunks = true;
        }
        this.currentReasoningBuffer = "";
    }
    async formatToolCall(chunk) {
        // Build content in a local buffer so the append to logRef.content
        // is atomic, preventing interleaving with synchronous chunk handlers.
        let content = `TOOL ${chunk.toolName}:\n`;
        const args = chunk.input ?? chunk.args;
        // Redact secrets from tool input (scans independently)
        const redactedArgs = await this.redactToolDataAsync(args);
        const inBlock = formatIoBlock("IN", redactedArgs);
        if (inBlock) {
            content += inBlock;
        }
        this.logRef.content += content;
    }
    async formatToolResult(chunk) {
        // Build content in a local buffer so the append to logRef.content
        // is atomic, preventing interleaving with synchronous chunk handlers.
        let content = "";
        // SDK uses 'output' for tool-result chunks, but some tests may use 'result'
        const result = chunk.output ?? chunk.result;
        // Redact secrets from tool output (scans independently)
        const redactedResult = await this.redactToolDataAsync(result);
        const outBlock = formatIoBlock("OUT", redactedResult);
        if (outBlock) {
            content += outBlock;
        }
        content += "\n";
        this.logRef.content += content;
    }
    /**
     * Redact secrets from tool data (input or output).
     * Scans independently if no cached redactor is available.
     */
    async redactToolDataAsync(data) {
        if (!this.policy.secretRedactionEnabled) {
            return data;
        }
        // Convert data to string for scanning
        const dataString = safeJsonStringify(data);
        const scanResult = await scanContentForSecrets(dataString);
        if (!scanResult.hasSecrets) {
            return data;
        }
        // Create redactor for this data
        const redactor = new SecretRedactor(scanResult.findings);
        // For objects/arrays, use deep redaction
        if (typeof data === "object" && data !== null) {
            return redactor.redactDeep(data);
        }
        // For strings, use simple redaction
        if (typeof data === "string") {
            return redactor.redact(data);
        }
        return data;
    }
    onStepComplete(step) {
        if (step.reasoningText && !this.reasoningLoggedViaChunks) {
            this.logRef.content += `REASONING:\n${step.reasoningText}\n\n`;
        }
        this.reasoningLoggedViaChunks = false;
    }
    onRetry(error, attempt, delayMs) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logRef.content += `RETRY:\nattempt=${attempt} delay=${delayMs}ms error="${errorMessage}"\n\n`;
    }
    onFallback(from, to, reason) {
        this.logRef.content += `FALLBACK:\n${from.name}/${from.model} -> ${to.name}/${to.model} reason=${reason}\n\n`;
    }
    onStreamFinish(result) {
        this.flushTextBuffer();
        this.flushReasoningBuffer();
        this.logRef.content += "\n";
        this.logRef.content += formatSection("USAGE") + "\n";
        const usage = result.totalUsage;
        if (usage) {
            let usageLine = `Tokens: Input=${usage.inputTokens ?? 0} Output=${usage.outputTokens ?? 0} Total=${usage.totalTokens ?? 0}`;
            if ((usage.cachedInputTokens ?? 0) > 0) {
                usageLine += ` Cached=${usage.cachedInputTokens}`;
            }
            this.logRef.content += usageLine + "\n";
        }
        this.logRef.content += `Finish reason: ${result.finishReason}\n`;
        this.logRef.content += `Total Steps: ${result.steps.length}\n`;
    }
    async onSessionComplete(session, config) {
        const duration = this.computeDuration(session);
        this.logRef.content += "\n";
        this.logRef.content += formatSection("LLM CONVERSATION COMPLETE") + "\n";
        this.logRef.content += `End Timestamp: ${new Date().toISOString()}\n`;
        this.logRef.content += `Duration: ${duration}ms\n`;
        this.logRef.content += `Chunks: ${session.metrics.chunkCount}\n`;
        this.logRef.content += `Retries: ${session.metrics.retryCount}\n`;
        await this.saveLog(session, config);
    }
    async onSessionError(error, session, config) {
        const duration = this.computeDuration(session);
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logRef.content += "\n";
        this.logRef.content += formatSection("LLM CONVERSATION ERROR") + "\n";
        this.logRef.content += `Error Timestamp: ${new Date().toISOString()}\n`;
        this.logRef.content += `Error: ${errorMessage}\n`;
        this.logRef.content += `Duration: ${duration}ms\n`;
        await this.saveLog(session, config);
    }
    async onSessionAbort(session, config) {
        const duration = this.computeDuration(session);
        this.logRef.content += "\n";
        this.logRef.content += formatSection("LLM CONVERSATION ABORTED") + "\n";
        this.logRef.content += `Abort Timestamp: ${new Date().toISOString()}\n`;
        this.logRef.content += `Duration: ${duration}ms\n`;
        await this.saveLog(session, config);
    }
    computeDuration(session) {
        if (session.metrics.durationMs > 0) {
            return session.metrics.durationMs;
        }
        if (this.sessionStartTime) {
            return Date.now() - this.sessionStartTime;
        }
        return 0;
    }
    async saveLog(session, config) {
        // Check policy gate: artifact saving may be disabled (e.g., for cloud-prem)
        if (!this.policy.artifactSavingEnabled) {
            return;
        }
        if (!config.logging.saveArtifact) {
            return;
        }
        // Drain the queue -- the no-op resolves only after all prior operations complete
        await this.operationQueue.enqueue(() => Promise.resolve());
        try {
            const conversationId = config.logging.conversationId ?? session.id;
            const artifact = {
                type: "file",
                filePath: `llm-conversation-${conversationId}.log`,
                content: this.logRef.content,
            };
            const stepId = `llm-conversation-${conversationId}`;
            const runTimestamp = new Date().toISOString();
            await config.logging.saveArtifact(artifact, stepId, runTimestamp);
        }
        catch {
            getLogger().error("Failed to save LLM conversation log");
        }
    }
    getLogContent() {
        return this.logRef.content;
    }
}
//# sourceMappingURL=logging.js.map