import { Injectable, OnModuleInit } from '@nestjs/common';
import {
  ProgramSession,
  ProgramRegistrationOnlineSession,
  BackgroundJob,
} from 'src/common/entities';
import { OnlineTypeEnum } from 'src/common/enum/online-type.enum';
import { SessionProviderType } from 'src/common/enum/session-provider.enum';
import { SessionLinkModeEnum } from 'src/common/enum/session-link-mode.enum';
import {
  CreateSessionInput,
  UpdateSessionInput,
  CreateSharedSessionInput,
} from 'src/common/interfaces/online-session.interface';
import { OnlineSessionProvider } from 'src/online-session/interfaces/online-session-provider.interface';
import { OnlineSessionProviderRegistry } from 'src/online-session/services/online-session-provider.registry';
import {
  BulkRegistrationStart,
  BulkRegistrationFailureList,
  SessionRegistrationList,
  ProvisionStatusOverview,
  ProvisionRegistrationList,
  ProvisionRegistrationStatus,
  EligibleCountSummary,
  ProgramEligibleRegistrationList,
  ProgramEligibleRegistrationsQuery,
  RmContactOption,
  RegistrationActivationResult,
  SessionAttendanceSummary,
} from 'src/online-session/interfaces/online-session.interface';
import { RegistrationOnlineSessionActivationStatus } from 'src/common/enum/registration-online-session-activation-status.enum';
import { RegisterParticipantDto } from 'src/online-session/dto/register-participant.dto';
import { BulkRegisterParticipantsDto } from 'src/online-session/dto/bulk-register-participants.dto';
import { WebinarService } from './sessions/webinar.service';
import { MeetingService } from './sessions/meeting.service';
import { ZoomSessionHandler } from './interfaces/zoom-session.interface';
import { ZoomRegistrationService } from './services/zoom-registration.service';
import { ZoomBulkRegistrationService } from './services/zoom-bulk-registration.service';
import { ZoomGeneratedLinkService } from './services/zoom-generated-link.service';
import { SessionCommunicationService } from 'src/session-communication/session-communication.service';
import { GenerateZoomGeneralLinksV1Dto } from './dto/generate-zoom-general-links-v1.dto';
import {
  ROLE_MAP,
  ACTION_MAP,
  SHARED_SESSION_GENERAL_LINK_ROLE_KEYS,
  SHARED_SESSION_GENERAL_LINK_PLACEHOLDER,
} from './constants/zoom-provider.constants';
import { AppLoggerService } from 'src/common/services/logger.service';
import { UserTypeFilterValue } from 'src/common/utils/user-type-filter.util';

/**
 * ADAPTER: exposes Zoom through the provider-neutral OnlineSessionProvider port
 * and self-registers with the orchestrator's registry (same idiom as queue
 * processors). Webinar vs meeting is a one-line dispatch — no factory.
 */
@Injectable()
export class ZoomProvider implements OnlineSessionProvider, OnModuleInit {
  readonly key = SessionProviderType.ZOOM;

  constructor(
    private readonly registry: OnlineSessionProviderRegistry,
    private readonly webinar: WebinarService,
    private readonly meeting: MeetingService,
    private readonly registration: ZoomRegistrationService,
    private readonly bulkRegistration: ZoomBulkRegistrationService,
    private readonly generatedLink: ZoomGeneratedLinkService,
    private readonly sessionCommunication: SessionCommunicationService,
    private readonly logger: AppLoggerService,
  ) {}

  onModuleInit(): void {
    this.registry.register(this.key, this);
  }

  create(input: CreateSessionInput): Promise<ProgramSession> {
    return this.handler(input.onlineType).create(input);
  }

