import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { InjectQueue } from '@nestjs/bullmq';
import { Repository } from 'typeorm';
import { Queue } from 'bullmq';
import type { Request } from 'express';

import { RawEvent } from '../entities/raw-event.entity';
import { SessionFingerprint } from '../entities/session-fingerprint.entity';
import { DeviceEnrichmentService, ClientDeviceData, EnrichedDeviceData } from './device-enrichment.service';
import { AttributionService, AttributionInput, ResolvedAttribution } from './attribution.service';

export interface TrackViewInput {
  pageUrl: string;
  referrer?:  string;
  userId?:  string;
  sessionId: string;
  metadata?:  Record<string, unknown>;
  clientDevice?:  ClientDeviceData;
  attribution?:  AttributionInput;
}

export interface TrackEventInput {
  eventType: string;
  sessionId: string;
  userId?:  string;
  pageUrl?:  string;
  metadata?:  Record<string, unknown>;
  timestamp?:  number;
}

export interface TrackConversionInput {
  conversionType: string;
  sessionId: string;
  userId?:  string;
  value?:  number;
  currency?:  string;
  metadata?:  Record<string, unknown>;
}

export interface TrackingResult {
  success: boolean;
  eventId: string | null;
  error?:  string;
}

export interface BatchTrackingResult {
  success: boolean;
  count: number;
  errors: string[];
}

@Injectable()
export class TrackingService {
  private readonly logger = new Logger(TrackingService.name);

  constructor(
    private readonly deviceEnrichmentService: DeviceEnrichmentService,
    private readonly attributionService: AttributionService,
    @InjectRepository(RawEvent)
    private readonly rawEventRepository: Repository<RawEvent>,
    @InjectRepository(SessionFingerprint)
    private readonly fingerprintRepository: Repository<SessionFingerprint>,
    @InjectQueue('analytics-events')
    private readonly eventsQueue: Queue,
  ) {}

