import { Injectable } from '@nestjs/common';
import { AppLoggerService } from 'src/common/services/logger.service';
import { handleKnownErrors } from 'src/common/utils/handle-error.util';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import InifniBadRequestException from 'src/common/exceptions/infini-badrequest-exception';
import { ZoomAnalyticsResourceType } from 'src/common/enum/zoom-analytics-resource-type.enum';
import {
  ZOOM_ANALYTICS_WEBHOOK_HEADER,
  ZOOM_ANALYTICS_LOG,
  ZOOM_ANALYTICS_WEBHOOK_LOG_MODULE,
} from '../constants/zoom-analytics.constants';
import { ZoomAnalyticsWebhookEvent } from '../enums/zoom-analytics-webhook-event.enum';
import {
  ZoomAnalyticsWebhookSignatureUtil,
  ZoomAnalyticsUrlValidationResponse,
} from '../utils/zoom-analytics-webhook-signature.util';
import { ZoomAnalyticsConfigService } from './zoom-analytics-config.service';
import { ZoomAnalyticsProviderRegistry } from '../registries/zoom-analytics-provider.registry';
import { ZoomWebinarRepository } from '../repositories/zoom-webinar.repository';

const MEETING_EVENT_PREFIX = 'meeting.';

interface ZoomAnalyticsWebhookObject {
  id?: string | number;
  /** Disambiguates which sibling session a shared recurring webinar's event belongs to — present for recurring webinars. */
  occurrence_id?: string;
  start_time?: string;
  end_time?: string;
  participant?: {
    id?: string;
    email?: string;
    join_time?: string;
    leave_time?: string;
    /** The display name Zoom reported for this participant at join/leave time. */
    user_name?: string;
    /** Why the participant left ("left the meeting", "removed by the host", network errors, ...) — only on participant_left. */
    leave_reason?: string;
  };
}

interface ZoomAnalyticsWebhookBody {
  event?: string;
  payload?: {
    plainToken?: string;
    object?: ZoomAnalyticsWebhookObject;
  };
}

/**
 * Handles the new analytics webhook: URL-validation handshake, HMAC
 * signature + replay verification, and dispatch to live tracking
 * (participant_joined/left) or session lifecycle (started/ended). Handles
 * both Webinar and Meeting events — the event name's own `webinar.`/
 * `meeting.` prefix picks the provider (see resourceTypeForEvent), never a
 * global setting, so both resource types work correctly in the same
 * deployment. Entirely new code; does not touch any existing Zoom webhook
 * plumbing (there is none).
 */
@Injectable()
export class ZoomAnalyticsWebhookService {
  constructor(
    private readonly config: ZoomAnalyticsConfigService,
    private readonly registry: ZoomAnalyticsProviderRegistry,
    private readonly webinarRepository: ZoomWebinarRepository,
    private readonly logger: AppLoggerService,
  ) {}

