import { safeJsonStringify } from "../../util/log-sanitizer.js";
import { getLogger, getPrefixedLogger, getErrorMeta, } from "../../util/logger.js";
import { LLMObsTracer } from "../llmobs/tracer.js";
import { scanContentForSecrets, SecretRedactor, classifyPromptSafety, createBlockedUserMessage, FAIL_CLOSED_RESULT, } from "../security/index.js";
import { ContextV2Adapter } from "./context-v2/index.js";
import { createVercelProvider } from "./interaction/adapters/vercel.js";
import { StreamOrchestrator, StreamConfig, LoggingObserver, LLMObsObserver, ContextObserver, RetryNotificationObserver, wrapToolsForObservability, } from "./stream/index.js";
import { extractWorkflowMetrics } from "./workflow-metrics.js";
/**
 * Extracts text content from a user message.
 *
 * @param user - The user message to extract text from
 * @param scope - Which text parts to extract: "all" (default) or "last" (user prompt only)
 * @returns Extracted text content
 */
export function extractTextFromMessage(message, scope = "all") {
    if (typeof message.content === "string") {
        return message.content;
    }
    const textParts = message.content.filter((part) => part.type === "text");
    if (scope === "last") {
        const lastPart = textParts[textParts.length - 1];
        if (lastPart) {
            return lastPart.text;
        }
    }
    return textParts.map((part) => part.text).join("\n");
}
/**
 * Creates a new user message with text content redacted.
 */
