import {
  readEnvVar,
  resolveHomeDir
} from "./chunk-MARWOCVP.js";
import "./chunk-PZ5AY32C.js";

// src/opik-exporter.ts
import { createHash, randomBytes } from "crypto";
import fs from "fs";
import path from "path";
var OPIK_EXPORTER_SLOT = "__openclawOpikExporter";
function readOpikOpenclawConfig(log) {
  try {
    const configPath = readEnvVar("OPENCLAW_ENGRAM_CONFIG_PATH") || readEnvVar("OPENCLAW_CONFIG_PATH") || path.join(resolveHomeDir(), ".openclaw", "openclaw.json");
    const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
    const entry = raw?.plugins?.entries?.["opik-openclaw"];
    if (!entry?.enabled || !entry?.config) return {};
    const c = entry.config;
    return {
      apiUrl: typeof c.apiUrl === "string" && c.apiUrl.length > 0 ? c.apiUrl : void 0,
      projectName: typeof c.projectName === "string" ? c.projectName : void 0,
      workspaceName: typeof c.workspaceName === "string" ? c.workspaceName : void 0,
      apiKey: typeof c.apiKey === "string" && c.apiKey.length > 0 ? c.apiKey : void 0
    };
  } catch (err) {
    log?.debug?.(`[opik-exporter] could not read opik-openclaw config: ${err}`);
    return {};
  }
}
function uuidV7() {
  const now = Date.now();
  const bytes = randomBytes(16);
  bytes[0] = now / 2 ** 40 & 255;
  bytes[1] = now / 2 ** 32 & 255;
  bytes[2] = now / 2 ** 24 & 255;
  bytes[3] = now / 2 ** 16 & 255;
  bytes[4] = now / 2 ** 8 & 255;
  bytes[5] = now & 255;
  bytes[6] = bytes[6] & 15 | 112;
  bytes[8] = bytes[8] & 63 | 128;
  const hex = bytes.toString("hex");
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
function buildHeaders(cfg) {
  const headers = { "Content-Type": "application/json" };
  if (cfg.workspaceName) headers["Comet-Workspace"] = cfg.workspaceName;
  if (cfg.apiKey) headers["authorization"] = cfg.apiKey;
  return headers;
}
async function postSpanBatch(cfg, spans, log) {
  const url = `${cfg.apiUrl.replace(/\/$/, "")}/v1/private/spans/batch`;
  try {
    const res = await fetch(url, {
      method: "POST",
      headers: buildHeaders(cfg),
      body: JSON.stringify({ spans })
    });
    if (!res.ok) {
      const text = await res.text().catch(() => "");
      log.debug?.(`[opik-exporter] span batch failed ${res.status}: ${text}`);
    }
  } catch (err) {
    log.debug?.(`[opik-exporter] span batch error: ${err}`);
  }
}
async function postTraceBatch(cfg, traces, log) {
  const url = `${cfg.apiUrl.replace(/\/$/, "")}/v1/private/traces/batch`;
  try {
    const res = await fetch(url, {
      method: "POST",
      headers: buildHeaders(cfg),
      body: JSON.stringify({ traces })
    });
    if (!res.ok) {
      const text = await res.text().catch(() => "");
      log.debug?.(`[opik-exporter] trace batch failed ${res.status}: ${text}`);
      return false;
    }
    return true;
  } catch (err) {
    log.debug?.(`[opik-exporter] trace batch error: ${err}`);
    return false;
  }
}
var OpikExporter = class _OpikExporter {
  cfg;
  log;
  _handler;
  inFlight = /* @__PURE__ */ new Map();
  /** Track which trace_ids we have already created parent trace objects for. */
  createdTraces = /* @__PURE__ */ new Set();
  /** In-flight ensureTrace promises keyed by traceId — concurrent callers await the same promise. */
  pendingTraces = /* @__PURE__ */ new Map();
  /** TTL for in-flight LLM entries (ms). Entries older than this are discarded. */
  static IN_FLIGHT_TTL_MS = 5 * 60 * 1e3;
  // 5 minutes
  constructor(cfg, log) {
    this.cfg = cfg;
    this.log = log;
  }
  /**
   * Subscribe to Engram's global trace slot. Safe to call multiple times —
   * chains with existing subscribers (e.g. Langfuse) rather than replacing.
   */
  subscribe() {
    if (!this.cfg.enabled) return;
    const g = globalThis;
    const active = g[OPIK_EXPORTER_SLOT];
    if (active === this) {
      this.log.debug?.("[opik-exporter] already subscribed \u2014 skipping duplicate");
      return;
    }
    if (active) active._detach();
    g[OPIK_EXPORTER_SLOT] = this;
    const existing = g.__openclawEngramTrace;
    const handler = (event) => {
      try {
        this.handleEvent(event);
      } catch (err) {
        this.log.debug?.(`[opik-exporter] handler error: ${err}`);
      }
    };
    this._handler = handler;
    g.__openclawEngramTrace = typeof existing === "function" ? (event) => {
      try {
        existing(event);
      } catch {
      }
      handler(event);
    } : handler;
    this.log.info(
      `[opik-exporter] subscribed \u2014 apiUrl=${this.cfg.apiUrl} project=${this.cfg.projectName}`
    );
  }
  /** Deactivate this exporter instance.
   *
   * We intentionally do NOT restore __openclawEngramTrace to a prior handler
   * because any subscriber that chained *after* us would be silently dropped
   * by such a restoration. Instead we mark the instance as disabled so that
   * handleEvent() becomes a no-op while the callback chain itself is left
   * untouched. The next subscribe() call from a fresh instance will re-enter
   * the chain on top of whatever is there at that point.
   */
  _detach() {
    const g = globalThis;
    if (g[OPIK_EXPORTER_SLOT] === this) delete g[OPIK_EXPORTER_SLOT];
    this.cfg.enabled = false;
    this._handler = void 0;
    this.log.debug?.("[opik-exporter] detached \u2014 events silenced, chain preserved");
  }
  unsubscribe() {
    this._detach();
  }
  handleEvent(event) {
    if (!this.cfg.enabled) return;
    if (event.kind === "recall_summary") {
      void this.onRecall(event);
    } else if (event.kind === "llm_start") {
      this.onLlmStart(event);
    } else if (event.kind === "llm_end" || event.kind === "llm_error") {
      void this.onLlmEnd(event);
    }
  }
  // -------------------------------------------------------------------------
  // Recall events → general span
  // -------------------------------------------------------------------------
  async onRecall(evt) {
    const now = /* @__PURE__ */ new Date();
    const startTime = new Date(now.getTime() - evt.durationMs).toISOString();
    const endTime = now.toISOString();
    const input = {
      recallMode: evt.recallMode,
      recalledMemoryCount: evt.recalledMemoryCount,
      recallNamespaces: evt.recallNamespaces,
      source: evt.source,
      qmdEnabled: evt.qmdEnabled,
      qmdAvailable: evt.qmdAvailable,
      retrievalQueryLength: evt.retrievalQueryLength,
      recallResultLimit: evt.recallResultLimit,
      promptLength: evt.promptLength
    };
    if (this.cfg.traceRecallContent && evt.recalledContent) {
      input.recalledContent = evt.recalledContent;
    }
    const metadata = {
      source: "engram",
      injected: evt.injected,
      contextChars: evt.contextChars,
      durationMs: evt.durationMs
    };
    if (evt.identityInjectionMode) {
      metadata.identityInjectionMode = evt.identityInjectionMode;
      metadata.identityInjectedChars = evt.identityInjectedChars;
    }
    if (evt.timings) metadata.timings = evt.timings;
    const traceId = evt.sessionKey ? this.sessionToTraceId(evt.sessionKey) : uuidV7();
    await this.ensureTrace(traceId, evt.sessionKey ?? "engram:recall", startTime, endTime);
    const output = {
      injected: evt.injected,
      recalledMemoryCount: evt.recalledMemoryCount,
      contextChars: evt.contextChars,
      durationMs: evt.durationMs
    };
    if (this.cfg.traceRecallContent && evt.recalledContent) {
      output.recalledContent = evt.recalledContent;
    }
    const span = {
      id: uuidV7(),
      trace_id: traceId,
      project_name: this.cfg.projectName,
      name: "engram:recall",
      type: "general",
      start_time: startTime,
      end_time: endTime,
      input,
      output,
      metadata,
      tags: ["engram", "recall"]
    };
    await postSpanBatch(this.cfg, [span], this.log);
  }
  // -------------------------------------------------------------------------
  // Engram internal LLM events (extraction / consolidation) → llm span
  // -------------------------------------------------------------------------
  onLlmStart(evt) {
    this.inFlight.set(evt.traceId, {
      startedAt: Date.now(),
      startTime: (/* @__PURE__ */ new Date()).toISOString(),
      model: evt.model,
      operation: evt.operation,
      input: evt.input,
      spanId: this.sessionToTraceId(evt.traceId + ":span"),
      traceId: this.sessionToTraceId(evt.traceId)
    });
  }
  sweepStaleInFlight() {
    const cutoff = Date.now() - _OpikExporter.IN_FLIGHT_TTL_MS;
    for (const [id, entry] of this.inFlight) {
      if (entry.startedAt < cutoff) {
        this.inFlight.delete(id);
      }
    }
  }
  async onLlmEnd(evt) {
    if (this.inFlight.size > 20) this.sweepStaleInFlight();
    const state = this.inFlight.get(evt.traceId);
    this.inFlight.delete(evt.traceId);
    const startTime = state?.startTime ?? (/* @__PURE__ */ new Date()).toISOString();
    const endTime = (/* @__PURE__ */ new Date()).toISOString();
    const usage = {};
    if (evt.tokenUsage?.input != null) usage.prompt_tokens = evt.tokenUsage.input;
    if (evt.tokenUsage?.output != null) usage.completion_tokens = evt.tokenUsage.output;
    if (evt.tokenUsage?.total != null) usage.total_tokens = evt.tokenUsage.total;
    const traceId = state?.traceId ?? uuidV7();
    await this.ensureTrace(traceId, `engram:${evt.operation}`, startTime, endTime);
    const span = {
      id: state?.spanId ?? uuidV7(),
      trace_id: traceId,
      project_name: this.cfg.projectName,
      name: `engram:${evt.operation}`,
      type: "llm",
      model: evt.model,
      start_time: startTime,
      end_time: endTime,
      input: state?.input != null ? { prompt: state.input } : void 0,
      output: evt.kind === "llm_end" && evt.output != null ? { completion: evt.output } : evt.kind === "llm_error" ? { error: evt.error } : void 0,
      metadata: {
        source: "engram",
        operation: evt.operation,
        durationMs: evt.durationMs ?? (state ? Date.now() - state.startedAt : void 0)
      },
      tags: ["engram", evt.operation],
      usage: Object.keys(usage).length > 0 ? usage : void 0
    };
    await postSpanBatch(this.cfg, [span], this.log);
  }
  // -------------------------------------------------------------------------
  // Helpers
  // -------------------------------------------------------------------------
  /**
   * Create a parent trace object in Opik if we haven't already.
   * Opik does NOT auto-create traces from spans, so without this
   * the spans are orphaned and don't show up in the trace list UI.
   */
  async ensureTrace(traceId, name, startTime, endTime) {
    if (this.createdTraces.has(traceId)) return;
    const existing = this.pendingTraces.get(traceId);
    if (existing) {
      await existing;
      if (this.createdTraces.has(traceId)) return;
      const retrying = this.pendingTraces.get(traceId);
      if (retrying) {
        await retrying;
        if (this.createdTraces.has(traceId)) return;
      }
    }
    const work = (async () => {
      const trace = {
        id: traceId,
        project_name: this.cfg.projectName,
        name,
        start_time: startTime,
        end_time: endTime,
        tags: ["engram"]
      };
      const ok = await postTraceBatch(this.cfg, [trace], this.log);
      if (!ok) {
        this.pendingTraces.delete(traceId);
        return;
      }
      this.createdTraces.add(traceId);
      this.pendingTraces.delete(traceId);
      if (this.createdTraces.size > 1e4) {
        const first = this.createdTraces.values().next().value;
        if (first) this.createdTraces.delete(first);
      }
    })();
    this.pendingTraces.set(traceId, work);
    await work;
  }
  /**
   * Convert a sessionKey to a stable, deterministic UUID v7-shaped ID so that
   * spans for the same session share a trace_id and are threaded in Opik.
   * Uses SHA-256 for collision resistance. The timestamp bytes (0-5) come from
   * the hash itself — they don't reflect real time, but Opik only validates
   * the version/variant bits, not timestamp ordering.
   */
  sessionToTraceId(sessionKey) {
    const digest = createHash("sha256").update(sessionKey).digest();
    digest[6] = digest[6] & 15 | 112;
    digest[8] = digest[8] & 63 | 128;
    const hex = digest.slice(0, 16).toString("hex");
    return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
  }
};
function createOpikExporter(raw, log) {
  if (raw.opikTraceEnabled === false) return null;
  const detected = readOpikOpenclawConfig(log);
  const apiUrl = raw.opikApiUrl ?? detected.apiUrl;
  if (!apiUrl) {
    if (raw.opikTraceEnabled === true) {
      log.warn(
        "[opik-exporter] opikTraceEnabled=true but no apiUrl found \u2014 set opikApiUrl in Engram config or ensure opik-openclaw plugin is configured"
      );
    }
    return null;
  }
  const cfg = {
    enabled: true,
    apiUrl,
    projectName: raw.opikProjectName ?? detected.projectName ?? "openclaw",
    workspaceName: raw.opikWorkspaceName ?? detected.workspaceName ?? "default",
    apiKey: raw.opikApiKey ?? detected.apiKey,
    traceRecallContent: raw.opikTraceRecallContent === true
  };
  return new OpikExporter(cfg, log);
}
export {
  OpikExporter,
  createOpikExporter
};
//# sourceMappingURL=opik-exporter.js.map