  async process(
    body: ZoomAnalyticsWebhookBody,
    headers: Record<string, unknown>,
  ): Promise<ZoomAnalyticsUrlValidationResponse | void> {
    this.logger.setContext({ module: ZOOM_ANALYTICS_WEBHOOK_LOG_MODULE });

    if (body?.event === ZoomAnalyticsWebhookEvent.ENDPOINT_URL_VALIDATION) {
      // Always answered, even while disabled — Zoom's one-time endpoint-setup handshake has no side
      // effects and must keep working so the URL stays validated for whenever the feature is turned on.
      return ZoomAnalyticsWebhookSignatureUtil.buildUrlValidationResponse(
        body?.payload?.plainToken ?? '',
        this.config.getWebhookSecret(),
      );
    }

    if (!this.config.isEnabled()) {
      this.logger.log(ZOOM_ANALYTICS_LOG.WEBHOOK_IGNORED_DISABLED, { event: body?.event });
      return;
    }

    const timestamp = this.headerValue(headers, ZOOM_ANALYTICS_WEBHOOK_HEADER.TIMESTAMP);
    if (!ZoomAnalyticsWebhookSignatureUtil.isTimestampFresh(timestamp)) {
      this.logger.warn(ZOOM_ANALYTICS_LOG.WEBHOOK_REJECTED_STALE, { event: body?.event });
      throw new InifniBadRequestException(ERROR_CODES.ZOOM_WEBHOOK_INVALID_SIGNATURE, null, null);
    }

    const signature = this.headerValue(headers, ZOOM_ANALYTICS_WEBHOOK_HEADER.SIGNATURE);
    if (
      !ZoomAnalyticsWebhookSignatureUtil.verifySignature(
        body,
        signature,
        timestamp,
        this.config.getWebhookSecret(),
      )
    ) {
      this.logger.warn(ZOOM_ANALYTICS_LOG.WEBHOOK_REJECTED_SIGNATURE, { event: body?.event });
      throw new InifniBadRequestException(ERROR_CODES.ZOOM_WEBHOOK_INVALID_SIGNATURE, null, null);
    }

    await this.dispatch(body);
  }

  private async dispatch(body: ZoomAnalyticsWebhookBody): Promise<void> {
    // Raw payload — signature already verified by this point in process(). Only place the exact
    // shape Zoom sends (field names, which are populated/empty) is ever visible; nothing downstream
    // persists the full body, only the handful of fields each handler extracts.
    this.logger.log(ZOOM_ANALYTICS_LOG.WEBHOOK_RECEIVED, { event: body.event, payload: body.payload });
    try {
      const resourceType = this.resourceTypeForEvent(body.event);
      switch (body.event) {
        case ZoomAnalyticsWebhookEvent.PARTICIPANT_JOINED:
        case ZoomAnalyticsWebhookEvent.MEETING_PARTICIPANT_JOINED:
          await this.onParticipantJoined(body.payload?.object, resourceType);
          break;
        case ZoomAnalyticsWebhookEvent.PARTICIPANT_LEFT:
        case ZoomAnalyticsWebhookEvent.MEETING_PARTICIPANT_LEFT:
          await this.onParticipantLeft(body.payload?.object, resourceType);
          break;
        case ZoomAnalyticsWebhookEvent.WEBINAR_STARTED:
        case ZoomAnalyticsWebhookEvent.MEETING_STARTED:
          await this.onSessionStarted(body.payload?.object, resourceType);
          break;
        case ZoomAnalyticsWebhookEvent.WEBINAR_ENDED:
        case ZoomAnalyticsWebhookEvent.MEETING_ENDED:
          await this.onSessionEnded(body.payload?.object, resourceType);
          break;
        default:
          this.logger.log('Unhandled Zoom analytics webhook event', { event: body.event });
      }
    } catch (error: any) {
      this.logger.error('Failed to handle Zoom analytics webhook event', error?.stack, {
        error,
        event: body.event,
      });
      handleKnownErrors(ERROR_CODES.ZOOM_WEBHOOK_PROCESSING_FAILED, error);
    }
  }

  /** The event name's own `webinar.`/`meeting.` prefix picks the provider — never a global config value. */
  private resourceTypeForEvent(event: string | undefined): ZoomAnalyticsResourceType {
    return event?.startsWith(MEETING_EVENT_PREFIX)
      ? ZoomAnalyticsResourceType.MEETING
      : ZoomAnalyticsResourceType.WEBINAR;
  }

  private async onParticipantJoined(
    object: ZoomAnalyticsWebhookObject | undefined,
    resourceType: ZoomAnalyticsResourceType,
  ): Promise<void> {
    const extId = String(object?.id ?? '');
    const email = object?.participant?.email;
    if (!extId || !email) {
      this.logger.warn(ZOOM_ANALYTICS_LOG.WEBHOOK_EVENT_MISSING_DATA, {
        event: 'participant_joined',
        extId,
        hasEmail: !!email,
      });
      return;
    }
    const joinTime = this.toDate(object?.participant?.join_time) ?? new Date();
    const meetingStartTime = this.toDate(object?.start_time);
    const provider = this.registry.resolve(resourceType);
    await provider.recordParticipantJoined(
      extId,
      object?.occurrence_id ?? null,
      email,
      joinTime,
      object?.participant?.id ?? null,
      object?.participant?.user_name ?? null,
      meetingStartTime,
    );
  }

