import { MetricsRecordingHeader } from "@livekit/protocol";
import { ThrowsPromise } from "@livekit/throws-transformer/throws";
import {
  context as otelContext,
  trace
} from "@opentelemetry/api";
import { SeverityNumber } from "@opentelemetry/api-logs";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { CompressionAlgorithm } from "@opentelemetry/otlp-exporter-base";
import { Resource } from "@opentelemetry/resources";
import { BatchSpanProcessor, NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import FormData from "form-data";
import { AccessToken } from "livekit-server-sdk";
import fs from "node:fs/promises";
import { isInstructions, renderInstructions } from "../llm/chat_context.js";
import { enableOtelLogging } from "../log.js";
import { filterZeroValues } from "../metrics/model_usage.js";
import { SimpleOTLPHttpLogExporter } from "./otel_http_exporter.js";
import { flushPinoLogs, initPinoCloudExporter } from "./pino_otel_transport.js";
class DynamicTracer {
  tracerProvider;
  tracer;
  instrumentingModuleName;
  constructor(instrumentingModuleName) {
    this.instrumentingModuleName = instrumentingModuleName;
    this.tracerProvider = trace.getTracerProvider();
    this.tracer = trace.getTracer(instrumentingModuleName);
  }
  /**
   * Set a new tracer provider. This updates the underlying tracer instance.
   * @param provider - The new tracer provider to use
   */
  setProvider(provider) {
    this.tracerProvider = provider;
    this.tracer = this.tracerProvider.getTracer(this.instrumentingModuleName);
  }
  /**
   * Get the underlying OpenTelemetry tracer.
   * Use this to access the full Tracer API when needed.
   */
  getTracer() {
    return this.tracer;
  }
  /**
   * Start a span manually (without making it active).
   * You must call span.end() when done.
   *
   * @param options - Span configuration including name
   * @returns The created span
   */
  startSpan(options) {
    const ctx = options.context || otelContext.active();
    const span = this.tracer.startSpan(
      options.name,
      {
        attributes: options.attributes,
        startTime: options.startTime
      },
      ctx
    );
    return span;
  }
  /**
   * Start a new span and make it active in the current context.
   * The span will automatically be ended when the provided function completes (unless endOnExit=false).
   *
   * @param fn - The function to execute within the span context
   * @param options - Span configuration including name
   * @returns The result of the provided function
   */
  async startActiveSpan(fn, options) {
    const ctx = options.context || otelContext.active();
    const endOnExit = options.endOnExit === void 0 ? true : options.endOnExit;
    const opts = { attributes: options.attributes, startTime: options.startTime };
    return await this.tracer.startActiveSpan(options.name, opts, ctx, async (span) => {
      try {
        return await fn(span);
      } finally {
        if (endOnExit) {
          span.end();
        }
      }
    });
  }
  /**
   * Synchronous version of startActiveSpan for non-async operations.
   *
   * @param fn - The function to execute within the span context
   * @param options - Span configuration including name
   * @returns The result of the provided function
   */
  startActiveSpanSync(fn, options) {
    const ctx = options.context || otelContext.active();
    const endOnExit = options.endOnExit === void 0 ? true : options.endOnExit;
    const opts = { attributes: options.attributes, startTime: options.startTime };
    return this.tracer.startActiveSpan(options.name, opts, ctx, (span) => {
      try {
        return fn(span);
      } finally {
        if (endOnExit) {
          span.end();
        }
      }
    });
  }
}
const tracer = new DynamicTracer("livekit-agents");
class MetadataSpanProcessor {
  metadata;
  constructor(metadata) {
    this.metadata = metadata;
  }
  onStart(span, _parentContext) {
    span.setAttributes(this.metadata);
  }
  onEnd(_span) {
  }
  shutdown() {
    return ThrowsPromise.resolve();
  }
  forceFlush() {
    return ThrowsPromise.resolve();
  }
}
function setTracerProvider(provider, options) {
  if (options == null ? void 0 : options.metadata) {
    provider.addSpanProcessor(new MetadataSpanProcessor(options.metadata));
  }
  tracer.setProvider(provider);
}
async function setupCloudTracer(options) {
  const { roomId, jobId, cloudHostname, enableTraces = true, enableLogs = true } = options;
  const apiKey = process.env.LIVEKIT_API_KEY;
  const apiSecret = process.env.LIVEKIT_API_SECRET;
  if (!apiKey || !apiSecret) {
    throw new Error("LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for cloud tracing");
  }
  const token = new AccessToken(apiKey, apiSecret, {
    identity: "livekit-agents-telemetry",
    ttl: "6h"
  });
  token.addObservabilityGrant({ write: true });
  try {
    const jwt = await token.toJwt();
    const headers = {
      Authorization: `Bearer ${jwt}`
    };
    const metadata = {
      room_id: roomId,
      job_id: jobId
    };
    const resource = new Resource({
      [ATTR_SERVICE_NAME]: "livekit-agents",
      room_id: roomId,
      job_id: jobId
    });
    if (enableTraces) {
      const spanExporter = new OTLPTraceExporter({
        url: `https://${cloudHostname}/observability/traces/otlp/v0`,
        headers,
        compression: CompressionAlgorithm.GZIP
      });
      const tracerProvider = new NodeTracerProvider({
        resource,
        spanProcessors: [new MetadataSpanProcessor(metadata), new BatchSpanProcessor(spanExporter)]
      });
      tracerProvider.register();
      setTracerProvider(tracerProvider);
    }
    if (enableLogs) {
      initPinoCloudExporter({
        cloudHostname,
        roomId,
        jobId
      });
      enableOtelLogging();
    }
  } catch (error) {
    console.error("Failed to setup cloud tracer:", error);
    throw error;
  }
}
async function flushOtelLogs() {
  await flushPinoLogs();
}
const ROLE_MAP = {
  developer: "DEVELOPER",
  system: "SYSTEM",
  user: "USER",
  assistant: "ASSISTANT"
};
function chatItemToProto(item) {
  var _a, _b;
  const itemDict = {};
  if (item.type === "message") {
    const msg = {
      id: item.id,
      role: ROLE_MAP[item.role] ?? item.role.toUpperCase(),
      content: item.content.map((c) => ({
        text: isInstructions(c) ? c.value : c
      })),
      createdAt: toRFC3339(item.createdAt)
    };
    if (item.interrupted) {
      msg.interrupted = item.interrupted;
    }
    if (item.extra && Object.keys(item.extra).length > 0) {
      msg.extra = item.extra;
    }
    if (item.transcriptConfidence !== void 0) {
      msg.transcriptConfidence = item.transcriptConfidence;
    }
    const metrics = item.metrics;
    if (metrics && Object.keys(metrics).length > 0) {
      const protoMetrics = {};
      if (metrics.startedSpeakingAt !== void 0) {
        protoMetrics.startedSpeakingAt = toRFC3339(metrics.startedSpeakingAt * 1e3);
      }
      if (metrics.stoppedSpeakingAt !== void 0) {
        protoMetrics.stoppedSpeakingAt = toRFC3339(metrics.stoppedSpeakingAt * 1e3);
      }
      if (metrics.transcriptionDelay !== void 0) {
        protoMetrics.transcriptionDelay = metrics.transcriptionDelay;
      }
      if (metrics.endOfTurnDelay !== void 0) {
        protoMetrics.endOfTurnDelay = metrics.endOfTurnDelay;
      }
      if (metrics.onUserTurnCompletedDelay !== void 0) {
        protoMetrics.onUserTurnCompletedDelay = metrics.onUserTurnCompletedDelay;
      }
      if (metrics.llmNodeTtft !== void 0) {
        protoMetrics.llmNodeTtft = metrics.llmNodeTtft;
      }
      if (metrics.ttsNodeTtfb !== void 0) {
        protoMetrics.ttsNodeTtfb = metrics.ttsNodeTtfb;
      }
      if (metrics.playbackLatency !== void 0) {
        protoMetrics.playbackLatency = metrics.playbackLatency;
      }
      if (metrics.e2eLatency !== void 0) {
        protoMetrics.e2eLatency = metrics.e2eLatency;
      }
      msg.metrics = protoMetrics;
    }
    itemDict.message = msg;
  } else if (item.type === "function_call") {
    itemDict.functionCall = {
      id: item.id,
      callId: item.callId,
      arguments: item.args,
      name: item.name,
      createdAt: toRFC3339(item.createdAt)
    };
  } else if (item.type === "function_call_output") {
    itemDict.functionCallOutput = {
      id: item.id,
      name: item.name,
      callId: item.callId,
      output: item.output,
      isError: item.isError,
      createdAt: toRFC3339(item.createdAt)
    };
  } else if (item.type === "agent_handoff") {
    const handoff = {
      id: item.id,
      newAgentId: item.newAgentId,
      createdAt: toRFC3339(item.createdAt)
    };
    if (item.oldAgentId !== void 0 && item.oldAgentId !== null && item.oldAgentId !== "") {
      handoff.oldAgentId = item.oldAgentId;
    }
    itemDict.agentHandoff = handoff;
  } else if (item.type === "agent_config_update") {
    const configUpdate = {
      id: item.id,
      createdAt: toRFC3339(item.createdAt)
    };
    if (item.instructions !== void 0) {
      configUpdate.instructions = renderInstructions(item.instructions);
    }
    if (item.toolsAdded !== void 0) {
      configUpdate.toolsAdded = item.toolsAdded;
    }
    if (item.toolsRemoved !== void 0) {
      configUpdate.toolsRemoved = item.toolsRemoved;
    }
    itemDict.agentConfigUpdate = configUpdate;
  }
  try {
    if (item.type === "function_call" && typeof ((_a = itemDict.functionCall) == null ? void 0 : _a.arguments) === "string") {
      itemDict.functionCall.arguments = JSON.parse(itemDict.functionCall.arguments);
    } else if (item.type === "function_call_output" && typeof ((_b = itemDict.functionCallOutput) == null ? void 0 : _b.output) === "string") {
      itemDict.functionCallOutput.output = JSON.parse(itemDict.functionCallOutput.output);
    }
  } catch {
  }
  return itemDict;
}
function toRFC3339(valueMs) {
  const dt = valueMs instanceof Date ? valueMs : new Date(valueMs);
  const truncated = new Date(Math.floor(dt.getTime()));
  return truncated.toISOString();
}
async function uploadSessionReport(options) {
  var _a;
  const { agentName, cloudHostname, report } = options;
  const logExporter = new SimpleOTLPHttpLogExporter({
    cloudHostname,
    resourceAttributes: {
      room_id: report.roomId,
      job_id: report.jobId
    },
    scopeName: "chat_history",
    scopeAttributes: {
      room_id: report.roomId,
      job_id: report.jobId,
      room: report.room
    }
  });
  const logRecords = [];
  const commonAttrs = {
    room_id: report.roomId,
    job_id: report.jobId,
    "logger.name": "chat_history"
  };
  const usage = ((_a = report.modelUsage) == null ? void 0 : _a.map(filterZeroValues)) || null;
  logRecords.push({
    body: "session report",
    timestampMs: report.startedAt || report.timestamp || 0,
    attributes: {
      ...commonAttrs,
      "session.options": report.options || {},
      "session.report_timestamp": report.timestamp,
      agent_name: agentName,
      usage
    }
  });
  let lastTimestamp = 0;
  const chatItems = report.recordingOptions.transcript ? report.chatHistory.items : [];
  for (const item of chatItems) {
    if (!item) continue;
    const hasValidTimestamp = Number.isFinite(item.createdAt);
    let itemTimestamp = hasValidTimestamp ? item.createdAt : Date.now();
    if (itemTimestamp <= lastTimestamp) {
      itemTimestamp = lastTimestamp + 1e-3;
    }
    lastTimestamp = itemTimestamp;
    const itemProto = chatItemToProto(item);
    let severityNumber = SeverityNumber.UNSPECIFIED;
    let severityText = "unspecified";
    if (item.type === "function_call_output" && item.isError) {
      severityNumber = SeverityNumber.ERROR;
      severityText = "error";
    }
    logRecords.push({
      body: "chat item",
      timestampMs: itemTimestamp,
      // Adjusted for monotonic ordering
      attributes: { "chat.item": itemProto, ...commonAttrs },
      severityNumber,
      severityText
    });
  }
  await logExporter.export(logRecords);
  const hasAudio = Boolean(
    report.recordingOptions.audio && report.audioRecordingPath && report.audioRecordingStartedAt
  );
  if (!report.recordingOptions.transcript && !hasAudio) {
    return;
  }
  const apiKey = process.env.LIVEKIT_API_KEY;
  const apiSecret = process.env.LIVEKIT_API_SECRET;
  if (!apiKey || !apiSecret) {
    throw new Error("LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for session upload");
  }
  const token = new AccessToken(apiKey, apiSecret, { ttl: "6h" });
  token.addObservabilityGrant({ write: true });
  const jwt = await token.toJwt();
  const formData = new FormData();
  const audioStartTime = report.audioRecordingStartedAt ?? 0;
  const headerMsg = new MetricsRecordingHeader({
    roomId: report.roomId,
    duration: BigInt(0),
    // TODO: Calculate actual duration from report
    startTime: {
      seconds: BigInt(Math.floor(audioStartTime / 1e3)),
      nanos: Math.floor(audioStartTime % 1e3 * 1e6)
    }
  });
  const headerBytes = Buffer.from(headerMsg.toBinary());
  formData.append("header", headerBytes, {
    filename: "header.binpb",
    contentType: "application/protobuf",
    knownLength: headerBytes.length,
    header: {
      "Content-Type": "application/protobuf",
      "Content-Length": headerBytes.length.toString()
    }
  });
  if (report.recordingOptions.transcript) {
    const chatHistoryJson = JSON.stringify(report.chatHistory.toJSON({ excludeTimestamp: false }));
    const chatHistoryBuffer = Buffer.from(chatHistoryJson, "utf-8");
    formData.append("chat_history", chatHistoryBuffer, {
      filename: "chat_history.json",
      contentType: "application/json",
      knownLength: chatHistoryBuffer.length,
      header: {
        "Content-Type": "application/json",
        "Content-Length": chatHistoryBuffer.length.toString()
      }
    });
  }
  if (report.recordingOptions.audio && report.audioRecordingPath && report.audioRecordingStartedAt) {
    let audioBytes;
    try {
      audioBytes = await fs.readFile(report.audioRecordingPath);
    } catch {
      audioBytes = Buffer.alloc(0);
    }
    if (audioBytes.length > 0) {
      formData.append("audio", audioBytes, {
        filename: "recording.ogg",
        contentType: "audio/ogg",
        knownLength: audioBytes.length,
        header: {
          "Content-Type": "audio/ogg",
          "Content-Length": audioBytes.length.toString()
        }
      });
    }
  }
  return new ThrowsPromise((resolve, reject) => {
    formData.submit(
      {
        protocol: "https:",
        host: cloudHostname,
        path: "/observability/recordings/v0",
        method: "POST",
        headers: {
          Authorization: `Bearer ${jwt}`
        }
      },
      (err, res) => {
        if (err) {
          reject(new Error(`Failed to upload session report: ${err.message}`));
          return;
        }
        if (res.statusCode && res.statusCode >= 400) {
          let body = "";
          res.on("data", (chunk) => {
            body += chunk.toString();
          });
          res.on("error", (readErr) => {
            reject(
              new Error(
                `Failed to upload session report: ${res.statusCode} ${res.statusMessage} (body read error: ${readErr.message})`
              )
            );
          });
          res.on("end", () => {
            reject(
              new Error(
                `Failed to upload session report: ${res.statusCode} ${res.statusMessage} - ${body}`
              )
            );
          });
          return;
        }
        res.resume();
        res.on("error", (readErr) => reject(new Error(`Response read error: ${readErr.message}`)));
        res.on("end", () => resolve());
      }
    );
  });
}
export {
  flushOtelLogs,
  setTracerProvider,
  setupCloudTracer,
  tracer,
  uploadSessionReport
};
//# sourceMappingURL=traces.js.map