// Distributed Tracing Service
// OpenTelemetry integration for request correlation and distributed tracing

import { logger } from '@/infrastructure/monitoring/logger';

export interface TraceContext {
  traceId: string;
  spanId: string;
  parentSpanId?: string;
  baggage?: Record<string, string>;
}

export interface Span {
  name: string;
  startTime: number;
  endTime?: number;
  attributes: Record<string, any>;
  events: Array<{
    name: string;
    timestamp: number;
    attributes?: Record<string, any>;
  }>;
  status: 'ok' | 'error';
  error?: Error;
  parentSpanId?: string;
}

/**
 * Tracing Service
 * Provides distributed tracing capabilities using OpenTelemetry patterns
 */
export class TracingService {
  private spans: Map<string, Span> = new Map();
  private activeSpans: Map<string, string> = new Map(); // traceId -> spanId

  /**
   * Start a new span
   */
  startSpan(
    name: string,
    traceId?: string,
    parentSpanId?: string,
    attributes?: Record<string, any>
  ): TraceContext {
    const generatedTraceId = traceId || this.generateTraceId();
    const spanId = this.generateSpanId();

    const span: Span = {
      name,
      startTime: Date.now(),
      attributes: {
        ...attributes,
        'span.name': name,
        'span.kind': 'internal',
      },
      events: [],
      status: 'ok',
    };

    const spanKey = `${generatedTraceId}:${spanId}`;
    this.spans.set(spanKey, span);
    this.activeSpans.set(generatedTraceId, spanId);

    logger.debug('Span started', {
      traceId: generatedTraceId,
      spanId,
      name,
      parentSpanId,
    });

    return {
      traceId: generatedTraceId,
      spanId,
      parentSpanId,
    };
  }

  /**
   * End a span
   */
  endSpan(traceId: string, spanId: string, status: 'ok' | 'error' = 'ok', error?: Error): void {
    const spanKey = `${traceId}:${spanId}`;
    const span = this.spans.get(spanKey);

    if (!span) {
      logger.warn('Span not found', { traceId, spanId });
      return;
    }

    span.endTime = Date.now();
    span.status = status;
    if (error) {
      span.error = error;
      span.attributes['error'] = true;
      span.attributes['error.message'] = error.message;
      span.attributes['error.stack'] = error.stack;
    }

    // Calculate duration
    const duration = span.endTime - span.startTime;
    span.attributes['duration.ms'] = duration;

    // Export span (in production, send to OpenTelemetry collector)
    this.exportSpan(traceId, spanId, span);

    this.activeSpans.delete(traceId);
  }

  /**
   * Add event to span
   */
  addEvent(traceId: string, spanId: string, name: string, attributes?: Record<string, any>): void {
    const spanKey = `${traceId}:${spanId}`;
    const span = this.spans.get(spanKey);

    if (!span) {
      logger.warn('Span not found for event', { traceId, spanId });
      return;
    }

    span.events.push({
      name,
      timestamp: Date.now(),
      attributes,
    });
  }

  /**
   * Add attribute to span
   */
  setAttribute(traceId: string, spanId: string, key: string, value: any): void {
    const spanKey = `${traceId}:${spanId}`;
    const span = this.spans.get(spanKey);

    if (!span) {
      logger.warn('Span not found for attribute', { traceId, spanId });
      return;
    }

    span.attributes[key] = value;
  }

  /**
   * Get trace context from headers
   */
  extractTraceContext(headers: Record<string, string | string[] | undefined>): TraceContext | null {
    const traceId = this.getHeader(headers, 'x-trace-id') || this.getHeader(headers, 'traceparent');
    const spanId = this.getHeader(headers, 'x-span-id');
    const parentSpanId = this.getHeader(headers, 'x-parent-span-id');

    if (!traceId) {
      return null;
    }

    // Parse W3C Trace Context format if present
    if (traceId.startsWith('00-')) {
      const parts = traceId.split('-');
      return {
        traceId: parts[1] || traceId,
        spanId: parts[2] || spanId || this.generateSpanId(),
        parentSpanId: parts[3] || parentSpanId || undefined,
      };
    }

    return {
      traceId,
      spanId: spanId || this.generateSpanId(),
      parentSpanId: parentSpanId || undefined,
    };
  }