  /** Shared ("same link") provisioning — recurring webinar or meeting, per onlineType. */
  async createShared(input: CreateSharedSessionInput): Promise<ProgramSession[]> {
    const sessions = await this.handler(input.onlineType).createSharedGroup(input);
    // General/common-invite links assume ONE link shared by the whole program — only
    // meaningful for the SHARED link type. PER_SESSION (individual) sessions each have
    // their own distinct link, so there is no single common link to generate/announce.
    if (input.linkType !== SessionLinkModeEnum.SHARED) {
      return sessions;
    }
    const programId = sessions.find((session) => session.programId != null)?.programId;
    // Fire-and-forget: auto-issue Zoom general links for RM / Admin / Coordinator in the
    // background so it never blocks or affects the createShared response. Failures are
    // logged inside the task; the .catch is a safety net against any unhandled rejection.
    if (programId != null) {
      void this.generateSharedSessionRoleLinks(Number(programId), input.actorUserId).catch(
        (error) => {
          this.logger.error(
            `Background shared-session general-link generation crashed: ${(error as Error)?.message}`,
            (error as Error)?.stack,
          );
        },
      );
    } else {
      this.logger.warn(
        `createShared: no programId found in returned sessions, skipping background shared-session general-link generation`,
      );
    }
    return sessions;
  }

  /**
   * Best-effort background task: generate Zoom general links for the RM, Admin and Coordinator
   * roles across the program (every provisioned session, idempotent per the generate flow).
   * Never fails the session creation.
   */
  private async generateSharedSessionRoleLinks(
    programId: number,
    actorUserId?: number,
  ): Promise<void> {
    try {
      const result = await this.generatedLink.generate(
        {
          programId,
          roleKeys: SHARED_SESSION_GENERAL_LINK_ROLE_KEYS,
          placeholderName: SHARED_SESSION_GENERAL_LINK_PLACEHOLDER.name,
          placeholderCount: SHARED_SESSION_GENERAL_LINK_PLACEHOLDER.count,
          placeholderDomain: SHARED_SESSION_GENERAL_LINK_PLACEHOLDER.domain,
        } as GenerateZoomGeneralLinksV1Dto,
        actorUserId,
      );
      this.logger.log(
        `Shared-session general-link generation finished for program ${programId}: ${result.created.length} created, ${result.skippedExisting} already existed, ${result.failed.length} failed`,
      );
      // The links now exist — notify the staff who hold them (email + WhatsApp). Best-effort:
      // a common-invite failure must never affect the (already-returned) createShared response.
      try {
        this.logger.log(
          `Auto-sending common invite for program ${programId} after shared-session general-link generation`,
        );
        await this.sessionCommunication.sendCommonInviteBulk(programId, actorUserId ?? 0);
      } catch (error) {
        this.logger.error(
          `Failed to auto-send common invite for program ${programId}: ${(error as Error)?.message}`,
          (error as Error)?.stack,
        );
      }

      // Notify admins of the generated system/placeholder links (email only). Independent
      // best-effort task — a failure here must not affect anything above.
      try {
        this.logger.log(
          `Auto-sending system links for program ${programId} after shared-session general-link generation`,
        );
        await this.sessionCommunication.sendSystemLinksBulk(programId, actorUserId ?? 0);
      } catch (error) {
        this.logger.error(
          `Failed to auto-send system links for program ${programId}: ${(error as Error)?.message}`,
          (error as Error)?.stack,
        );
      }
    } catch (error) {
      this.logger.error(
        `Failed to auto-generate shared-session general links for program ${programId}: ${(error as Error)?.message}`,
        (error as Error)?.stack,
      );
      return;
    }
  }