function redactUserMessage(user, redactor) {
    if (!redactor.hasFindings()) {
        return user;
    }
    if (typeof user.content === "string") {
        return {
            ...user,
            content: redactor.redact(user.content),
        };
    }
    // Content is an array - redact text parts
    return {
        ...user,
        content: user.content.map((part) => {
            if (part.type === "text") {
                return { ...part, text: redactor.redact(part.text) };
            }
            return part;
        }),
    };
}
export class LLMClient {
    contextManagerV2;
    appShell;
    baseProvider;
    retryOptions;
    unifiedProviderEnabled;
    organizationId;
    applicationId;
    getJwt;
    onUsageRecorded;
    logger = getPrefixedLogger("[llm-client]");
    constructor(params) {
        this.contextManagerV2 = params.contextManagerV2;
        this.appShell = params.appShell;
        this.baseProvider = params.baseProvider ?? createVercelProvider();
        this.retryOptions = params.retryOptions;
        this.unifiedProviderEnabled = params.unifiedProviderEnabled ?? false;
        this.organizationId = params.organizationId;
        this.applicationId = params.applicationId;
        this.getJwt = params.getJwt;
        this.onUsageRecorded = params.onUsageRecorded;
    }
    /**
     * Applies safety classification to a user message.
     *
     * @param user - The user message to classify
     * @param userText - The extracted text to classify
     * @param config - Safety classification configuration
     * @returns Object containing the processed message and classification result
     */
    async applySafetyClassification(user, userText, config) {
        const failOpen = config.failOpen ?? true;
        if (!config.model) {
            this.logger.warn("[safety-classifier] No model configured, skipping classification");
            return {
                message: user,
                classification: {
                    safe: true,
                    justification: "No model configured, skipped classification",
                },
            };
        }
        try {
            this.logger.info("[safety-classifier] Running safety classification");
            const classification = await classifyPromptSafety(userText, {
                model: config.model,
                timeout: config.timeout ?? 5000,
            });
            if (!classification.safe) {
                this.logger.warn("[safety-classifier] Prompt classified as unsafe", {
                    justification: classification.justification,
                    categories: classification.categories,
                });
                return {
                    message: createBlockedUserMessage(user, classification),
                    classification: {
                        safe: false,
                        justification: classification.justification,
                        categories: classification.categories,
                    },
                };
            }
            this.logger.info("[safety-classifier] Prompt classified as safe");
            return {
                message: user,
                classification: {
                    safe: true,
                    justification: classification.justification,
                },
            };
        }
        catch (error) {
            this.logger.error("[safety-classifier] Classification failed", getErrorMeta(error));
            if (failOpen) {
                this.logger.info("[safety-classifier] Failing open, allowing request to proceed");
                return {
                    message: user,
                    classification: {
                        safe: true,
                        justification: "Classification error, failed open",
                        error: error instanceof Error ? error.message : String(error),
                    },
                };
            }
            else {
                this.logger.warn("[safety-classifier] Failing closed, blocking request");
                return {
                    message: createBlockedUserMessage(user, FAIL_CLOSED_RESULT),
                    classification: {
                        safe: false,
                        justification: FAIL_CLOSED_RESULT.justification,
                        categories: FAIL_CLOSED_RESULT.categories,
                        error: error instanceof Error ? error.message : String(error),
                    },
                };
            }
        }
    }
    async streamText(options, logger = getLogger(), tracer = LLMObsTracer.instance) {
        const { model, user, system, context: contextConfig, logging: loggingConfig, retry: retryConfig, providerConfig, enableFallback, fallbackStrategy, peer, chatSessionStore, observers: customObservers, prepareStep, onStepFinish, onFinish, onChunk, onError, summarizationModel, safety: safetyConfig, ...streamOptions } = options;
        if (!user) {
            throw new Error("user message is required");
        }
        let safeUser = user;
        let safetySpan;
        // Apply safety features if configured and at least one is enabled
        const hasSecretRedaction = safetyConfig?.secretRedaction?.enabled === true;
        const hasClassification = safetyConfig?.classification?.enabled === true;
        if (safetyConfig && (hasSecretRedaction || hasClassification)) {
            safetySpan = tracer.startActiveSpan({
                name: "llmclient.safety",
                kind: "task",
            });
            // Extract text once using unified scope
            const scope = safetyConfig.scope ?? "all";
            const userText = extractTextFromMessage(user, scope);
            // Secret redaction (if enabled)
            if (hasSecretRedaction) {
                await tracer.trace({
                    name: "llmclient.safety.secretRedaction",
                    kind: "task",
                }, async () => {
                    try {
                        const scanResult = await scanContentForSecrets(userText);
                        // Check if scan completed successfully
                        if (!scanResult.success) {
                            this.logger.error(`[secret-scan] Secret scanning failed, cannot verify if prompt contains secrets: ${scanResult.error ?? "unknown error"}`);
                            // Fail-open: Continue without redaction rather than blocking legitimate requests
                            // Alternative: throw error to fail-closed and block the request
                            return;
                        }
                        const secretRedactor = new SecretRedactor(scanResult.findings);
                        if (scanResult.hasSecrets) {
                            this.logger.warn(`[secret-scan] Found ${scanResult.findings.length} potential secret(s)`, {
                                findingCount: scanResult.findings.length,
                                hasVerified: scanResult.hasVerifiedSecrets,
                                detectorTypes: scanResult.findings.map((f) => f.detectorType),
                            });
                        }
                        safeUser = redactUserMessage(safeUser, secretRedactor);
                    }
                    catch (error) {
                        this.logger.error("[secret-scan] Secret scanning threw an exception, proceeding without redaction", getErrorMeta(error));
                        // Fail-open: Continue with unredacted message rather than blocking
                    }
                });
            }
            // Safety classification (if enabled)
            if (hasClassification && safetyConfig.classification) {
                const config = safetyConfig.classification;
                if (!config.model) {
                    this.logger.warn("[safety-classifier] No model configured, skipping classification");
                }
                else {
                    await tracer.trace({
                        name: "llmclient.safety.classification",
                        kind: "task",
                    }, async () => {
                        // Extract text from the potentially redacted message to avoid leaking secrets to classifier
                        const textToClassify = extractTextFromMessage(safeUser, scope);
                        const result = await this.applySafetyClassification(safeUser, textToClassify, config);
                        safeUser = result.message;
                        // Annotate classification span with results
                        tracer.annotate({
                            tags: {
                                "safety.classification.safe": result.classification.safe,
                                "safety.classification.blocked": !result.classification.safe,
                            },
                            metadata: {
                                classification: {
                                    safe: result.classification.safe,
                                    justification: result.classification.justification,
                                    ...(result.classification.categories && {
                                        categories: result.classification.categories,
                                    }),
                                    ...(result.classification.error && {
                                        error: result.classification.error,
                                    }),
                                },
                            },
                        });
                    });
                }
            }
            safetySpan?.finish();
        }
        if (!providerConfig?.provider || !providerConfig?.model) {
            throw new Error("providerConfig with provider and model is required. " +
                "This should be populated from llmConfig sent by the UI.");
        }
        const contextId = contextConfig?.contextId;
        const { contextHandle, ctx } = await this.createContext(contextId, summarizationModel ?? model, contextConfig?.contextOptionsV2, peer, tracer);
        if (contextConfig?.messages?.length) {
            await ctx.initializeWith(contextConfig.messages);
        }
        if (system) {
            ctx.setSystemPrompt({ role: "system", content: system });
        }
        await ctx.startTurn(safeUser);
        try {
            const fallbackEnabled = this.unifiedProviderEnabled && (enableFallback ?? false);
            logger.info(`[llm-client] provider=${providerConfig.provider} model=${providerConfig.model} ` +
                `unifiedProvider=${this.unifiedProviderEnabled} fallback=${fallbackEnabled}` +
                ` retry=${safeJsonStringify(retryConfig)}`);
            const config = StreamConfig.create({
                retry: {
                    ...this.retryOptions,
                    ...retryConfig,
                    maxTotalTimeMs: retryConfig?.maxTotalTimeMs ??
                        this.retryOptions?.maxTotalTimeMs ??
                        2 * 60 * 1000,
                },
                provider: providerConfig.provider,
                model: providerConfig.model,
                fallback: {
                    enabled: fallbackEnabled,
                    strategy: fallbackStrategy,
                },
                logging: loggingConfig !== false
                    ? {
                        enabled: true,
                        conversationId: loggingConfig?.conversationId,
                        saveArtifact: this.appShell.saveGeneratedArtifact.bind(this.appShell),
                        metadata: loggingConfig?.metadata,
                    }
                    : { enabled: false },
            });
            const observers = [];
            if (loggingConfig !== false) {
                // Determine if this is a cloud-prem deployment
                const isCloudPrem = process.env.SUPERBLOCKS_DEPLOYMENT_TYPE === "cloud-prem";
                observers.push(new LoggingObserver({
                    logRef: loggingConfig?.logRef,
                    policy: {
                        // Enable secret redaction only when explicitly requested via safety config
                        secretRedactionEnabled: safetyConfig?.secretRedaction?.enabled ?? false,
                        // Disable artifact saving for cloud-prem by default
                        artifactSavingEnabled: !isCloudPrem,
                    },
                }));
            }
            observers.push(new LLMObsObserver({ tracer, logger }));
            observers.push(new ContextObserver({
                contextHandle,
            }));
            observers.push(new RetryNotificationObserver({ peer, chatSessionStore }));
            // Workflow metrics tracking
            if (this.onUsageRecorded) {
                const callback = this.onUsageRecorded;
                observers.push({
                    attach(events) {
                        events.on("stream:finish", (result) => {
                            callback(extractWorkflowMetrics(result.steps));
                        });
                    },
                });
            }
            if (customObservers) {
                observers.push(...customObservers);
            }
            const orchestrator = new StreamOrchestrator({
                provider: this.baseProvider,
                config,
                observers,
                logger: this.logger,
                organizationId: this.organizationId,
                applicationId: this.applicationId,
                getJwt: this.getJwt,
            });
            const { tools, ...restStreamOptions } = streamOptions;
            const tracedTools = tools
                ? wrapToolsForObservability(tools, tracer)
                : undefined;
            const result = await orchestrator.execute({
                ...restStreamOptions,
                tools: tracedTools,
                model,
                messages: await ctx.getMessages(),
                prepareStep: async (step) => {
                    this.logger.debug("prepareStep");
                    ctx.startStep();
                    step = { ...step, messages: await ctx.getMessages() };
                    return prepareStep?.(step) ?? step;
                },
                onStepFinish,
                onFinish,
                onChunk,
            });
            if (onError) {
                void result.response?.catch((error) => {
                    const normalizedError = error instanceof Error ? error : new Error(String(error));
                    void onError(normalizedError);
                });
            }
            return result;
        }
        catch (error) {
            contextHandle.release();
            throw error;
        }
    }
    async createContext(contextId, summarizationModel, contextOptionsV2, peer, tracer) {
        const context = await this.contextManagerV2.getOrCreateContext(contextId, summarizationModel, contextOptionsV2, tracer);
        const adapter = ContextV2Adapter.fromContext(context);
        // Always set compaction callbacks to track phase 2 for skill reload prompts
        adapter.setCompactionCallbacks({
            onCompactionStart: (phase, messageId) => {
                if (phase === "phase1")
                    return;
                if (peer) {
                    void peer.call.aiPushMessage({
                        id: messageId,
                        type: "control",
                        group: `context-compaction-${phase}`,
                        text: "Summarizing conversation",
                        status: "pending",
                    });
                }
            },
            onCompactionComplete: (phase, messageId) => {
                if (phase === "phase1")
                    return;
                // Mark that skills need to be re-read after phase 2 summarization
                adapter.markSkillsNeedReload();
                if (peer) {
                    void peer.call.aiPushMessage({
                        id: messageId,
                        type: "control",
                        group: `context-compaction-${phase}`,
                        text: "Summarized past conversation",
                        status: "validated",
                    });
                }
            },
        });
        this.logger.info(`[llm-client] context (${contextId?.name ?? "ephemeral"}, persistent=${!!this.contextManagerV2 && !!contextId})`);
        return {
            contextHandle: {
                context: adapter,
                release: () => { },
            },
            ctx: adapter,
        };
    }
}
//# sourceMappingURL=client.js.map