import {
  Controller,
  Get,
  Post,
  Put,
  Patch,
  Delete,
  Body,
  Param,
  Query,
  Req,
  Res,
  HttpCode,
  HttpStatus,
  ParseIntPipe,
  UseGuards,
  UsePipes,
  ValidationPipe,
} from '@nestjs/common';
import { Response } from 'express';
import {
  ApiTags,
  ApiOperation,
  ApiBearerAuth,
  ApiSecurity,
  ApiBody,
  ApiParam,
  ApiQuery,
} from '@nestjs/swagger';
import { ProgramSession } from 'src/common/entities';
import { toWebinarDetails, toMeetingDetails } from 'src/common/utils/online-session.mapper';
import { AuthenticatedRequest } from 'src/common/interfaces/authenticated-request.interface';
import { ResponseService } from 'src/common/response-handling/response-handler';
import { handleControllerError } from 'src/common/utils/controller-response-handling';
import { RolesGuard } from 'src/common/guards/roles.guard';
import { Roles } from 'src/common/decorators/roles.decorator';
import { ROLE_VALUES } from 'src/common/constants/strings-constants';
import { CombinedAuthGuard } from 'src/auth/combined-auth.guard';
import { ZoomWebinarStatus } from 'src/common/enum/zoom-webinar-status.enum';
import {
  ProgramEligibleKpiCategory,
  ProgramEligibleKpiFilter,
} from 'src/common/enum/program-eligible-kpi.enum';
import InifniBadRequestException from 'src/common/exceptions/infini-badrequest-exception';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { OnlineSessionService } from '../services/online-session.service';
import { CreateOnlineSessionDto } from '../dto/create-online-session.dto';
import { BulkCreateOnlineSessionDto } from '../dto/bulk-create-online-session.dto';
import { CreateSharedOnlineSessionDto } from '../dto/create-shared-online-session.dto';
import { UpdateOnlineSessionDto } from '../dto/update-online-session.dto';
import { UpdateOnlineSessionStatusDto } from '../dto/update-online-session-status.dto';
import { RegisterParticipantDto } from '../dto/register-participant.dto';
import { BulkRegisterParticipantsDto } from '../dto/bulk-register-participants.dto';
import { BulkFailuresQueryDto } from '../dto/bulk-failures-query.dto';
import { ProgramFailuresQueryDto, ProgramRetryQueryDto } from '../dto/program-failures-query.dto';
import { ListSessionRegistrationsDto } from '../dto/list-session-registrations.dto';
import { ListProgramEligibleRegistrationsDto } from '../dto/list-program-eligible-registrations.dto';
import { SetRegistrationActivationDto } from '../dto/set-registration-activation.dto';
import {
  ProvisionStatusQueryDto,
  ProvisionRegistrationsQueryDto,
} from '../dto/provision-status-query.dto';
import {
  OnlineSessionResponse,
  SessionBulkCommunications,
  SessionAttendanceSummary,
  PendingSessionJob,
} from '../interfaces/online-session.interface';
import {
  ONLINE_SESSION_RESPONSE,
  ONLINE_SESSION_SWAGGER,
  ONLINE_SESSION_LIST_DEFAULTS,
  ONLINE_SESSION_LIST_TABLE_HEADERS,
  PROGRAM_ELIGIBLE_REGISTRATIONS_TABLE_HEADERS,
  PROGRAM_ELIGIBLE_REGISTRATIONS_STATIC_FILTERS,
  getProvisionRegistrationsTableHeaders,
  OnlineSessionFilterOption,
} from 'src/common/constants/online-session.constants';

/**
 * Provider-agnostic online-session API. Knows nothing about Zoom — it only
 * speaks to the OnlineSessionService facade.
 */
@ApiTags(ONLINE_SESSION_SWAGGER.TAG)
@Controller('online-session')
@UseGuards(CombinedAuthGuard, RolesGuard)
@Roles('admin', 'shoba', 'relational_manager')
@ApiBearerAuth('Authorization')
@ApiSecurity('userIdAuth')
@ApiSecurity('activeRoleAuth')
export class OnlineSessionController {
  constructor(
    private readonly service: OnlineSessionService,
    private readonly responseService: ResponseService,
  ) {}