  /**
   * Fire-and-forget: auto-issue general (role + placeholder/"system") links for one
   * session — used by bulk creation, where sessions may belong to different programs
   * so there is no single shared programId to fan out from. This is the PER_SESSION
   * analog of `createShared`'s background task: once the session's role links exist, the
   * per-session common invite (to their holders) and per-session system links (to admins) are
   * sent, scoped to this session so the messages carry this session's name/date/time. Best-effort
   * — a notification failure never affects the (already-completed) provisioning.
   */
  generateGeneralLinks(session: ProgramSession, actorUserId?: number): void {
    void this.generatedLink
      .generate(
        {
          sessionId: session.id,
          roleKeys: SHARED_SESSION_GENERAL_LINK_ROLE_KEYS,
          placeholderName: SHARED_SESSION_GENERAL_LINK_PLACEHOLDER.name,
          placeholderCount: SHARED_SESSION_GENERAL_LINK_PLACEHOLDER.count,
          placeholderDomain: SHARED_SESSION_GENERAL_LINK_PLACEHOLDER.domain,
        } as GenerateZoomGeneralLinksV1Dto,
        actorUserId,
      )
      .then((result) => {
        this.logger.log(
          `Bulk-session general-link generation finished for session ${session.id}: ${result.created.length} created, ${result.skippedExisting} already existed, ${result.failed.length} failed`,
        );
      })
      .then(() => this.sendPerSessionGeneralLinkNotifications(session, actorUserId))
      .catch((error) => {
        this.logger.error(
          `Background bulk-session general-link generation crashed for session ${session.id}: ${(error as Error)?.message}`,
          (error as Error)?.stack,
        );
      });
  }

  /**
   * Notify the holders of this session's generated links — the per-session common invite (staff
   * holding ROLE links) and the per-session system links (admins) — scoped to this one session.
   * Passing the sessionId selects the per-session template variant and scopes recipients + merge
   * fields to this session. The per-session analog of the both-sends step in
   * `generateSharedSessionRoleLinks`. Each send is independently best-effort: swallows failures
   * (including "no recipients") so it never affects the caller's fire-and-forget generation flow.
   */
  private async sendPerSessionGeneralLinkNotifications(
    session: ProgramSession,
    actorUserId?: number,
  ): Promise<void> {
    const programId = session.programId;
    if (programId == null) {
      return;
    }
    try {
      this.logger.log(
        `Auto-sending per-session common invite for program ${programId} session ${session.id}`,
      );
      await this.sessionCommunication.sendCommonInviteBulk(
        Number(programId),
        actorUserId ?? 0,
        session.id,
      );
    } catch (error) {
      this.logger.error(
        `Failed to auto-send per-session common invite for session ${session.id}: ${(error as Error)?.message}`,
        (error as Error)?.stack,
      );
    }
    try {
      this.logger.log(
        `Auto-sending per-session system links for program ${programId} session ${session.id}`,
      );
      await this.sessionCommunication.sendSystemLinksBulk(
        Number(programId),
        actorUserId ?? 0,
        session.id,
      );
    } catch (error) {
      this.logger.error(
        `Failed to auto-send per-session system links for session ${session.id}: ${(error as Error)?.message}`,
        (error as Error)?.stack,
      );
    }
  }

  update(session: ProgramSession, input: UpdateSessionInput): Promise<ProgramSession> {
    return this.handler(session.onlineType).update(session, input);
  }

  remove(session: ProgramSession, actorUserId?: number): Promise<void> {
    return this.handler(session.onlineType).remove(session, actorUserId);
  }

  // ---------------------------------------------------------------------------
  // Registration & tracking
  // ---------------------------------------------------------------------------

  register(input: RegisterParticipantDto): Promise<ProgramRegistrationOnlineSession | void> {
    return this.registration.handle({
      registrationId: input.registrationId,
      sessionId: input.sessionId,
      action: ACTION_MAP[input.action],
      role: input.role ? ROLE_MAP[input.role] : undefined,
      actingUserId: input.actingUserId,
    });
  }

  bulkRegister(
    input: BulkRegisterParticipantsDto,
    actorUserId?: number,
  ): Promise<BulkRegistrationStart> {
    return this.bulkRegistration.startBulkRegistration(
      {
        programId: input.programId,
        sessionId: input.sessionId,
        role: input.role ? ROLE_MAP[input.role] : undefined,
        batchSize: input.batchSize,
      },
      actorUserId,
    );
  }

