/**
 * `withTracer()` — adapt an `@opentelemetry/api` Tracer (or any
 * structurally-compatible tracer) into the vendor-neutral
 * `ObservabilityHook` that Shade's internals consume.
 *
 * No-op behavior:
 *   - When `tracer` is `undefined`.
 *   - When `process.env.SHADE_OTEL_ENABLED` is not set to a truthy value
 *     ("1"/"true") AND `opts.force` isn't passed.
 *   - When `opts.sample` is provided and the per-span dice roll fails.
 *
 * The OTel structural surface we depend on is intentionally tiny — only
 * `tracer.startSpan(name)` and the resulting span's
 * `setAttribute/setStatus/recordException/end` methods. This keeps us
 * compatible with any OTel-flavoured tracer (Jaeger, Honeycomb, Tempo,
 * Sentry's OTel adapter, etc.) without locking into one vendor.
 */

import {
  NOOP_HOOK,
  noopSpan,
  type Attributes,
  type AttrValue,
  type ObservabilityHook,
  type Span,
} from './types.js';

/** The structural subset of an OTel `Span` that we use. */
export interface OtelSpanLike {
  setAttribute(key: string, value: AttrValue): unknown;
  setAttributes?(attrs: Attributes): unknown;
  recordException?(err: unknown): unknown;
  setStatus?(status: { code: number; message?: string }): unknown;
  end(endTime?: number): unknown;
}

/** The structural subset of an OTel `Tracer` that we use. */
export interface OtelTracerLike {
  startSpan(name: string, options?: { attributes?: Attributes }): OtelSpanLike;
}

export interface WithTracerOptions {
  /**
   * Per-span sample rate in `[0, 1]`. Default `1` (sample everything when
   * the hook is active). Sampled-out spans return a no-op span.
   */
  sample?: number;
  /**
   * Bypass the `SHADE_OTEL_ENABLED` env-var gate. Useful for tests and
   * for embeddings where the hosting application makes the on/off
   * decision itself.
   */
  force?: boolean;
  /** Override the env-var name. Defaults to `SHADE_OTEL_ENABLED`. */
  envVar?: string;
  /** Override the random source (used in tests for determinism). */
  random?: () => number;
}

/**
 * Per the OTel spec: `SpanStatusCode.OK = 1`, `ERROR = 2`. We hardcode
 * the integers so we don't pull in `@opentelemetry/api` at runtime.
 */
const OTEL_STATUS_OK = 1;
const OTEL_STATUS_ERROR = 2;

export function withTracer(
  tracer: OtelTracerLike | null | undefined,
  opts: WithTracerOptions = {},
): ObservabilityHook {
  if (tracer === null || tracer === undefined) return NOOP_HOOK;

  const envVar = opts.envVar ?? 'SHADE_OTEL_ENABLED';
  if (!opts.force && !envEnabled(envVar)) return NOOP_HOOK;

  const sample = opts.sample ?? 1;
  if (sample <= 0) return NOOP_HOOK;
  const random = opts.random ?? Math.random;

  return {
    startSpan(name: string, attrs?: Attributes): Span {
      if (sample < 1 && random() >= sample) return noopSpan();
      const otelSpan = attrs !== undefined
        ? tracer.startSpan(name, { attributes: attrs })
        : tracer.startSpan(name);
      return adaptSpan(otelSpan);
    },
  };
}

function adaptSpan(otel: OtelSpanLike): Span {
  let ended = false;
  return {
    setAttribute(key, value) {
      if (ended) return;
      otel.setAttribute(key, value);
    },
    setAttributes(attrs) {
      if (ended) return;
      if (typeof otel.setAttributes === 'function') {
        otel.setAttributes(attrs);
      } else {
        for (const [k, v] of Object.entries(attrs)) otel.setAttribute(k, v);
      }
    },
    recordException(err) {
      if (ended) return;
      if (typeof otel.recordException === 'function') {
        otel.recordException(err);
      } else {
        otel.setAttribute(
          'exception.message',
          err instanceof Error ? err.message : String(err),
        );
      }
    },
    setStatus(status, message) {
      if (ended) return;
      if (typeof otel.setStatus === 'function') {
        const code = status === 'ok' ? OTEL_STATUS_OK : OTEL_STATUS_ERROR;
        otel.setStatus(message !== undefined ? { code, message } : { code });
      }
    },
    end() {
      if (ended) return;
      ended = true;
      otel.end();
    },
  };
}

function envEnabled(name: string): boolean {
  const proc =
    typeof globalThis !== 'undefined'
      ? (globalThis as unknown as { process?: { env?: Record<string, string | undefined> } }).process
      : undefined;
  const v = proc?.env?.[name];
  if (v === undefined || v === '') return false;
  return v === '1' || v.toLowerCase() === 'true';
}