  /**
   * Inject trace context into headers
   */
  injectTraceContext(context: TraceContext): Record<string, string> {
    return {
      'x-trace-id': context.traceId,
      'x-span-id': context.spanId,
      'x-parent-span-id': context.parentSpanId || '',
      // W3C Trace Context format
      traceparent: `00-${context.traceId}-${context.spanId}-01`,
    };
  }

  /**
   * Export span (in production, send to OpenTelemetry collector)
   */
  private async exportSpan(traceId: string, spanId: string, span: Span): Promise<void> {
    // In production, this would send to OpenTelemetry collector
    // For now, log structured trace data
    logger.info('Span exported', {
      traceId,
      spanId,
      name: span.name,
      duration: span.endTime! - span.startTime,
      status: span.status,
      attributes: span.attributes,
      events: span.events.length,
    });

    // Send to OpenTelemetry collector if configured
    const collectorUrl = process.env.OTEL_COLLECTOR_URL;
    if (collectorUrl) {
      try {
        const otelSpan = this.formatSpanForOTel(traceId, spanId, span);
        await fetch(`${collectorUrl}/v1/traces`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            resourceSpans: [
              {
                resource: {
                  attributes: [
                    { key: 'service.name', value: { stringValue: 'dbis-iru' } },
                    { key: 'service.version', value: { stringValue: process.env.APP_VERSION || '1.0.0' } },
                  ],
                },
                scopeSpans: [
                  {
                    spans: [otelSpan],
                  },
                ],
              },
            ],
          }),
        }).catch((error) => {
          logger.warn('Failed to send span to OpenTelemetry collector', {
            error: error instanceof Error ? error.message : 'Unknown error',
          });
        });
      } catch (error) {
        logger.warn('OpenTelemetry export failed', {
          error: error instanceof Error ? error.message : 'Unknown error',
        });
      }
    }
  }

  /**
   * Format span for OpenTelemetry
   */
  private formatSpanForOTel(traceId: string, spanId: string, span: Span): Record<string, unknown> {
    return {
      traceId: this.hexToBytes(traceId),
      spanId: this.hexToBytes(spanId),
      parentSpanId: span.parentSpanId ? this.hexToBytes(span.parentSpanId) : undefined,
      name: span.name,
      kind: 1, // INTERNAL
      startTimeUnixNano: span.startTime * 1000000,
      endTimeUnixNano: span.endTime ? span.endTime * 1000000 : undefined,
      attributes: Object.entries(span.attributes).map(([key, value]) => ({
        key,
        value: { stringValue: String(value) },
      })),
      events: span.events.map((event) => ({
        timeUnixNano: event.timestamp * 1000000,
        name: event.name,
        attributes: event.attributes
          ? Object.entries(event.attributes).map(([key, value]) => ({
              key,
              value: { stringValue: String(value) },
            }))
          : [],
      })),
      status: {
        code: span.status === 'ok' ? 1 : 2, // OK = 1, ERROR = 2
        message: span.error?.message,
      },
    };
  }

  /**
   * Convert hex string to bytes
   */
  private hexToBytes(hex: string): string {
    // Convert hex string to base64 for OTel
    return Buffer.from(hex, 'hex').toString('base64');
  }

  /**
   * Generate trace ID (W3C format)
   */
  private generateTraceId(): string {
    // Generate 32-character hex string (16 bytes)
    return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
  }

  /**
   * Generate span ID (W3C format)
   */
  private generateSpanId(): string {
    // Generate 16-character hex string (8 bytes)
    return Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
  }

  /**
   * Get header value
   */
  private getHeader(headers: Record<string, string | string[] | undefined>, name: string): string | null {
    const value = headers[name.toLowerCase()];
    if (!value) {
      return null;
    }
    return Array.isArray(value) ? value[0] : value;
  }
}

export const tracingService = new TracingService();
