import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ProgramRegistration, ProgramRegistrationOnlineSession, ProgramSession } from 'src/common/entities';
import { AppLoggerService } from 'src/common/services/logger.service';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { InifniNotFoundException } from 'src/common/exceptions/infini-notfound-exception';
import InifniBadRequestException from 'src/common/exceptions/infini-badrequest-exception';
import { ZoomRegistrationRepository } from '../repositories/zoom-registration.repository';
import { ZoomRegistrationService } from './zoom-registration.service';
import { ZoomFinalSessionAttendanceService } from './zoom-final-session-attendance.service';
import { ZoomRegistrationUpdate } from '../enums/zoom-registration-update.enum';
import { ZoomRole } from '../enums/zoom-role.enum';
import {
  FinalSessionConfirmBlockedItem,
  FinalSessionConfirmFailedItem,
  FinalSessionConfirmResult,
} from '../interfaces/final-session-confirm.interface';

/**
 * Attendance-driven cutoff for a program's FINAL session, distinct from
 * `ZoomRegistrationService.setRegistrationActivation` (a manual on/off toggle
 * cascading over every upcoming session). A registrant who missed one or more
 * of a program's earlier (already elapsed) sessions is unregistered from ONLY
 * the final session — no separate status table: "currently blocked" is simply
 * "no live `ProgramRegistrationOnlineSession` row for the final session",
 * reusing the same register()/unregister() primitive
 * (`ZoomRegistrationService.handle`) the real seeker-registration flow uses.
 * The shared attendance math (which sessions have elapsed, who was present)
 * lives in `ZoomFinalSessionAttendanceService` — kept separate so
 * `ZoomRegistrationService` can depend on it too without a dependency cycle
 * back through this service.
 */
@Injectable()
export class ZoomFinalSessionConfirmService {
  constructor(
    @InjectRepository(ProgramSession)
    private readonly programSessionRepo: Repository<ProgramSession>,
    private readonly registrationRepository: ZoomRegistrationRepository,
    private readonly zoomRegistrationService: ZoomRegistrationService,
    private readonly attendanceService: ZoomFinalSessionAttendanceService,
    private readonly logger: AppLoggerService,
  ) {}

  async confirmFinalSession(
    programId: number,
    actingUserId?: number,
    rmContactId?: number,
  ): Promise<FinalSessionConfirmResult> {
    const { finalSession, nonFinalSessions } = await this.resolveFinalSession(programId);
    const onlineSessionId = finalSession.onlineSession?.id;
    if (!onlineSessionId) {
      throw new InifniBadRequestException(
        ERROR_CODES.ZOOM_SESSION_NOT_PROVISIONED,
        null,
        null,
        finalSession.id.toString(),
      );
    }

    const { elapsedSessions, presentRegistrationIdsBySession } =
      await this.attendanceService.computeElapsedAttendance(nonFinalSessions);

    const candidates = await this.registrationRepository.listRegisteredExtensionsForSession(
      onlineSessionId,
      rmContactId,
    );

    const blocked: FinalSessionConfirmBlockedItem[] = [];
    const failed: FinalSessionConfirmFailedItem[] = [];

    for (const { registration } of candidates) {
      const missedSessionIds = elapsedSessions
        .filter((session) => !presentRegistrationIdsBySession.get(session.id)?.has(registration.id))
        .map((session) => session.id);

      if (!missedSessionIds.length) continue;

      try {
        await this.zoomRegistrationService.handle(
          {
            registrationId: registration.id,
            sessionId: finalSession.id,
            action: ZoomRegistrationUpdate.UNREGISTER,
            actingUserId,
          },
          // Block the FINAL session only — keep the (already-elapsed) earlier sessions'
          // registration rows intact rather than cascading across the shared webinar group.
          { restrictUnregisterToTargetSession: true },
        );
        blocked.push({ registrationId: registration.id, fullName: registration.fullName, missedSessionIds });
      } catch (error) {
        this.logger.error('Failed to unregister registrant from final session', error?.stack, {
          error,
          registrationId: registration.id,
          programId,
        });
        failed.push({ registrationId: registration.id, error: error?.message ?? 'Unknown error' });
      }
    }

    finalSession.onlineSession!.finalized = true;
    await this.programSessionRepo.save(finalSession);

    this.logger.log('Final session confirm run complete', {
      programId,
      finalSessionId: finalSession.id,
      totalEvaluated: candidates.length,
      blockedCount: blocked.length,
      failedCount: failed.length,
    });

    return {
      programId,
      finalSessionId: finalSession.id,
      totalEvaluated: candidates.length,
      blockedCount: blocked.length,
      blocked,
      failed,
    };
  }

  async reinstateForFinalSession(
    registrationId: number,
    reason: string | undefined,
    actingUserId?: number,
    rmContactId?: number,
  ): Promise<ProgramRegistrationOnlineSession | void> {
    const registration = await this.requireRegistration(registrationId);
    if (rmContactId != null && registration.rmContact !== rmContactId) {
      throw new InifniNotFoundException(ERROR_CODES.ZOOM_REGISTRATION_NOTFOUND, null, null, registrationId.toString());
    }
    if (!registration.programId) {
      throw new InifniBadRequestException(
        ERROR_CODES.ZOOM_SESSION_NOT_PROVISIONED,
        null,
        null,
        registrationId.toString(),
      );
    }

    const { finalSession } = await this.resolveFinalSession(registration.programId);

    const extension = await this.zoomRegistrationService.handle({
      registrationId,
      sessionId: finalSession.id,
      action: ZoomRegistrationUpdate.REGISTER,
      role: ZoomRole.ATTENDEE,
      actingUserId,
    });

    this.logger.log('Registrant reinstated for final session', {
      registrationId,
      finalSessionId: finalSession.id,
      reason: reason ?? null,
      actingUserId: actingUserId ?? null,
    });

    return extension;
  }

  /** Ordered sessions of a program (same convention used for TAT "final session" resolution). */
  private async resolveFinalSession(
    programId: number,
  ): Promise<{ finalSession: ProgramSession; nonFinalSessions: ProgramSession[] }> {
    const sessions = await this.attendanceService.loadOrderedSessions(programId);

    if (sessions.length <= 1) {
      throw new InifniBadRequestException(
        ERROR_CODES.ZOOM_FINAL_SESSION_NOT_APPLICABLE,
        null,
        null,
        programId.toString(),
      );
    }

    return { finalSession: sessions[sessions.length - 1], nonFinalSessions: sessions.slice(0, -1) };
  }

  private async requireRegistration(registrationId: number): Promise<ProgramRegistration> {
    const registration = await this.registrationRepository.findRegistrationById(registrationId);
    if (!registration) {
      throw new InifniNotFoundException(ERROR_CODES.ZOOM_REGISTRATION_NOTFOUND, null, null, registrationId.toString());
    }
    return registration;
  }
}
