import { Processor, WorkerHost, OnWorkerEvent, InjectQueue } from '@nestjs/bullmq';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import type { Job, Queue } from 'bullmq';
import {
  EVENTS_QUEUE,
  INGEST_EVENT_JOB,
  PROCESS_EVENT_JOB,
  type RawEventEnvelope,
  type ProcessEventJob,
} from '@lilith/analytics';

import { AggregationService } from './aggregation.service';
import { RawEvent } from '../entities/raw-event.entity';
import { IngestService } from '../ingest/ingest.service';

/**
 * Single worker on the shared `analytics-events` queue. It handles two job
 * types so they share one redis connection and consumer pool:
 *
 *   ingest-event  → enrich + write the canonical raw_events row (IngestService),
 *                   then emit a process-event job for the row it inserted.
 *   process-event → aggregate an already-written row into aggregated_metrics.
 *
 * Splitting these into two @Processor classes on the same queue would NOT work:
 * BullMQ workers pull any job regardless of name, so a single switch is the
 * correct pattern.
 */
@Injectable()
@Processor(EVENTS_QUEUE, {
  concurrency: 10,
})
export class EventsProcessor extends WorkerHost {
  private readonly logger = new Logger(EventsProcessor.name);

  constructor(
    private readonly aggregationService: AggregationService,
    private readonly ingestService: IngestService,
    @InjectRepository(RawEvent)
    private readonly rawEventRepository: Repository<RawEvent>,
    @InjectQueue(EVENTS_QUEUE)
    private readonly eventsQueue: Queue,
  ) {
    super();
  }

  async process(job: Job): Promise<void> {
    switch (job.name) {
      case INGEST_EVENT_JOB:
        return this.handleIngest(job as Job<RawEventEnvelope>);
      case PROCESS_EVENT_JOB:
        return this.handleAggregate(job as Job<ProcessEventJob>);
      default:
        this.logger.warn(`Unknown job name '${job.name}' on ${EVENTS_QUEUE} — skipping`);
        return;
    }
  }

  /** Write the canonical row, then hand off to aggregation (only if newly inserted). */
  private async handleIngest(job: Job<RawEventEnvelope>): Promise<void> {
    const written = await this.ingestService.write(job.data);
    if (!written) return; // duplicate drain — row already present, already aggregated

    await this.eventsQueue.add(
      PROCESS_EVENT_JOB,
      {
        eventId: written.eventId,
        eventType: written.eventType,
        sessionId: written.sessionId,
      } satisfies ProcessEventJob,
      // Reuse the event id so a re-drain can't double-enqueue aggregation.
      { jobId: `agg:${written.eventId}` },
    );
  }

  /** Aggregate one already-written raw event. */
  private async handleAggregate(job: Job<ProcessEventJob>): Promise<void> {
    const { eventId, eventType, sessionId } = job.data;

    this.logger.debug(`Aggregating event: ${eventType} (id: ${eventId}) from session ${sessionId}`);

    const event = await this.rawEventRepository.findOne({ where: { id: eventId } });

    if (!event) {
      this.logger.warn(`Event ${eventId} not found in raw_events — skipping`);
      return;
    }

    if (event.processed) {
      this.logger.debug(`Event ${eventId} already processed — skipping`);
      return;
    }

    await this.aggregationService.processEvent({
      eventType: event.eventType,
      timestamp: event.timestamp,
      sessionId: event.sessionId,
      userId: event.userId,
      properties: {
        ...(event.metadata ?? {}),
        path: event.pageUrl ?? undefined,
        deviceType: event.deviceType ?? undefined,
      },
    });

    await this.rawEventRepository.update(eventId, {
      processed: true,
      processedAt: new Date(),
    });
  }

  @OnWorkerEvent('completed')
  onCompleted(job: Job) {
    this.logger.debug(`Job ${job.id} (${job.name}) completed`);
  }

  @OnWorkerEvent('failed')
  onFailed(job: Job, error: Error) {
    this.logger.error(`Job ${job.id} (${job.name}) failed: ${error.message}`);
  }
}
