/**
 * LLMObs-to-OTEL span exporter.
 *
 * Intercepts LLMObs spans at the writer level and converts them to OTEL spans
 * for export via OTEL collector. Used in CLOUD_PREM deployments where direct
 * Datadog API access is not available.
 *
 * TIERED TELEMETRY ARCHITECTURE (v0.3.0):
 *
 * This exporter emits FULL-FIDELITY spans to the OTEL Collector with the
 * `include_tier3` tier policy hint. The Collector is responsible for tier separation:
 *
 * - Tier 1: Full data (prompts, user-email, org-id) → Customer's local observability
 * - Tier 2: Sanitized data (hashed IDs, no content) → Superblocks-managed observability
 * - Tier 3: AI content with identity stripped → AI quality analysis
 *
 * The SDK's job is to:
 * 1. Convert LLMObs spans to OTEL format
 * 2. Emit ALL data (including content and identity)
 * 3. Use gen_ai.* semantic conventions ONLY (no legacy llm.* prefixes)
 * 4. Set `superblocks.tier_hint: "include_tier3"` for AI quality analysis
 *
 * The Collector's job is to:
 * 1. Hash identity attributes (user-email → user.hash) for Tier 2
 * 2. Strip forbidden attributes (llmobs.input, llmobs.output) for Tier 2
 * 3. Strip identity (no hashing) for Tier 3, but preserve content
 * 4. Route to appropriate tier pipelines based on tier_hint
 *
 * Contract: tier2-traces.v0.3.0.json
 * See also: sdk-tier-policy-hints.md
 */
import { SpanKind, trace } from "@opentelemetry/api";
import { getLogger } from "../../util/logger.js";
/**
 * Tier policy hint attribute key.
 * Must match TIER_HINT_ATTRIBUTE from @superblocksteam/telemetry.
 */
const TIER_HINT_ATTRIBUTE = "superblocks.tier_hint";
/**
 * Tier policy hint for AI quality analysis.
 * All LLMObs spans get this hint so they're routed to Tier 3.
 * Must match TierPolicyHint.INCLUDE_TIER3 from @superblocksteam/telemetry.
 */
const TIER_HINT_INCLUDE_TIER3 = "include_tier3";
/**
 * Maximum length for llmobs.input / llmobs.output span attributes.
 * Very large LLM responses (100K+ tokens) can create spans that are
 * rejected by the OTEL Collector or cause memory pressure.
 */
const MAX_ATTRIBUTE_LENGTH = 64_000;
/**
 * Lazily loaded LLMObsSpanWriter from dd-trace internals.
 * Must be loaded after dd-trace is initialized to avoid accessing
 * uninitialized internal state (e.g., _prioritySampler.sample).
 */
let LLMObsSpanWriter = null;
async function loadLLMObsSpanWriter() {
    if (!LLMObsSpanWriter) {
        const module = await import("dd-trace/packages/dd-trace/src/llmobs/writers/spans.js");
        LLMObsSpanWriter = module.default;
    }
    return LLMObsSpanWriter;
}
/**
 * GenAI system (provider) values per OTEL canonical naming.
 */
const GENAI_SYSTEM_MAPPING = {
    openai: "openai",
    anthropic: "anthropic",
    claude: "anthropic",
    azure: "azure.ai.openai",
    azure_openai: "azure.ai.openai",
    bedrock: "aws.bedrock",
    aws_bedrock: "aws.bedrock",
    google: "gcp.gemini",
    gemini: "gcp.gemini",
    vertex: "gcp.vertex_ai",
    cohere: "cohere",
    mistral: "mistral_ai",
    deepseek: "deepseek",
    groq: "groq",
};
/**
 * Valid error categories per tier2-traces.v0.3.0.json contract.
 */
const VALID_ERROR_CATEGORIES = new Set([
    "none",
    "rate_limit",
    "context_length",
    "content_filter",
    "quota",
    "network",
    "timeout",
    "auth_error",
    "api_error",
    "validation_error",
    "aborted",
    "unknown",
]);
/**
 * Valid finish reasons per tier2-traces.v0.3.0.json contract.
 */