  /**
   * Track page view with device fingerprinting
   */
  async trackView(data: TrackViewInput, request: Request): Promise<TrackingResult> {
    try {
      // Extract User-Agent and IP from request headers
      const userAgent = request.headers['user-agent'] as string | undefined;
      const ipAddress = this.extractIpAddress(request);

      // Enrich device data (IP is hashed, never stored raw)
      const enriched = await this.deviceEnrichmentService.enrich(
        userAgent,
        ipAddress,
        data.clientDevice,
      );

      // Resolve attribution
      const attribution = this.attributionService.resolveTrafficSource({
        utmSource: data.attribution?.utmSource,
        utmMedium: data.attribution?.utmMedium,
        utmCampaign: data.attribution?.utmCampaign,
        utmContent: data.attribution?.utmContent,
        utmTerm: data.attribution?.utmTerm,
        referrer: data.referrer,
      });

      // Create or update session fingerprint (first-touch attribution)
      await this.upsertSessionFingerprint(
        data.sessionId,
        data.userId,
        enriched,
        attribution,
        data.pageUrl,
      );

      // Create raw event
      const event = this.rawEventRepository.create({
        eventType: 'pageview',
        sessionId: data.sessionId,
        userId: data.userId ?? null,
        pageUrl: data.pageUrl,
        referrer: data.referrer ?? null,
        deviceType: enriched.deviceType,
        metadata: data.metadata ?? null,
        timestamp: new Date(),
        processed: false,
      });

      const saved = await this.rawEventRepository.save(event);

      // Queue for async processing
      await this.eventsQueue.add('process-event', {
        eventId: saved.id,
        eventType: 'pageview',
        sessionId: data.sessionId,
      });

      return { success: true, eventId: saved.id };
    } catch (error) {
      this.logger.error('Failed to track view event', error);
      return {
        success: false,
        eventId: null,
        error: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  }

  /**
   * Track generic event
   */
  async trackEvent(data: TrackEventInput): Promise<TrackingResult> {
    try {
      const event = this.rawEventRepository.create({
        eventType: data.eventType,
        sessionId: data.sessionId,
        userId: data.userId ?? null,
        pageUrl: data.pageUrl ?? null,
        metadata: data.metadata ?? null,
        timestamp: data.timestamp ? new Date(data.timestamp) : new Date(),
        processed: false,
      });

      const saved = await this.rawEventRepository.save(event);

      // Queue for async processing
      await this.eventsQueue.add('process-event', {
        eventId: saved.id,
        eventType: data.eventType,
        sessionId: data.sessionId,
      });

      return { success: true, eventId: saved.id };
    } catch (error) {
      this.logger.error('Failed to track event', error);
      return {
        success: false,
        eventId: null,
        error: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  }

  /**
   * Track batch of events
   */
  async trackBatch(events: TrackEventInput[]): Promise<BatchTrackingResult> {
    const results = await Promise.allSettled(
      events.map((event) => this.trackEvent(event)),
    );

    const errors = results
      .filter((r): r is PromiseRejectedResult => r.status === 'rejected')
      .map((r) => (r.reason as Error)?.message ?? 'Unknown error');

    const successCount = results.filter((r) => r.status === 'fulfilled').length;

    return {
      success: errors.length === 0,
      count: successCount,
      errors,
    };
  }

  /**
   * Track conversion event
   */
  async trackConversion(data: TrackConversionInput): Promise<TrackingResult> {
    try {
      const event = this.rawEventRepository.create({
        eventType: 'conversion',
        sessionId: data.sessionId,
        userId: data.userId ?? null,
        metadata: {
          conversionType: data.conversionType,
          value: data.value,
          currency: data.currency,
          ...data.metadata,
        },
        timestamp: new Date(),
        processed: false,
      });

      const saved = await this.rawEventRepository.save(event);

      // Queue for async processing (high priority for conversions)
      await this.eventsQueue.add(
        'process-event',
        {
          eventId: saved.id,
          eventType: 'conversion',
          sessionId: data.sessionId,
          conversionType: data.conversionType,
        },
        { priority: 1 }, // Higher priority
      );

      return { success: true, eventId: saved.id };
    } catch (error) {
      this.logger.error('Failed to track conversion', error);
      return {
        success: false,
        eventId: null,
        error: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  }

  /**
   * Create or update session fingerprint
   * Attribution is first-touch: only set on creation
   */
  private async upsertSessionFingerprint(
    sessionId: string,
    userId: string | undefined,
    enriched: EnrichedDeviceData,
    attribution: ResolvedAttribution,
    landingPage: string,
  ): Promise<void> {
    try {
      const existing = await this.fingerprintRepository.findOne({
        where: { sessionId },
      });

      if (existing) {
        // Update viewport (may change on resize) and userId (if logged in later)
        if (enriched.viewportWidth !== undefined) {
          existing.viewportWidth = enriched.viewportWidth;
        }
        if (enriched.viewportHeight !== undefined) {
          existing.viewportHeight = enriched.viewportHeight;
        }
        if (userId && !existing.userId) {
          existing.userId = userId;
        }
        await this.fingerprintRepository.save(existing);
      } else {
        // Create new fingerprint with first-touch attribution
        const fingerprint = this.fingerprintRepository.create({
          sessionId,
          userId: userId ?? null,
          deviceType: enriched.deviceType,
          isBot: enriched.isBot,
          browser: enriched.browser,
          browserVersion: enriched.browserVersion,
          browserMajor: enriched.browserMajor,
          os: enriched.os,
          osVersion: enriched.osVersion,
          deviceVendor: enriched.deviceVendor,
          deviceModel: enriched.deviceModel,
          screenWidth: enriched.screenWidth ?? null,
          screenHeight: enriched.screenHeight ?? null,
          viewportWidth: enriched.viewportWidth ?? null,
          viewportHeight: enriched.viewportHeight ?? null,
          pixelRatio: enriched.pixelRatio ?? null,
          colorDepth: enriched.colorDepth ?? null,
          language: enriched.language ?? null,
          languages: enriched.languages ?? null,
          timezone: enriched.timezone ?? null,
          timezoneOffset: enriched.timezoneOffset ?? null,
          country: enriched.country,
          region: enriched.region,
          city: enriched.city,
          isEU: enriched.isEU,
          geoTimezone: enriched.geoTimezone,
          isVpn: enriched.isVpn,
          isDatacenter: enriched.isDatacenter,
          isTor: enriched.isTor,
          ipHash: enriched.ipHash,
          deviceMemory: enriched.deviceMemory ?? null,
          hardwareConcurrency: enriched.hardwareConcurrency ?? null,
          touchPoints: enriched.touchPoints ?? null,
          cookiesEnabled: enriched.cookiesEnabled ?? null,
          doNotTrack: enriched.doNotTrack ?? null,
          // First-touch attribution
          trafficSource: attribution.source,
          utmSource: attribution.rawSource,
          utmMedium: attribution.medium,
          utmCampaign: attribution.campaign,
          utmContent: attribution.content,
          utmTerm: attribution.term,
          referrer: attribution.referrer,
          landingPage,
        });
        await this.fingerprintRepository.save(fingerprint);
      }
    } catch (error) {
      // Log but don't fail - fingerprinting is enhancement, not critical
      this.logger.warn('Failed to upsert session fingerprint', error);
    }
  }

  /**
   * Extract client IP from request headers
   */
  private extractIpAddress(request: Request): string | undefined {
    const forwardedFor = request.headers['x-forwarded-for'];
    const realIp = request.headers['x-real-ip'];

    if (forwardedFor) {
      const firstIp = (forwardedFor as string).split(',')[0];
      return firstIp?.trim();
    }
    if (realIp) {
      return realIp as string;
    }
    return request.socket.remoteAddress;
  }
}