  /**
   * Strips the host `startUrl` (grants host control) before returning. `communications`
   * (Invite/Absent bulk-comm status) and `attendanceSummary` are attached only on GET
   * responses that pass them.
   */
  private toResponse(
    session: ProgramSession,
    communications?: SessionBulkCommunications,
    attendanceSummary?: SessionAttendanceSummary,
    pendingJob?: PendingSessionJob | null,
    includeProgramName = false,
  ): OnlineSessionResponse {
    const webinar = toWebinarDetails(session.onlineSession);
    const meeting = toMeetingDetails(session.onlineSession);
    const webinarDetails = webinar ? { ...webinar, startUrl: undefined } : null;
    const meetingDetails = meeting ? { ...meeting, startUrl: undefined } : null;
    return {
      id: session.id,
      programId: session.programId,
      programSessionId: session.id,
      name:
        includeProgramName && session.program?.name
          ? `${session.program.name} ${session.name}`
          : session.name,
      onlineType: session.onlineType,
      startsAt: session.startsAt,
      endsAt: session.endsAt,
      registrationStartsAt: session.registrationStartsAt,
      registrationEndsAt: session.registrationEndsAt,
      webinarDetails,
      meetingDetails,
      actualMeetingEndsAt: session.onlineSession?.actualMeetingEndsAt ?? null,
      hostStartOpensMinutesBefore: session.onlineSession?.hostStartOpensMinutesBefore ?? null,
      completed: !!session.endsAt && new Date(session.endsAt).getTime() < Date.now(),
      finalized: session.onlineSession?.finalized ?? false,
      // Session length in minutes (list table header key: 'duration').
      duration:
        session.startsAt && session.endsAt
          ? Math.round(
              (new Date(session.endsAt).getTime() - new Date(session.startsAt).getTime()) / 60000,
            )
          : null,
      valueCardDetails: session.valueCardDetails ?? null,
      pretestValueCardDetails: session.pretestValueCardDetails ?? null,
      ...(pendingJob !== undefined ? { pendingJob } : {}),
      ...(communications ? { communications } : {}),
      ...(attendanceSummary
        ? {
            // Flattened so each list table header key resolves directly on the row.
            registered: attendanceSummary.registered,
            attended: attendanceSummary.attended,
            absent: attendanceSummary.absent,
            under60: attendanceSummary.durationBuckets.under60,
            from60to90: attendanceSummary.durationBuckets.from60to90,
            from90to120: attendanceSummary.durationBuckets.from90to120,
          }
        : {}),
    };
  }

  /** Whether the caller holds the RM role — an RM only ever sees their own contacts. */
  private isRelationalManager(req: AuthenticatedRequest): boolean {
    return (req.user?.roles ?? []).includes(ROLE_VALUES.RELATIONAL_MANAGER) && !!req.user?.id;
  }