  bulkRegisterStatus(jobId: number): Promise<BackgroundJob> {
    return this.bulkRegistration.getBulkJobStatus(jobId);
  }

  bulkRegisterFailures(
    jobId: number,
    paging: { page: number; limit: number },
    rmContactId?: number,
  ): Promise<BulkRegistrationFailureList> {
    return this.bulkRegistration.getBulkJobFailures(jobId, paging, rmContactId);
  }

  retryBulkRegistration(jobId: number, actorUserId?: number): Promise<BulkRegistrationStart> {
    return this.bulkRegistration.retryFailedRegistration(jobId, actorUserId);
  }

  getProgramRegistrationFailures(
    programId: number,
    query: { sessionId?: number; page: number; limit: number },
    rmContactId?: number,
  ): Promise<BulkRegistrationFailureList> {
    return this.bulkRegistration.getProgramFailures(programId, query, rmContactId);
  }

  retryProgramRegistration(
    programId: number,
    sessionId?: number,
    actorUserId?: number,
  ): Promise<BulkRegistrationStart> {
    return this.bulkRegistration.retryProgramFailures(programId, sessionId, actorUserId);
  }

  getProgramProvisionStatus(
    programId: number,
    sessionId?: number,
    rmContactId?: number,
    userType?: UserTypeFilterValue[],
  ): Promise<ProvisionStatusOverview> {
    return this.bulkRegistration.getProgramProvisionStatus(
      programId,
      sessionId,
      rmContactId,
      userType,
    );
  }

  getSessionProvisionRegistrations(
    sessionId: number,
    query: {
      status?: ProvisionRegistrationStatus;
      page: number;
      limit: number;
      userType?: UserTypeFilterValue[];
    },
    rmContactId?: number,
  ): Promise<ProvisionRegistrationList> {
    return this.bulkRegistration.getSessionProvisionRegistrations(sessionId, query, rmContactId);
  }

  listRegistrations(
    sessionId: number,
    query: { page: number; limit: number; search?: string },
    rmContactId?: number,
  ): Promise<SessionRegistrationList> {
    return this.registration.listSessionRegistrations(sessionId, query, rmContactId);
  }

  exportRegistrations(
    sessionId: number,
    search?: string,
    rmContactId?: number,
  ): Promise<{ fileUrl: string }> {
    return this.registration.exportSessionRegistrations(sessionId, search, rmContactId);
  }

  setRegistrationActivation(
    registrationId: number,
    activationStatus: RegistrationOnlineSessionActivationStatus,
    reason: string | null | undefined,
    actingUserId: number | null | undefined,
  ): Promise<RegistrationActivationResult> {
    return this.registration.setRegistrationActivation(
      registrationId,
      activationStatus,
      reason,
      actingUserId,
    );
  }

  getEligibleCount(sessionId: number): Promise<EligibleCountSummary> {
    return this.registration.getEligibleCount(sessionId);
  }

  listProgramEligibleRegistrations(
    programId: number,
    query: ProgramEligibleRegistrationsQuery,
  ): Promise<ProgramEligibleRegistrationList> {
    return this.registration.listProgramEligibleRegistrations(programId, query);
  }

  getProgramEligibleRmContacts(): Promise<RmContactOption[]> {
    return this.registration.getProgramEligibleRmContacts();
  }

  getSessionAttendanceSummary(
    sessions: ProgramSession[],
    rmContactId?: number,
  ): Promise<Map<number, SessionAttendanceSummary>> {
    return this.registration.getSessionAttendanceSummary(sessions, rmContactId);
  }

  /** Picks the per-type handler. A third type is one more branch + one file. */
  private handler(type?: OnlineTypeEnum): ZoomSessionHandler {
    return type === OnlineTypeEnum.MEETING ? this.meeting : this.webinar;
  }
}