  private async onParticipantLeft(
    object: ZoomAnalyticsWebhookObject | undefined,
    resourceType: ZoomAnalyticsResourceType,
  ): Promise<void> {
    const extId = String(object?.id ?? '');
    const email = object?.participant?.email;
    if (!extId || !email) {
      this.logger.warn(ZOOM_ANALYTICS_LOG.WEBHOOK_EVENT_MISSING_DATA, {
        event: 'participant_left',
        extId,
        hasEmail: !!email,
      });
      return;
    }
    const leaveTime = this.toDate(object?.participant?.leave_time) ?? new Date();
    const meetingStartTime = this.toDate(object?.start_time);
    const provider = this.registry.resolve(resourceType);
    await provider.recordParticipantLeft(
      extId,
      object?.occurrence_id ?? null,
      email,
      leaveTime,
      object?.participant?.id ?? null,
      object?.participant?.user_name ?? null,
      object?.participant?.leave_reason ?? null,
      meetingStartTime,
    );
  }

  private async onSessionStarted(
    object: ZoomAnalyticsWebhookObject | undefined,
    resourceType: ZoomAnalyticsResourceType,
  ): Promise<void> {
    const extId = String(object?.id ?? '');
    if (!extId) return;
    const startTime = this.toDate(object?.start_time) ?? new Date();
    const session = await this.webinarRepository.findByExtIdForOccurrence(
      extId,
      object?.occurrence_id ?? null,
      startTime,
      startTime,
    );
    if (!session) return;
    const provider = this.registry.resolve(resourceType);
    await provider.markStarted(session, startTime);
  }

  private async onSessionEnded(
    object: ZoomAnalyticsWebhookObject | undefined,
    resourceType: ZoomAnalyticsResourceType,
  ): Promise<void> {
    const extId = String(object?.id ?? '');
    if (!extId) return;
    const endTime = this.toDate(object?.end_time) ?? new Date();
    const meetingStartTime = this.toDate(object?.start_time);
    const session = await this.webinarRepository.findByExtIdForOccurrence(
      extId,
      object?.occurrence_id ?? null,
      endTime,
      meetingStartTime,
    );
    if (!session) {
      this.logger.log('Zoom analytics webhook: session ended for untracked resource', { extId });
      return;
    }
    const provider = this.registry.resolve(resourceType);

    // Synchronous and independent of Report API readiness — getKpis needs to
    // know the session has ended even while reconcile() is still lagging.
    await provider.markEnded(session, endTime);

    // Background — a heavy reconciliation pass must never block Zoom's
    // webhook-ack deadline. The admin's "Mark session as complete" click
    // re-runs reconcile() as the safety net if this one dies mid-way;
    // reconcile() is idempotent.
    setImmediate(() => {
      Promise.resolve()
        .then(() => provider.reconcile(session))
        .catch((error: any) =>
          this.logger.error(ZOOM_ANALYTICS_LOG.BACKGROUND_RECONCILE_FAILED, error?.stack, {
            error,
            extId,
          }),
        );
    });
  }

  private headerValue(headers: Record<string, unknown>, name: string): string | undefined {
    const value = headers?.[name];
    return Array.isArray(value) ? value[0] : (value as string | undefined);
  }

  private toDate(value: unknown): Date | null {
    if (!value || (typeof value !== 'string' && typeof value !== 'number')) return null;
    const d = new Date(value);
    return isNaN(d.getTime()) ? null : d;
  }
}
