import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';

import { AnalyticsClient } from '../analytics-client';

import { TRACK_ANALYTICS_METADATA } from './constants';

import type { TrackAnalyticsOptions, TrackingContext } from './types';
import type { ViewEventData, EngagementEventData } from '../types';

/**
 * Interceptor that automatically tracks analytics events for decorated methods.
 *
 * This interceptor reads the @TrackAnalytics() metadata and sends appropriate
 * analytics events (view or engagement) to the analytics service.
 *
 * @example
 * ```typescript
 * // Apply globally in module
 * @Module({
 *   providers: [
 *     {
 *       provide: APP_INTERCEPTOR,
 *       useClass: AnalyticsInterceptor,
 *     },
 *   ],
 * })
 * export class AppModule {}
 *
 * // Or apply to specific controller
 * @UseInterceptors(AnalyticsInterceptor)
 * @Controller('products')
 * export class ProductsController {}
 * ```
 */
@Injectable()
export class AnalyticsInterceptor implements NestInterceptor {
  constructor(
    private readonly analyticsClient: AnalyticsClient,
    private readonly reflector: Reflector,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const trackingOptions = this.reflector.get<TrackAnalyticsOptions>(
      TRACK_ANALYTICS_METADATA,
      context.getHandler(),
    );

    if (!trackingOptions) {
      return next.handle();
    }

    const trackingContext = this.buildTrackingContext(context);

    return next.handle().pipe(
      tap((result: unknown) => {
        this.trackEvent(trackingOptions, { ...trackingContext, result });
      }),
      catchError((error: unknown) => {
        // Still track the event even on error (optional: add error metadata)
        this.trackEvent(trackingOptions, {
          ...trackingContext,
          error: error instanceof Error ? error : undefined
        });
        throw error;
      }),
    );
  }

  /**
   * Build tracking context from execution context
   */
  private buildTrackingContext(context: ExecutionContext): TrackingContext {
    const handler = context.getHandler();
    const className = context.getClass().name;
    const methodName = handler.name;
    const args = context.getArgs();

    const trackingContext: TrackingContext = {
      method: methodName,
      className,
      args,
    };

    // Try to extract HTTP context if available
    try {
      const httpContext = context.switchToHttp();
      const request = httpContext.getRequest();

      if (request) {
        trackingContext.request = {
          headers: request.headers || {},
          ip: request.ip || request.connection?.remoteAddress,
          method: request.method,
          url: request.url,
        };

        // Extract user from request if available
        if (request.user) {
          trackingContext.user = request.user;
        }
      }
    } catch {
      // Not an HTTP context, skip request extraction
    }

    return trackingContext;
  }

  /**
   * Track the analytics event based on options
   */
  private trackEvent(
    options: TrackAnalyticsOptions,
    context: TrackingContext,
  ): void {
    try {
      if (options.eventType === 'view') {
        this.trackViewEvent(options, context);
      } else if (options.eventType === 'engagement') {
        this.trackEngagementEvent(options, context);
      }
    } catch (error) {
      // Fail silently to not disrupt the main application flow
      console.error('Failed to track analytics event:', error);
    }
  }

  /**
   * Track a view event
   */
  private trackViewEvent(
    options: TrackAnalyticsOptions,
    context: TrackingContext,
  ): void {
    if (!options.contentType) {
      throw new Error('contentType is required for view events');
    }

    const contentId = this.extractId(options, context);
    const userId = this.extractUserId(options, context);

    const viewData: Omit<ViewEventData, 'app' | 'sessionId'> = {
      contentId,
      contentType: options.contentType,
      userId,
      referrer: context.request?.headers?.referer as string | undefined,
      ipAddress: context.request?.ip,
    };

    this.analyticsClient.trackView(viewData);
  }

  /**
   * Track an engagement event
   */
  private trackEngagementEvent(
    options: TrackAnalyticsOptions,
    context: TrackingContext,
  ): void {
    if (!options.metricType || !options.targetType) {
      throw new Error('metricType and targetType are required for engagement events');
    }

    const targetId = this.extractId(options, context);
    const userId = this.extractUserId(options, context);

    if (!userId) {
      // Engagement events require a user ID
      console.warn('Skipping engagement event: userId is required');
      return;
    }

    const engagementData: EngagementEventData = {
      userId,
      metricType: options.metricType,
      targetId,
      targetType: options.targetType,
      metadata: options.metadata,
    };

    this.analyticsClient.trackEngagement(engagementData);
  }

  /**
   * Extract content/target ID from context using the configured extractor
   */
  private extractId(
    options: TrackAnalyticsOptions,
    context: TrackingContext,
  ): string {
    if (options.idExtractor) {
      return options.idExtractor(...context.args);
    }

    // Default: use first argument as ID
    const firstArg = context.args[0];
    if (typeof firstArg === 'string') {
      return firstArg;
    }

    if (typeof firstArg === 'object' && firstArg !== null && 'id' in firstArg) {
      return (firstArg as { id: string }).id;
    }

    throw new Error('Could not extract ID from arguments. Provide an idExtractor.');
  }

  /**
   * Extract user ID from context using the configured extractor
   */
  private extractUserId(
    options: TrackAnalyticsOptions,
    context: TrackingContext,
  ): string | undefined {
    if (options.userIdExtractor) {
      return options.userIdExtractor(context);
    }

    // Default: extract from user object in context
    return context.user?.id;
  }
}
