import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from "@nestjs/common";
import { Observable, throwError } from "rxjs";
import { tap, catchError } from "rxjs/operators";
import type { Request, Response } from "express";
import { TelemetryService } from "./telemetry.service";
import type { Span } from "@opentelemetry/api";
import { SpanKind } from "@opentelemetry/api";
import {
  ATTR_HTTP_REQUEST_METHOD,
  ATTR_HTTP_RESPONSE_STATUS_CODE,
  ATTR_URL_FULL,
  ATTR_URL_PATH,
} from "@opentelemetry/semantic-conventions";

/**
 * Interceptor that automatically creates OpenTelemetry spans for all HTTP requests.
 * Records HTTP method, URL, status code, and trace context in response headers.
 *
 * @example
 * ```typescript
 * // Apply globally in AppModule
 * @Module({
 *   providers: [
 *     {
 *       provide: APP_INTERCEPTOR,
 *       useClass: TelemetryInterceptor,
 *     },
 *   ],
 * })
 * export class AppModule {}
 * ```
 */
@Injectable()
export class TelemetryInterceptor implements NestInterceptor {
  private readonly logger = new Logger(TelemetryInterceptor.name);

  constructor(private readonly telemetryService: TelemetryService) {}

  /**
   * Intercept HTTP requests and wrap them in OpenTelemetry spans.
   *
   * @param context - The execution context
   * @param next - The next call handler
   * @returns Observable of the response with tracing applied
   */
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const httpContext = context.switchToHttp();
    const request = httpContext.getRequest<Request>();
    const response = httpContext.getResponse<Response>();

    const method = request.method;
    const path = request.path || request.url;
    const spanName = `${method} ${path}`;

    const span = this.telemetryService.startSpan(spanName, {
      kind: SpanKind.SERVER,
      attributes: {
        [ATTR_HTTP_REQUEST_METHOD]: method,
        [ATTR_URL_PATH]: path,
        [ATTR_URL_FULL]: request.url,
        "code.function": context.getHandler().name,
        "code.namespace": context.getClass().name,
      },
    });

    return next.handle().pipe(
      tap(() => {
        this.finalizeSpan(span, response);
      }),
      catchError((error: Error) => {
        this.telemetryService.recordException(span, error);
        this.finalizeSpan(span, response);
        return throwError(() => error);
      })
    );
  }

  /**
   * Finalize the span by setting status code and adding trace context to headers.
   *
   * @param span - The span to finalize
   * @param response - The HTTP response
   */
  private finalizeSpan(span: Span, response: Response): void {
    try {
      const statusCode = response.statusCode;
      span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, statusCode);

      // Add trace context to response headers for distributed tracing
      const spanContext = span.spanContext();
      if (spanContext.traceId && !response.headersSent && !response.writableEnded) {
        response.setHeader("x-trace-id", spanContext.traceId);
      }
    } catch (error) {
      this.logger.warn("Failed to finalize span", error);
    } finally {
      span.end();
    }
  }
}
