import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Repository } from 'typeorm';
import { ProgramSession } from 'src/common/entities';
import { AttendanceStatus } from 'src/common/enum/attendance-status.enum';
import { OnlineAttendanceService } from 'src/online-attendance/online-attendance.service';

/**
 * Standalone attendance-driven "final session" resolver, shared by
 * `ZoomFinalSessionConfirmService` (unregisters an already-registered absentee),
 * `ZoomBulkRegistrationService` (skips generating a NEW join link for one), and
 * `ZoomRegistrationService` (reflects the same exclusion in the pre-registration
 * "registered" count). Deliberately has no dependency on any of those three — it
 * only needs the program's sessions + attendance marks — so none of them form a
 * dependency cycle by depending on it.
 */
@Injectable()
export class ZoomFinalSessionAttendanceService {
  constructor(
    @InjectRepository(ProgramSession)
    private readonly programSessionRepo: Repository<ProgramSession>,
    private readonly onlineAttendanceService: OnlineAttendanceService,
  ) {}

  /**
   * Registration ids (from `candidateRegistrationIds`) that missed at least one of the program's
   * already-elapsed earlier sessions — the attendance-driven rule `ZoomFinalSessionConfirmService
   * .confirmFinalSession` unregisters on, generalized to an arbitrary candidate set so a caller
   * that hasn't registered them for the final session yet (bulk link generation, the pre-
   * registration "registered" count) can gate on it too. Returns null when the program has <=1
   * sessions (no final-session concept applies).
   */
  async resolveFinalSessionAbsentees(
    programId: number,
    candidateRegistrationIds: number[],
  ): Promise<{ finalSessionId: number; absenteeRegistrationIds: Set<number> } | null> {
    const sessions = await this.loadOrderedSessions(programId);
    if (sessions.length <= 1) {
      return null;
    }
    const finalSession = sessions[sessions.length - 1];
    const nonFinalSessions = sessions.slice(0, -1);

    const absenteeRegistrationIds = new Set<number>();
    if (candidateRegistrationIds.length) {
      const { elapsedSessions, presentRegistrationIdsBySession } =
        await this.computeElapsedAttendance(nonFinalSessions);
      for (const registrationId of candidateRegistrationIds) {
        const missedAny = elapsedSessions.some(
          (session) => !presentRegistrationIdsBySession.get(session.id)?.has(registrationId),
        );
        if (missedAny) {
          absenteeRegistrationIds.add(registrationId);
        }
      }
    }

    return { finalSessionId: finalSession.id, absenteeRegistrationIds };
  }

  /**
   * The program's final session id, or null when the program has <=1 sessions (no final-session
   * concept applies). A single cheap query — deliberately separate from
   * {@link resolveFinalSessionAbsentees} so a caller on a hot/high-traffic path (e.g. the
   * unscoped online-session list, which can span many programs at once) can cheaply check "is
   * this session even a candidate?" before paying for the eligible-registrations fetch +
   * per-elapsed-session attendance queries that only matter if it is.
   */
  async getFinalSessionId(programId: number): Promise<number | null> {
    const sessions = await this.loadOrderedSessions(programId);
    return sessions.length > 1 ? sessions[sessions.length - 1].id : null;
  }

  /** Ordered (startsAt, displayOrder, id) non-deleted sessions of a program, with their online session. */
  async loadOrderedSessions(programId: number): Promise<ProgramSession[]> {
    return this.programSessionRepo.find({
      where: { programId, deletedAt: IsNull() },
      relations: { onlineSession: true },
      order: { startsAt: 'ASC', displayOrder: 'ASC', id: 'ASC' },
    });
  }

  /**
   * Of a program's non-final sessions, the ones already elapsed, each paired with the set of
   * registration ids marked PRESENT — the raw data every caller above gates on to find who missed
   * a prior session. One lookup per elapsed session (not per registrant, and run concurrently
   * rather than one at a time); the present set is reused for every candidate.
   */
  async computeElapsedAttendance(
    nonFinalSessions: ProgramSession[],
  ): Promise<{ elapsedSessions: ProgramSession[]; presentRegistrationIdsBySession: Map<number, Set<number>> }> {
    const now = new Date();
    const elapsedSessions = nonFinalSessions.filter(
      (session) => session.startsAt && new Date(session.startsAt).getTime() <= now.getTime(),
    );

    const presentRegistrationIdsBySession = new Map<number, Set<number>>();
    await Promise.all(
      elapsedSessions.map(async (session) => {
        const marks = await this.onlineAttendanceService.getManualMarksBySession(session.id);
        presentRegistrationIdsBySession.set(
          session.id,
          new Set(
            marks
              .filter((mark) => mark.attendanceStatus === AttendanceStatus.PRESENT)
              .map((mark) => mark.registrationId),
          ),
        );
      }),
    );

    return { elapsedSessions, presentRegistrationIdsBySession };
  }
}