  @Post()
  @HttpCode(HttpStatus.CREATED)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.CREATE })
  @ApiBody({ type: CreateOnlineSessionDto })
  async create(@Body() dto: CreateOnlineSessionDto, @Req() req: AuthenticatedRequest, @Res() res: Response) {
    try {
      const userId = req.user?.id ?? dto.createdBy;
      dto.createdBy = userId;
      dto.updatedBy = userId;
      const data = await this.service.create(dto);
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.CREATED,
        this.toResponse(data),
        HttpStatus.CREATED,
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Post('bulk')
  @HttpCode(HttpStatus.CREATED)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.CREATE_BULK })
  @ApiBody({ type: BulkCreateOnlineSessionDto })
  async createBulk(
    @Body() dto: BulkCreateOnlineSessionDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const userId = req.user?.id;
      for (const session of dto.sessions) {
        session.createdBy = userId ?? session.createdBy;
        session.updatedBy = userId ?? session.updatedBy;
      }
      const { created, failed } = await this.service.createBulk(dto.sessions);
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.BULK_CREATED,
        { created: created.map((session) => this.toResponse(session)), failed },
        HttpStatus.CREATED,
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Post('shared')
  @HttpCode(HttpStatus.CREATED)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.CREATE_SHARED })
  @ApiBody({ type: CreateSharedOnlineSessionDto })
  async createShared(
    @Body() dto: CreateSharedOnlineSessionDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      dto.createdBy = req.user?.id ?? dto.createdBy;
      const created = await this.service.createShared(dto);
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.SHARED_CREATED,
        created.map((session) => this.toResponse(session, undefined, undefined, undefined, true)),
        HttpStatus.CREATED,
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Post('registrations')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.REGISTER })
  @ApiBody({ type: RegisterParticipantDto })
  async register(
    @Body() dto: RegisterParticipantDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      dto.actingUserId = req.user?.id ?? dto.actingUserId;
      const data = await this.service.register(dto);
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.REGISTRATION_PROCESSED,
        data ?? undefined,
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Post('registrations/bulk')
  @HttpCode(HttpStatus.ACCEPTED)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.BULK_REGISTER })
  @ApiBody({ type: BulkRegisterParticipantsDto })
  async bulkRegister(
    @Body() dto: BulkRegisterParticipantsDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const data = await this.service.bulkRegister(dto, req.user?.id);
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.BULK_REGISTRATION_STARTED,
        data,
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Get('registrations/bulk/:jobId')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.BULK_REGISTER_STATUS })
  @ApiParam({ name: 'jobId', type: Number })
  async bulkRegisterStatus(
    @Param('jobId', ParseIntPipe) jobId: number,
    @Res() res: Response,
  ) {
    try {
      const data = await this.service.getBulkRegistrationStatus(jobId);
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.BULK_REGISTRATION_STATUS,
        data,
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Get('registrations/bulk/:jobId/failures')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.BULK_REGISTER_FAILURES })
  @ApiParam({ name: 'jobId', type: Number })
  async bulkRegisterFailures(
    @Param('jobId', ParseIntPipe) jobId: number,
    @Query() query: BulkFailuresQueryDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const rmContactId = this.isRelationalManager(req) ? req.user?.id : undefined;
      const data = await this.service.getBulkRegistrationFailures(
        jobId,
        { page: query.page, limit: query.limit },
        rmContactId,
      );
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.BULK_REGISTRATION_FAILURES,
        data,
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Post('registrations/bulk/:jobId/retry')
  @HttpCode(HttpStatus.ACCEPTED)
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.BULK_REGISTER_RETRY })
  @ApiParam({ name: 'jobId', type: Number })
  async retryBulkRegistration(
    @Param('jobId', ParseIntPipe) jobId: number,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const data = await this.service.retryBulkRegistration(jobId, req.user?.id);
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.BULK_REGISTRATION_RETRY_STARTED,
        data,
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Get('registrations/failures')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.PROGRAM_REGISTER_FAILURES })
  async programRegisterFailures(
    @Query() query: ProgramFailuresQueryDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const rmContactId = this.isRelationalManager(req) ? req.user?.id : undefined;
      const data = await this.service.getProgramRegistrationFailures(
        query.programId,
        { sessionId: query.sessionId, page: query.page, limit: query.limit },
        rmContactId,
      );
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.BULK_REGISTRATION_FAILURES,
        data,
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Post('registrations/retry')
  @HttpCode(HttpStatus.ACCEPTED)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.PROGRAM_REGISTER_RETRY })
  async retryProgramRegistration(
    @Query() query: ProgramRetryQueryDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const data = await this.service.retryProgramRegistration(
        query.programId,
        query.sessionId,
        req.user?.id,
      );
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.BULK_REGISTRATION_RETRY_STARTED,
        data,
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Get('eligible-registrations')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.PROGRAM_ELIGIBLE_REGISTRATIONS })
  async listProgramEligibleRegistrations(
    @Query() query: ListProgramEligibleRegistrationsDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const parsedFilters: Record<string, string | string[]> | undefined = query.filters
        ? JSON.parse(decodeURIComponent(query.filters))
        : undefined;

      // kpiCategory/kpiFilter travel bundled inside the same `filters` JSON blob (same wire
      // convention the zoom attendee table's own kpiCategory/kpiFilter side filter uses) rather
      // than as separate top-level query params — split out here so the repository's generic
      // filters map never sees these two reserved keys.
      // `userType` is the one filter that legitimately arrives as an array (its checkbox set),
      // so the blob is typed `string | string[]`; kpiCategory/kpiFilter are always scalar, coerced
      // here so the enum validations below stay simple string comparisons.
      const { kpiCategory: rawKpiCategory, kpiFilter: rawKpiFilter, ...registrationFilters } =
        parsedFilters ?? {};
      const kpiCategory = rawKpiCategory == null ? undefined : String(rawKpiCategory);
      const kpiFilter = rawKpiFilter == null ? undefined : String(rawKpiFilter);
      if (!!kpiCategory !== !!kpiFilter) {
        throw new InifniBadRequestException(ERROR_CODES.ZOOM_ANALYTICS_INVALID_KPI_FILTER);
      }
      if (kpiCategory && !Object.values(ProgramEligibleKpiCategory).includes(kpiCategory as ProgramEligibleKpiCategory)) {
        throw new InifniBadRequestException(ERROR_CODES.ZOOM_ANALYTICS_INVALID_KPI_FILTER);
      }
      if (kpiFilter && !Object.values(ProgramEligibleKpiFilter).includes(kpiFilter as ProgramEligibleKpiFilter)) {
        throw new InifniBadRequestException(ERROR_CODES.ZOOM_ANALYTICS_INVALID_KPI_FILTER);
      }

      // An RM caller only ever sees their own contacts (same convention as the
      // main registration list view) — so the rmContact filter is pointless for
      // them and omitted from the response.
      const isRMUser = this.isRelationalManager(req);

      const [result, rmContacts, communications, isEligibleRegistrationsReportEnabled] = await Promise.all([
        this.service.listProgramEligibleRegistrations(query.programId, {
          offset: query.offset,
          limit: query.limit,
          search: query.search,
          sortKey: query.sortKey,
          sortOrder: query.sortOrder,
          filters: Object.keys(registrationFilters).length ? registrationFilters : undefined,
          kpiCategory: kpiCategory as ProgramEligibleKpiCategory | undefined,
          kpiFilter: kpiFilter as ProgramEligibleKpiFilter | undefined,
          rmContactId: isRMUser ? req.user?.id : undefined,
        }),
        isRMUser ? Promise.resolve([]) : this.service.getProgramEligibleRmContacts(),
        this.service.getProgramBulkCommunications(Number(query.programId)),
        this.service.isEligibleRegistrationsReportEnabled(Number(query.programId)),
      ]);
      const filters: OnlineSessionFilterOption[] = [
        ...PROGRAM_ELIGIBLE_REGISTRATIONS_STATIC_FILTERS,
        ...(isRMUser
          ? []
          : [
              {
                key: 'rmContact',
                label: 'RM',
                type: 'dropdown' as const,
                options: rmContacts.map((rm) => ({ label: rm.name, value: String(rm.id) })),
              },
            ]),
      ];
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.PROGRAM_ELIGIBLE_REGISTRATIONS_RETRIEVED,
        {
          ...result,
          communications,
          tableHeaders: PROGRAM_ELIGIBLE_REGISTRATIONS_TABLE_HEADERS,
          filters,
          isEligibleRegistrationsReportEnabled,
        },
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Get('provision-status')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.PROVISION_STATUS })
  async provisionStatus(
    @Query() query: ProvisionStatusQueryDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const rmContactId = this.isRelationalManager(req) ? req.user?.id : undefined;
      const overview = await this.service.getProgramProvisionStatus(
        query.programId,
        query.sessionId,
        rmContactId,
        query.userType,
      );
      await this.responseService.success(res, ONLINE_SESSION_RESPONSE.PROVISION_STATUS_RETRIEVED, {
        programId: overview.programId,
        totalEligible: overview.totalEligible,
        sessions: overview.sessions.map((row) => ({
          ...this.toResponse(row.session),
          counts: row.counts,
          kpis: row.kpis,
        })),
      });
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Get('provision-status/:sessionId/registrations')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.PROVISION_REGISTRATIONS })
  @ApiParam({ name: 'sessionId', type: Number })
  async provisionRegistrations(
    @Param('sessionId', ParseIntPipe) sessionId: number,
    @Query() query: ProvisionRegistrationsQueryDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const rmContactId = this.isRelationalManager(req) ? req.user?.id : undefined;
      const [data, isZoomLinksReportEnabled] = await Promise.all([
        this.service.getSessionProvisionRegistrations(
          sessionId,
          {
            status: query.status,
            page: query.page,
            limit: query.limit,
            userType: query.userType,
          },
          rmContactId,
        ),
        this.service.isZoomLinksReportEnabledForSession(sessionId),
      ]);
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.PROVISION_REGISTRATIONS_RETRIEVED,
        { ...data, tableHeaders: getProvisionRegistrationsTableHeaders(query.status), isZoomLinksReportEnabled },
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Get(':id/registrations')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.LIST_REGISTRATIONS })
  @ApiParam({ name: 'id', type: Number })
  async listRegistrations(
    @Param('id', ParseIntPipe) id: number,
    @Query() query: ListSessionRegistrationsDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const rmContactId = this.isRelationalManager(req) ? req.user?.id : undefined;
      if (query.download) {
        const data = await this.service.exportRegistrations(id, query.search, rmContactId);
        await this.responseService.success(
          res,
          ONLINE_SESSION_RESPONSE.REGISTRATION_EXPORT_READY,
          data,
        );
        return;
      }
      const data = await this.service.listRegistrations(
        id,
        { page: query.page, limit: query.limit, search: query.search },
        rmContactId,
      );
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.REGISTRATIONS_RETRIEVED,
        data,
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Patch('registrations/:registrationId/activation')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.SET_ACTIVATION })
  @ApiParam({ name: 'registrationId', type: Number })
  @ApiBody({ type: SetRegistrationActivationDto })
  async setRegistrationActivation(
    @Param('registrationId', ParseIntPipe) registrationId: number,
    @Body() dto: SetRegistrationActivationDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const data = await this.service.setRegistrationActivation(
        registrationId,
        dto.activationStatus,
        dto.reason,
        req.user?.id,
      );
      await this.responseService.success(res, ONLINE_SESSION_RESPONSE.ACTIVATION_UPDATED, data);
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Get(':id/eligible-count')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.ELIGIBLE_COUNT })
  @ApiParam({ name: 'id', type: Number })
  async getEligibleCount(@Param('id', ParseIntPipe) id: number, @Res() res: Response) {
    try {
      const data = await this.service.getEligibleCount(id);
      await this.responseService.success(res, ONLINE_SESSION_RESPONSE.ELIGIBLE_COUNT_RETRIEVED, data);
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Get()
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.LIST })
  @ApiQuery({ name: 'limit', type: Number, required: false })
  @ApiQuery({ name: 'offset', type: Number, required: false })
  @ApiQuery({ name: 'status', enum: ZoomWebinarStatus, required: false })
  @ApiQuery({ name: 'programId', type: Number, required: false })
  async findAll(
    @Query('limit') limit = ONLINE_SESSION_LIST_DEFAULTS.LIMIT,
    @Query('offset') offset = ONLINE_SESSION_LIST_DEFAULTS.OFFSET,
    @Query('status') status: ZoomWebinarStatus,
    @Query('programId') programId: number,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const result = await this.service.findAll(
        Number(limit),
        Number(offset),
        status,
        programId ? Number(programId) : undefined,
      );
      const rmContactId = this.isRelationalManager(req) ? req.user?.id : undefined;
      const [communicationsBySession, attendanceBySession, pendingJobBySession, isSessionReportEnabled] = await Promise.all([
        this.service.getSessionBulkCommunications(
          result.data.map((session) => ({
            id: Number(session.id),
            programId: session.programId != null ? Number(session.programId) : null,
          })),
        ),
        this.service.getSessionAttendanceSummary(result.data, rmContactId),
        this.service.getPendingJobsForSessions(
          result.data.map((session) => ({
            id: Number(session.id),
            createdBy: session.createdBy ?? null,
          })),
        ),
        // "Download session report" affordance for this whole list — false (not omitted) when no
        // single programId was requested, since the umbrella flag is scoped per program type and
        // there's no one type to resolve across a cross-program listing.
        programId ? this.service.isSessionReportEnabled(Number(programId)) : Promise.resolve(false),
      ]);
      await this.responseService.success(res, ONLINE_SESSION_RESPONSE.LIST_RETRIEVED, {
        ...result,
        data: result.data.map((session) =>
          this.toResponse(
            session,
            communicationsBySession.get(Number(session.id)),
            attendanceBySession.get(Number(session.id)),
            pendingJobBySession.get(Number(session.id)) ?? null,
          ),
        ),
        tableHeaders: ONLINE_SESSION_LIST_TABLE_HEADERS,
        isSessionReportEnabled,
      });
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Get(':id')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.GET })
  @ApiParam({ name: 'id', type: Number })
  async findOne(
    @Param('id', ParseIntPipe) id: number,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const data = await this.service.findOne(id);
      const rmContactId = this.isRelationalManager(req) ? req.user?.id : undefined;
      const [communicationsBySession, attendanceBySession, pendingJobBySession] = await Promise.all([
        this.service.getSessionBulkCommunications([
          {
            id: Number(data.id),
            programId: data.programId != null ? Number(data.programId) : null,
          },
        ]),
        this.service.getSessionAttendanceSummary([data], rmContactId),
        this.service.getPendingJobsForSessions([
          { id: Number(data.id), createdBy: data.createdBy ?? null },
        ]),
      ]);
      await this.responseService.success(
        res,
        ONLINE_SESSION_RESPONSE.RETRIEVED,
        this.toResponse(
          data,
          communicationsBySession.get(Number(data.id)),
          attendanceBySession.get(Number(data.id)),
          pendingJobBySession.get(Number(data.id)) ?? null,
        ),
      );
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Put(':id')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.UPDATE })
  @ApiParam({ name: 'id', type: Number })
  @ApiBody({ type: UpdateOnlineSessionDto })
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: UpdateOnlineSessionDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      dto.updatedBy = req.user?.id ?? dto.updatedBy;
      const data = await this.service.update(id, dto);
      await this.responseService.success(res, ONLINE_SESSION_RESPONSE.UPDATED, this.toResponse(data));
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Patch(':id/status')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.UPDATE_STATUS })
  @ApiParam({ name: 'id', type: Number })
  @ApiBody({ type: UpdateOnlineSessionStatusDto })
  async updateStatus(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: UpdateOnlineSessionStatusDto,
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
  ) {
    try {
      const data = await this.service.updateStatus(id, dto.status, req.user?.id);
      await this.responseService.success(res, ONLINE_SESSION_RESPONSE.STATUS_UPDATED, this.toResponse(data));
    } catch (error) {
      handleControllerError(res, error);
    }
  }

  @Delete(':id')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: ONLINE_SESSION_SWAGGER.DELETE })
  @ApiParam({ name: 'id', type: Number })
  async remove(@Param('id', ParseIntPipe) id: number, @Req() req: AuthenticatedRequest, @Res() res: Response) {
    try {
      await this.service.remove(id, req.user?.id);
      await this.responseService.success(res, ONLINE_SESSION_RESPONSE.DELETED);
    } catch (error) {
      handleControllerError(res, error);
    }
  }
}