const VALID_FINISH_REASONS = new Set([
    "stop",
    "length",
    "content_filter",
    "tool_calls",
    "error",
]);
/**
 * Converts LLMObs spans to OTEL spans for CLOUD_PREM deployments.
 *
 * This exporter emits FULL-FIDELITY spans. The OTEL Collector handles
 * tier separation (hashing, stripping, routing).
 *
 * When initialized, patches LLMObsSpanWriter.flush to:
 * 1. Intercept all buffered span events
 * 2. Convert each to a FULL OTEL span with gen_ai.* attributes
 * 3. Include identity and content (Collector will sanitize for Tier 2)
 * 4. Drop original spans (prevent Datadog LLMObs API submission)
 */
export class LLMObsOtelExporter {
    originalFlush = null;
    /**
     * Get the OTEL tracer lazily to ensure it's fetched after NodeSDK is started.
     * If fetched at module load time, it can return a no-op tracer.
     */
    get otelTracer() {
        return trace.getTracer("llmobs", "1.0.0");
    }
    /**
     * Initialize the exporter by patching LLMObsSpanWriter.flush.
     * Safe to call multiple times - subsequent calls are no-ops.
     */
    async init() {
        if (this.originalFlush) {
            getLogger().debug("LLMObsOtelExporter: Already initialized");
            return;
        }
        getLogger().debug("LLMObsOtelExporter: Patching LLMObsSpanWriter.flush");
        const SpanWriter = await loadLLMObsSpanWriter();
        this.originalFlush = SpanWriter.prototype.flush;
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const self = this;
        SpanWriter.prototype.flush = function () {
            if (this._buffer.length > 0) {
                getLogger().debug(`LLMObsOtelExporter: Converting ${this._buffer.length} spans to OTEL (gen_ai.*)`);
                for (const spanEvent of this._buffer) {
                    try {
                        self.emitFullFidelitySpan(spanEvent);
                    }
                    catch (err) {
                        getLogger().error(`LLMObsOtelExporter: Failed to convert span: ${err instanceof Error ? err.message : String(err)}`);
                    }
                }
            }
            // Clear buffer - don't send to Datadog
            this._buffer = [];
            this._bufferSize = 0;
        };
        getLogger().info("LLMObsOtelExporter: Initialized for CLOUD_PREM mode (Collector handles tier separation)");
    }
    /**
     * Emit a FULL-FIDELITY OTEL span from an LLMObs event.
     *
     * This method includes ALL data - the Collector is responsible for
     * sanitizing and routing to the appropriate tier.
     *
     * Attributes emitted (gen_ai.* only, per v0.3.0 contract):
     * - gen_ai.system (provider)
     * - gen_ai.request.model
     * - gen_ai.operation.name
     * - gen_ai.usage.* (token counts)
     * - gen_ai.request.status
     * - gen_ai.client.duration_ms
     * - gen_ai.error.category
     * - gen_ai.response.finish_reasons
     * - gen_ai.client.time_to_first_token
     *
     * Identity attributes (Collector will hash for Tier 2):
     * - user-email
     * - organization-id
     * - application-id
     * - session.id
     *
     * Content attributes (Collector will strip for Tier 2):
     * - llmobs.input
     * - llmobs.output
     */
    emitFullFidelitySpan(event) {
        const spanKind = event.meta["span.kind"] || "unknown";
        const otelKind = spanKind === "llm" ? SpanKind.CLIENT : SpanKind.INTERNAL;
        // Convert nanoseconds to milliseconds for OTEL
        const startTimeMs = event.start_ns / 1e6;
        const endTimeMs = startTimeMs + event.duration / 1e6;
        const durationMs = event.duration / 1e6;
        // Map LLMObs span kind to OTEL span name pattern
        const spanName = this.mapToSpanName(event.name, spanKind);
        // Build FULL-FIDELITY attributes using gen_ai.* ONLY
        const attributes = {
            // === Tier policy hint (tells Collector to include in Tier 3) ===
            [TIER_HINT_ATTRIBUTE]: TIER_HINT_INCLUDE_TIER3,
            // === OTEL GenAI semantic conventions (required) ===
            "gen_ai.system": this.normalizeSystem(event.meta.model_provider),
            "gen_ai.request.model": event.meta.model_name,
            "gen_ai.operation.name": this.extractOperationName(event),
            // === Status ===
            "gen_ai.request.status": event.status === "error" ? "error" : "success",
            // === Timing ===
            "gen_ai.client.duration_ms": Math.round(durationMs),
            // === Token metrics (gen_ai.usage.*) ===
            ...this.extractTokenMetrics(event.metrics),
            // === Identity from tags (Collector will hash for Tier 2) ===
            ...this.extractIdentityFromTags(event.tags),
            // === Full content (Collector will strip for Tier 2, preserve for Tier 3) ===
            "llmobs.input": event.meta.input
                ? this.truncateAttribute(this.safeStringify(event.meta.input))
                : undefined,
            "llmobs.output": event.meta.output
                ? this.truncateAttribute(this.safeStringify(event.meta.output))
                : undefined,
        };
        // Error handling
        if (event.status === "error") {
            attributes["error"] = true;
            attributes["gen_ai.error.category"] = this.extractErrorCategory(event);
        }
        // Finish reason
        const finishReason = this.extractFinishReason(event);
        if (finishReason) {
            attributes["gen_ai.response.finish_reasons"] = finishReason;
        }
        // Time to first token
        const timeToFirstToken = this.extractTimeToFirstToken(event);
        if (timeToFirstToken !== undefined) {
            attributes["gen_ai.client.time_to_first_token"] = timeToFirstToken;
        }
        // Create the OTEL span
        const otelSpan = this.otelTracer.startSpan(spanName, {
            kind: otelKind,
            startTime: startTimeMs,
            attributes: this.filterUndefined(attributes),
        });
        // Set OTEL status for errors
        if (event.status === "error") {
            otelSpan.setStatus({
                code: 2, // SpanStatusCode.ERROR
            });
        }
        // End span with correct end time
        otelSpan.end(endTimeMs);
        getLogger().debug(`LLMObsOtelExporter: Emitted span ${spanName} (${spanKind})`);
    }
    /**
     * Map LLMObs span name to contract-defined span pattern.
     * Uses gen_ai.* naming per v0.3.0 contract.
     */
    mapToSpanName(originalName, spanKind) {
        // Map to contract-defined span names (gen_ai.* per v0.3.0)
        if (spanKind === "llm") {
            return "gen_ai.request";
        }
        if (spanKind === "tool") {
            return "gen_ai.tool_call";
        }
        // Map ai.streamText before the generic workflow/task handler,
        // since these spans may have spanKind "task" and would otherwise
        // be caught by the block below.
        if (originalName === "ai.streamText") {
            return "gen_ai.stream";
        }
        if (originalName === "ai.streamText.step") {
            return "gen_ai.stream.step";
        }
        if (spanKind === "workflow" || spanKind === "task") {
            // Clark session spans
            if (originalName.includes("clark") || originalName.includes("Clark")) {
                return "clark.session";
            }
            return originalName.startsWith("ai.")
                ? originalName
                : `ai.${originalName}`;
        }
        return originalName;
    }
    /**
     * Extract token metrics with gen_ai.usage.* naming.
     */
    extractTokenMetrics(metrics) {
        if (!metrics)
            return {};
        const result = {};
        // Input tokens
        const inputTokens = metrics.input_tokens ??
            metrics.inputTokens ??
            metrics.prompt_tokens ??
            metrics.promptTokens;
        if (inputTokens !== undefined) {
            result["gen_ai.usage.input_tokens"] = inputTokens;
        }
        // Output tokens
        const outputTokens = metrics.output_tokens ??
            metrics.outputTokens ??
            metrics.completion_tokens ??
            metrics.completionTokens;
        if (outputTokens !== undefined) {
            result["gen_ai.usage.output_tokens"] = outputTokens;
        }
        // Total tokens
        const totalTokens = metrics.total_tokens ??
            metrics.totalTokens ??
            (inputTokens !== undefined && outputTokens !== undefined
                ? inputTokens + outputTokens
                : undefined);
        if (totalTokens !== undefined) {
            result["gen_ai.usage.total_tokens"] = totalTokens;
        }
        // Reasoning tokens (o1-style models)
        const reasoningTokens = metrics.reasoning_tokens ?? metrics.reasoningTokens;
        if (reasoningTokens !== undefined) {
            result["gen_ai.usage.reasoning_tokens"] = reasoningTokens;
        }
        // Cached tokens
        const cacheReadTokens = metrics.cached_tokens ??
            metrics.cachedTokens ??
            metrics.cache_read_tokens ??
            metrics.cacheReadTokens;
        const cacheWriteTokens = metrics.cache_write_tokens ?? metrics.cacheWriteTokens;
        const cachedTokens = cacheReadTokens !== undefined && cacheWriteTokens !== undefined
            ? cacheReadTokens + cacheWriteTokens
            : (cacheReadTokens ?? cacheWriteTokens);
        if (cachedTokens !== undefined) {
            result["gen_ai.usage.cached_tokens"] = cachedTokens;
        }
        return result;
    }
    /**
     * Extract identity attributes from tags.
     * These will be hashed by the Collector for Tier 2.
     */
    extractIdentityFromTags(tags) {
        const result = {};
        for (const tag of tags) {
            const colonIndex = tag.indexOf(":");
            if (colonIndex === -1)
                continue;
            const key = tag.slice(0, colonIndex);
            const value = tag.slice(colonIndex + 1);
            // Map known identity tags
            switch (key) {
                case "user_email":
                case "userEmail":
                case "user.email":
                    result["user-email"] = value;
                    break;
                case "org_id":
                case "orgId":
                case "org.id":
                case "organization_id":
                case "organizationId":
                    result["organization-id"] = value;
                    break;
                case "app_id":
                case "appId":
                case "app.id":
                case "application_id":
                case "applicationId":
                    result["application-id"] = value;
                    break;
                case "session_id":
                case "sessionId":
                    result["session.id"] = value;
                    break;
                default:
                    // Only pass through explicitly known identity-related tags
                    break;
            }
        }
        return result;
    }
    /**
     * Normalize provider to OTEL gen_ai.system canonical names.
     */
    normalizeSystem(provider) {
        if (!provider)
            return undefined;
        const trimmed = provider.trim();
        if (!trimmed)
            return undefined;
        const normalized = trimmed.toLowerCase().replace(/[-_\s]/g, "_");
        return GENAI_SYSTEM_MAPPING[normalized] || trimmed.toLowerCase();
    }
    /**
     * Extract operation name from event.
     * Maps span kind to OTEL gen_ai.operation.name values.
     */
    extractOperationName(event) {
        const spanKind = event.meta["span.kind"] || "";
        const name = event.name.toLowerCase();
        // Map span kind
        if (spanKind === "tool")
            return "execute_tool";
        if (spanKind === "workflow" || spanKind === "agent")
            return "invoke_agent";
        if (spanKind === "embedding")
            return "embeddings";
        // Infer from span name
        if (name.includes("embed"))
            return "embeddings";
        if (name.includes("agent") || name.includes("clark"))
            return "invoke_agent";
        if (name.includes("tool"))
            return "execute_tool";
        if (name.includes("complete") && !name.includes("chat"))
            return "text_completion";
        // Default to chat
        return "chat";
    }
    /**
     * Extract error category from event metadata.
     */
    extractErrorCategory(event) {
        // Check metadata for explicit error category
        const metadata = event.meta.metadata;
        if (metadata) {
            const category = metadata.errorCategory;
            if (category && this.isValidErrorCategory(category)) {
                return category;
            }
        }
        // Check tags for error category
        for (const tag of event.tags) {
            if (tag.startsWith("errorCategory:")) {
                const category = tag.slice("errorCategory:".length);
                if (this.isValidErrorCategory(category)) {
                    return category;
                }
            }
            if (tag === "wasAborted:true") {
                return "aborted";
            }
        }
        return "unknown";
    }
    /**
     * Extract finish reason from event.
     */
    extractFinishReason(event) {
        const metadata = event.meta.metadata;
        if (metadata) {
            const reason = metadata.finishReason;
            if (reason && this.isValidFinishReason(reason)) {
                return reason;
            }
        }
        // Check tags
        for (const tag of event.tags) {
            if (tag.startsWith("finishReason:")) {
                const reason = tag.slice("finishReason:".length);
                if (this.isValidFinishReason(reason)) {
                    return reason;
                }
            }
        }
        return undefined;
    }
    /**
     * Extract time to first token from event metadata.
     * Returns value in seconds (OTEL convention).
     */
    extractTimeToFirstToken(event) {
        const metadata = event.meta.metadata;
        if (!metadata)
            return undefined;
        // Check for timeToFirstToken (in seconds, from Datadog LLMObs)
        const ttftSeconds = metadata.timeToFirstToken;
        if (typeof ttftSeconds === "number" && ttftSeconds > 0) {
            return ttftSeconds;
        }
        // Check for timeToFirstTokenMs (in milliseconds)
        const ttftMs = metadata.timeToFirstTokenMs;
        if (typeof ttftMs === "number" && ttftMs > 0) {
            return ttftMs / 1000;
        }
        // Check metrics for timeToFirstToken
        if (event.metrics) {
            const metricsttft = event.metrics.timeToFirstToken;
            if (typeof metricsttft === "number" && metricsttft > 0) {
                return metricsttft;
            }
        }
        return undefined;
    }
    /**
     * Safely stringify a value for span attributes.
     */
    safeStringify(value) {
        if (typeof value === "string")
            return value;
        try {
            return JSON.stringify(value);
        }
        catch {
            return String(value);
        }
    }
    /**
     * Truncate an attribute value to MAX_ATTRIBUTE_LENGTH.
     * Prevents OTEL Collector rejection and memory pressure from very large values.
     */
    truncateAttribute(value) {
        if (value.length <= MAX_ATTRIBUTE_LENGTH)
            return value;
        return (value.slice(0, MAX_ATTRIBUTE_LENGTH) +
            `... [truncated ${value.length - MAX_ATTRIBUTE_LENGTH} chars]`);
    }
    /**
     * Validate error category is in the allowed enum.
     */
    isValidErrorCategory(category) {
        return VALID_ERROR_CATEGORIES.has(category);
    }
    /**
     * Validate finish reason is in the allowed enum.
     */
    isValidFinishReason(reason) {
        return VALID_FINISH_REASONS.has(reason);
    }
    /**
     * Filter out undefined values from attributes object.
     */
    filterUndefined(attrs) {
        const result = {};
        for (const [key, value] of Object.entries(attrs)) {
            if (value !== undefined) {
                result[key] = value;
            }
        }
        return result;
    }
    /**
     * Restore the original LLMObsSpanWriter.flush method.
     * Used for cleanup/testing.
     */
    restore() {
        if (this.originalFlush && LLMObsSpanWriter) {
            LLMObsSpanWriter.prototype.flush = this.originalFlush;
            this.originalFlush = null;
            getLogger().debug("LLMObsOtelExporter: Restored original flush method");
        }
    }
}
/**
 * Singleton instance of the LLMObs-to-OTEL exporter.
 * Call llmobsOtelExporter.init() in CLOUD_PREM mode to enable.
 *
 * NOTE: This exporter emits FULL-FIDELITY spans.
 * The OTEL Collector handles tier separation (hashing, stripping, routing).
 */
export const llmobsOtelExporter = new LLMObsOtelExporter();
//# sourceMappingURL=otel-exporter.js.map