import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
import { ProgramRegistrationOnlineSession, ProgramRegistration, User } from 'src/common/entities';
import { OnlineSessionRegistrationStatus } from 'src/common/enum/online-session-registration-status.enum';
import { RegistrationOnlineSessionActivationStatus } from 'src/common/enum/registration-online-session-activation-status.enum';
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';

/** One seeker on the roster of a webinar or meeting online session. */
export interface ZoomAnalyticsRosterEntry {
  registrationId: number;
  userId: number | null;
  fullName: string | null;
  email: string | null;
  mobile: string | null;
  /** The seeker's associated Relational Manager (registration.rm_contact), for the follow-up table. */
  rmName: string | null;
  /** registration.user_profile_url. */
  profileImage: string | null;
}

const ROSTER_SELECT = [
  'registration.id AS "registrationId"',
  'registration.user_id AS "userId"',
  'registration.full_name AS "fullName"',
  'registration.email_address AS "email"',
  'registration.mobile_number AS "mobile"',
  'rm_user.full_name AS "rmName"',
  'registration.user_profile_url AS "profileImage"',
];

/**
 * Read-only repository over the *existing* `ProgramRegistrationOnlineSession`
 * / `ProgramRegistration` tables — reused as data sources, never modified.
 * Builds the "who was supposed to attend" roster for each resource type:
 * - Webinars register seekers as PANELISTS, not registrants, so
 *   `findPanelistRosterByOnlineSession` is deliberately NOT the same universe
 *   Zoom's `/past_webinars/{id}/absentees` endpoint would give you (that's
 *   registrant-scoped and would miss every panelist).
 * - Meetings never set `is_panelist` (no panelist concept there — see
 *   MeetingService.addParticipant), so `findRegistrantRosterByOnlineSession`
 *   is the same query minus that filter: every REGISTERED registrant counts.
 */
@Injectable()
export class ZoomAnalyticsRosterRepository {
  constructor(
    @InjectRepository(ProgramRegistrationOnlineSession)
    private readonly extensionRepo: Repository<ProgramRegistrationOnlineSession>,
    private readonly logger: AppLoggerService,
  ) {}

  /** A row drops out of the roster the moment it's INACTIVE for this session, regardless of why. */
  private excludeInactiveRows<T extends ObjectLiteral>(query: SelectQueryBuilder<T>): SelectQueryBuilder<T> {
    return query.andWhere('extension.activation_status = :activeActivationStatus', {
      activeActivationStatus: RegistrationOnlineSessionActivationStatus.ACTIVE,
    });
  }

  /**
   * `rmContactId`, when passed, scopes the roster to seekers whose registration's
   * `rm_contact` is that RM user — used to restrict an RM caller's analytics view
   * to their own seekers. Left undefined for every internal/bookkeeping caller
   * (reconcile, webhook matching, self-heal), which must always see the full roster.
   */
  async findPanelistRosterByOnlineSession(
    onlineSessionId: number,
    rmContactId?: number,
  ): Promise<ZoomAnalyticsRosterEntry[]> {
    try {
      const query = this.extensionRepo
        .createQueryBuilder('extension')
        // See findRegistrantRosterByOnlineSession for why .withDeleted() is required here.
        .withDeleted()
        .innerJoin(ProgramRegistration, 'registration', 'registration.id = extension.registration_id')
        .leftJoin(User, 'rm_user', 'rm_user.id = registration.rm_contact')
        .where('extension.online_session_id = :onlineSessionId', { onlineSessionId })
        .andWhere('extension.is_panelist = true')
        .andWhere('extension.status = :status', { status: OnlineSessionRegistrationStatus.REGISTERED })
        .andWhere('extension.deleted_at IS NULL');
      this.excludeInactiveRows(query);
      if (rmContactId) {
        query.andWhere('registration.rm_contact = :rmContactId', { rmContactId });
      }
      const rows = await query.select(ROSTER_SELECT).getRawMany<ZoomAnalyticsRosterEntry>();
      return rows.map(this.coerceRow);
    } catch (error) {
      this.logger.error('Error finding Zoom analytics panelist roster', error?.stack, {
        error,
        onlineSessionId,
      });
      handleKnownErrors(ERROR_CODES.ZOOM_ANALYTICS_GET_FAILED, error);
    }
  }

  async findRegistrantRosterByOnlineSession(
    onlineSessionId: number,
    rmContactId?: number,
  ): Promise<ZoomAnalyticsRosterEntry[]> {
    try {
      const query = this.extensionRepo
        .createQueryBuilder('extension')
        // Disables TypeORM's automatic soft-delete join filter — ProgramRegistration has a
        // @DeleteDateColumn, so a plain innerJoin silently re-adds `registration.deleted_at IS NULL`
        // to the join condition even with no explicit `.andWhere` for it. This session's roster is a
        // historical record governed by THIS extension row's own active/inactive state, not by
        // whatever later happens to the registration (cancelled/archived/deleted) elsewhere.
        .withDeleted()
        .innerJoin(ProgramRegistration, 'registration', 'registration.id = extension.registration_id')
        .leftJoin(User, 'rm_user', 'rm_user.id = registration.rm_contact')
        .where('extension.online_session_id = :onlineSessionId', { onlineSessionId })
        .andWhere('extension.status = :status', { status: OnlineSessionRegistrationStatus.REGISTERED })
        .andWhere('extension.deleted_at IS NULL');
      this.excludeInactiveRows(query);
      if (rmContactId) {
        query.andWhere('registration.rm_contact = :rmContactId', { rmContactId });
      }
      const rows = await query.select(ROSTER_SELECT).getRawMany<ZoomAnalyticsRosterEntry>();
      return rows.map(this.coerceRow);
    } catch (error) {
      this.logger.error('Error finding Zoom analytics meeting registrant roster', error?.stack, {
        error,
        onlineSessionId,
      });
      handleKnownErrors(ERROR_CODES.ZOOM_ANALYTICS_GET_FAILED, error);
    }
  }

  /**
   * `registration.id`/`registration.user_id` are `bigint` columns — node-postgres
   * returns bigint as a string (to avoid precision loss), but `getRawMany` does no
   * further coercion, so a raw row's `registrationId`/`userId` are actually strings
   * at runtime despite the `number` type. Every consumer compares/keys-by these
   * (e.g. `Map<number, ...>.get(seeker.registrationId)`), so a string here would
   * silently fail every such lookup — coerce to a real number at the one boundary
   * where these rows enter the system.
   */
  private coerceRow(row: ZoomAnalyticsRosterEntry): ZoomAnalyticsRosterEntry {
    return {
      ...row,
      registrationId: Number(row.registrationId),
      userId: row.userId === null ? null : Number(row.userId),
    };
  }
}
