// src/telemetry/tracing.ts
// Minimal tracing infrastructure - works with or without OpenTelemetry

/**
 * Trace context for propagation across transport boundaries.
 */
export interface TraceContext {
  traceId: string;
  spanId: string;
  parentSpanId?: string;
}

/**
 * Span for tracking operations.
 */
export interface ISpan {
  traceId: string;
  spanId: string;
  name: string;
  startTime: number;
  endTime?: number;
  attributes: Record<string, unknown>;
  status: 'ok' | 'error';
  parentSpanId?: string;
  end(): void;
  setAttribute(key: string, value: unknown): void;
  setStatus(status: 'ok' | 'error', message?: string): void;
}

// Global state
let _enabled = false;
let _activeSpan: ISpan | null = null;
const _spans: ISpan[] = [];
const _listeners: Array<(span: ISpan) => void> = [];

/**
 * Generate a random ID (16 hex chars for spanId, 32 for traceId).
 */
function generateId(length: 16 | 32 = 16): string {
  const chars = '0123456789abcdef';
  let result = '';
  for (let i = 0; i < length; i++) {
    result += chars[Math.floor(Math.random() * 16)];
  }
  return result;
}

/**
 * Create a new span.
 */
function createSpan(name: string, parentContext?: TraceContext): ISpan {
  const span: ISpan = {
    traceId: parentContext?.traceId ?? generateId(32),
    spanId: generateId(16),
    parentSpanId: parentContext?.spanId,
    name,
    startTime: performance.now(),
    attributes: {},
    status: 'ok',
    end() {
      this.endTime = performance.now();
      _spans.push(this);
      _listeners.forEach(fn => fn(this));
      if (_activeSpan === this) {
        _activeSpan = null;
      }
    },
    setAttribute(key: string, value: unknown) {
      this.attributes[key] = value;
    },
    setStatus(status: 'ok' | 'error', message?: string) {
      this.status = status;
      if (message) this.attributes['error.message'] = message;
    },
  };
  return span;
}

// ============================================================================
// Public API
// ============================================================================

/**
 * Enable or disable tracing.
 */
export function setTracingEnabled(enabled: boolean): void {
  _enabled = enabled;
  if (enabled) {
    // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
    console.log('[Tracing] Enabled');
  }
}

/**
 * Check if tracing is enabled.
 */
export function isTracingEnabled(): boolean {
  return _enabled;
}

/**
 * Start a new span. If tracing is disabled, returns a no-op span.
 */
export function startSpan(name: string, parentContext?: TraceContext): ISpan {
  if (!_enabled) {
    // Return no-op span
    return {
      traceId: '',
      spanId: '',
      name,
      startTime: 0,
      attributes: {},
      status: 'ok',
      end() {},
      setAttribute() {},
      setStatus() {},
    };
  }

  // Auto-inherit from active span when no explicit parent given.
  // This ensures nested RPC calls (e.g., tool → RequestReply.execute())
  // share the parent's traceId instead of generating orphan traces.
  const effectiveParent = parentContext
    ?? (_activeSpan ? { traceId: _activeSpan.traceId, spanId: _activeSpan.spanId } : undefined);

  const span = createSpan(name, effectiveParent);
  _activeSpan = span;
  return span;
}

/**
 * Get the current active span's trace context.
 */
export function getActiveTraceContext(): TraceContext | undefined {
  if (!_enabled || !_activeSpan) return undefined;
  return {
    traceId: _activeSpan.traceId,
    spanId: _activeSpan.spanId,
  };
}

/**
 * Extract trace context from a message payload.
 */
export function extractTraceContext(payload: unknown): TraceContext | undefined {
  if (!payload || typeof payload !== 'object') return undefined;
  const p = payload as Record<string, unknown>;
  if (p.traceContext && typeof p.traceContext === 'object') {
    const tc = p.traceContext as Record<string, unknown>;
    if (typeof tc.traceId === 'string' && typeof tc.spanId === 'string') {
      return {
        traceId: tc.traceId,
        spanId: tc.spanId,
      };
    }
  }
  return undefined;
}

/**
 * Inject trace context into a message payload.
 */
export function injectTraceContext<T>(payload: T): T {
  if (!_enabled) return payload;
  const ctx = getActiveTraceContext();
  if (!ctx) return payload;

  if (payload && typeof payload === 'object') {
    return { ...payload, traceContext: ctx };
  }
  return payload;
}

/**
 * Run a function with a span, automatically ending it.
 */
export async function withSpan<T>(
  name: string,
  fn: (span: ISpan) => Promise<T> | T,
  parentContext?: TraceContext
): Promise<T> {
  const span = startSpan(name, parentContext);
  try {
    const result = await fn(span);
    span.setStatus('ok');
    return result;
  } catch (err) {
    span.setStatus('error', err instanceof Error ? err.message : String(err));
    throw err;
  } finally {
    span.end();
  }
}

/**
 * Add a listener for completed spans.
 */
export function onSpanEnd(listener: (span: ISpan) => void): () => void {
  _listeners.push(listener);
  return () => {
    const idx = _listeners.indexOf(listener);
    if (idx >= 0) _listeners.splice(idx, 1);
  };
}

/**
 * Get all recorded spans (for debugging).
 */
export function getSpans(): ISpan[] {
  return [..._spans];
}

/**
 * Clear recorded spans.
 */
export function clearSpans(): void {
  _spans.length = 0;
}

/**
 * Print a summary of spans to console.
 */
export function printTraceSummary(): void {
  if (_spans.length === 0) {
    // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
    console.log('[Tracing] No spans recorded');
    return;
  }

  // Group by traceId
  const traces = new Map<string, ISpan[]>();
  for (const span of _spans) {
    const existing = traces.get(span.traceId) ?? [];
    existing.push(span);
    traces.set(span.traceId, existing);
  }

  // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
  console.log(`[Tracing] ${traces.size} traces, ${_spans.length} spans total`);
  for (const [traceId, spans] of traces) {
    // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
    console.log(`\n  Trace ${traceId.slice(0, 8)}...`);
    const sorted = spans.sort((a, b) => a.startTime - b.startTime);
    for (const span of sorted) {
      const duration = span.endTime ? (span.endTime - span.startTime).toFixed(2) : '?';
      const indent = span.parentSpanId ? '    ' : '  ';
      const status = span.status === 'error' ? '❌' : '✓';
      // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
      console.log(`${indent}${status} ${span.name}: ${duration}ms`);
    }
  }
}

// ============================================================================
// Console exporter (logs spans as they complete)
// ============================================================================

let _consoleExporterActive = false;

export function enableConsoleExporter(): () => void {
  if (_consoleExporterActive) return () => {};
  _consoleExporterActive = true;

  return onSpanEnd((span) => {
    const duration = span.endTime ? (span.endTime - span.startTime).toFixed(2) : '?';
    const status = span.status === 'error' ? 'ERROR' : 'OK';
    const parent = span.parentSpanId ? ` parent=${span.parentSpanId.slice(0, 8)}` : '';
    // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
    console.log(
      `[SPAN] ${span.name} trace=${span.traceId.slice(0, 8)} span=${span.spanId.slice(0, 8)}${parent} ${duration}ms [${status}]`
    );
  });
}
