import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { DataSource, IsNull } from 'typeorm';
import { RegistrationRepository } from './registration.repository';
import { RegistrationService } from './registration.service';
import { ArchiveRegistrationDto } from './dto/archive-registration.dto';
import { WaitlistReleaseDto } from './dto/waitlist-release.dto';
import { WaitlistReleaseTypeEnum } from 'src/common/enum/waitlist-release-type.enum';
import { WaitlistDenyDto } from './dto/waitlist-deny.dto';
import { SeatTransferDto, SeatTransferAnswerDto, SeatTransferUserDto } from './dto/seat-transfer.dto';
import { AdminSendParentalConsentDto, AdminUploadParentalConsentDto, ParentalConsentListQueryDto, SeekerUploadParentalConsentDto } from './dto/update-registration.dto';
import { ParentalFormStatusEnum } from 'src/common/enum/parental-form-status.enum';
import {
  ARCHIVABLE_STATUSES,
  ARCHIVE_ALLOWED_ROLES,
  ARCHIVE_BLOCKED_PAYMENT_STATUSES,
  WAITLIST_RELEASE_ALLOWED_ROLES,
  WAITLIST_RELEASE_BULK_LIMIT,
  WAITLIST_DENY_ALLOWED_ROLES,
  WAITLIST_DENY_BULK_LIMIT,
  SEAT_TRANSFER_ALLOWED_ROLES,
  SEAT_TRANSFER_ALLOWED_PAYMENT_STATUSES,
} from './registration-action.constants';
import { RegistrationActionKey } from 'src/common/enum/registration-action-key.enum';
import { RegistrationStatusEnum } from 'src/common/enum/registration-status.enum';
import { RegistrationActionValidation, RegistrationActionRow } from 'src/common/interfaces/registration-action.interface';
import { AllocationClearingService } from 'src/common/services/allocation-clearing.service';
import { AppLoggerService } from 'src/common/services/logger.service';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { ERROR_MESSAGES } from 'src/common/i18n/error-messages';
import { CLEARANCE_REASONS } from 'src/common/constants/strings-constants';
import { ProgramQuestion, ProgramRegistration, User } from 'src/common/entities';
import InifniBadRequestException from 'src/common/exceptions/infini-badrequest-exception';
import { InifniNotFoundException } from 'src/common/exceptions/infini-notfound-exception';
import { PaymentStatusEnum } from 'src/common/enum/payment-status.enum';
import { UserRepository } from 'src/user/user.repository';
import { handleKnownErrors } from 'src/common/utils/handle-error.util';
import { PaymentService } from 'src/payment/payment.service';
import { calculateAge, isParentalConsentRequired } from 'src/common/utils/common.util';
import { ZoomRegistrationService } from 'src/zoom/services/zoom-registration.service';
import { RegistrationOnlineSessionActivationSource } from 'src/common/enum/registration-online-session-activation-source.enum';

@Injectable()
export class RegistrationActionService {
  constructor(
    private readonly registrationRepository: RegistrationRepository,
    private readonly allocationClearingService: AllocationClearingService,
    private readonly logger: AppLoggerService,
    private readonly userRepository: UserRepository,
    private readonly dataSource: DataSource,
    private readonly registrationService: RegistrationService,
    @Inject(forwardRef(() => PaymentService)) private readonly paymentService: PaymentService,
    @Inject(forwardRef(() => ZoomRegistrationService))
    private readonly zoomRegistrationService: ZoomRegistrationService,
  ) {}

  async archive(dto: ArchiveRegistrationDto, user: User): Promise<{ processed: number[]; skipped: number[] }> {
    const processed: number[] = [];
    const skipped: number[] = [];

    const registrations = await this.registrationRepository.findRegistrationsByIdsForAction(dto.registrationIds);
    const registrationMap = new Map(registrations.map((r) => [r.id, r]));

    await Promise.all(
      dto.registrationIds.map(async (id) => {
        const registration = registrationMap.get(id) ?? null;
        const validation = this.validateForAction(registration, RegistrationActionKey.ARCHIVE, user);

        if (!validation.valid) {
          this.logger.log(`[RegistrationAction:${RegistrationActionKey.ARCHIVE}] Skipped reg ${id}: ${validation.reason}`);
          skipped.push(id);
          return;
        }

        try {
          if (registration!.allocatedProgramId) {
            await this.allocationClearingService.clearRegistrationAllocations(
              registration!.id,
              user.id,
              CLEARANCE_REASONS.REGISTRATION_CANCELLED(registration!.id),
            );
          }

          await this.registrationRepository.archiveRegistration(id, user.id);

          // Best-effort: release Zoom access on the registrant's upcoming sessions.
          // Never throws, but wrapped defensively so a hypothetical failure here
          // can't get an already-archived registration reported as "skipped".
          try {
            await this.zoomRegistrationService.cascadeTerminalActivation(
              id,
              RegistrationOnlineSessionActivationSource.ARCHIVED,
              user.id,
            );
          } catch (zoomError) {
            this.logger.error(
              `[RegistrationAction:${RegistrationActionKey.ARCHIVE}] Zoom cascade failed for reg ${id}: ${(zoomError as Error)?.message}`,
              (zoomError as Error)?.stack,
            );
          }

          processed.push(id);
        } catch (error) {
          this.logger.error(
            `[RegistrationAction:${RegistrationActionKey.ARCHIVE}] Failed for reg ${id}: ${(error as Error)?.message}`,
            (error as Error)?.stack,
          );
          skipped.push(id);
        }
      }),
    );

    return { processed, skipped };
  }

  async waitlistRelease(dto: WaitlistReleaseDto, user: User): Promise<{ processed: number[]; skipped: number[] }> {
    if (dto.registrationIds.length > WAITLIST_RELEASE_BULK_LIMIT) {
      throw new InifniBadRequestException(
        ERROR_CODES.PROGRAM_REGISTRATION_NOTFOUND,
        null,
        null,
        `Bulk limit exceeded: max ${WAITLIST_RELEASE_BULK_LIMIT} registrations per request`,
      );
    }

    const processed: number[] = [];
    const skipped: number[] = [];

    const registrations = await this.registrationRepository.findRegistrationsByIdsForAction(dto.registrationIds);
    const registrationMap = new Map(registrations.map((r) => [r.id, r]));

    await Promise.all(
      dto.registrationIds.map(async (id) => {
        const registration = registrationMap.get(id) ?? null;
        const validation = this.validateForAction(registration, RegistrationActionKey.WAITLIST_RELEASE, user);

        if (!validation.valid) {
          this.logger.log(`[RegistrationAction:${RegistrationActionKey.WAITLIST_RELEASE}] Skipped reg ${id}: ${validation.reason}`);
          skipped.push(id);
          return;
        }

        try {
          await this.registrationRepository.releaseWaitlistSeat(id, dto.type, user.id);
          processed.push(id);

          if (
            (dto.type === WaitlistReleaseTypeEnum.PAYMENT && registration?.paymentStatus === PaymentStatusEnum.ONLINE_COMPLETED) ||
            registration?.paymentStatus === PaymentStatusEnum.OFFLINE_PENDING
          ) {
            this.paymentService.sendParentalConsentEmailForRegistration(id).catch((err) =>
              this.logger.error(
                `[RegistrationAction:${RegistrationActionKey.WAITLIST_RELEASE}] Failed to send PCF email for reg ${id}: ${(err as Error)?.message}`,
                (err as Error)?.stack,
              ),
            );
          }
        } catch (error) {
          this.logger.error(
            `[RegistrationAction:${RegistrationActionKey.WAITLIST_RELEASE}] Failed for reg ${id}: ${(error as Error)?.message}`,
            (error as Error)?.stack,
          );
          skipped.push(id);
        }
      }),
    );

    return { processed, skipped };
  }

  async waitlistDeny(dto: WaitlistDenyDto, user: User): Promise<{ processed: number[]; skipped: number[] }> {
    if (dto.registrationIds.length > WAITLIST_DENY_BULK_LIMIT) {
      throw new InifniBadRequestException(
        ERROR_CODES.PROGRAM_REGISTRATION_NOTFOUND,
        null,
        null,
        `Bulk limit exceeded: max ${WAITLIST_DENY_BULK_LIMIT} registrations per request`,
      );
    }

    const processed: number[] = [];
    const skipped: number[] = [];

    const registrations = await this.registrationRepository.findRegistrationsByIdsForAction(dto.registrationIds);
    const registrationMap = new Map(registrations.map((r) => [r.id, r]));

    await Promise.all(
      dto.registrationIds.map(async (id) => {
        const registration = registrationMap.get(id) ?? null;
        const validation = this.validateForAction(registration, RegistrationActionKey.WAITLIST_DENY, user);

        if (!validation.valid) {
          this.logger.log(`[RegistrationAction:${RegistrationActionKey.WAITLIST_DENY}] Skipped reg ${id}: ${validation.reason}`);
          skipped.push(id);
          return;
        }

        const hasPaidPayment =
          registration!.paymentStatus === PaymentStatusEnum.ONLINE_COMPLETED ||
          registration!.paymentStatus === PaymentStatusEnum.OFFLINE_COMPLETED ||
          registration!.paymentStatus === PaymentStatusEnum.OFFLINE_PENDING;

        try {
          await this.registrationRepository.denyWaitlistSeat(id, hasPaidPayment, user.id);
          processed.push(id);
        } catch (error) {
          this.logger.error(
            `[RegistrationAction:${RegistrationActionKey.WAITLIST_DENY}] Failed for reg ${id}: ${(error as Error)?.message}`,
            (error as Error)?.stack,
          );
          skipped.push(id);
        }
      }),
    );

    return { processed, skipped };
  }

  async validateSeatTransferUser(
    registrationId: number,
    phone: string,
    email: string,
    countryCode: string,
  ): Promise<{
    user: User;
    countryCode: string | null;
    alreadyRegistered: boolean;
    proxyAllowed: boolean;
    existingRegistrationId: number | null;
    answerFields: Array<{ questionId: number; label: string; type: string; answerLocation: string | null; isEditable: boolean }>;
  }> {
    const registration = await this.registrationRepository.findRegistrationById(registrationId);
    if (!registration) {
      throw new InifniNotFoundException(
        ERROR_CODES.REGISTRATION_NOT_FOUND,
        null,
        null,
        registrationId.toString(),
      );
    }

    const targetUser = await this.lookupUserByPhoneEmail(phone, email, countryCode);

    const programId = registration.programId;
    const proxyAllowed = registration.program?.allowsProxyRegistration ?? false;

    const existing = await this.findExistingActiveRegistrationForTarget(targetUser.id, programId);
    const alreadyRegistered = !!existing;

    if (alreadyRegistered && !proxyAllowed) {
      throw new InifniBadRequestException(ERROR_CODES.SEAT_TRANSFER_TARGET_ALREADY_REGISTERED);
    }

    const paymentCompleted = registration.paymentDetails?.some(
      (p) => p.paymentStatus === PaymentStatusEnum.ONLINE_COMPLETED || p.paymentStatus === PaymentStatusEnum.OFFLINE_COMPLETED,
    ) ?? false;
    const questionMappings = programId ? await this.registrationRepository.findProgramQuestions(programId) : [];
    const answerFields = this.buildSeatTransferAnswerFields(questionMappings, paymentCompleted);

    return {
      user: targetUser,
      countryCode: targetUser.countryCode ?? null,
      alreadyRegistered,
      proxyAllowed,
      existingRegistrationId: existing?.id ?? null,
      answerFields,
    };
  }

  private async findExistingActiveRegistrationForTarget(
    targetUserId: number,
    programId: number | null | undefined,
  ): Promise<ProgramRegistration | null> {
    const INACTIVE_STATUSES = [RegistrationStatusEnum.CANCELLED, RegistrationStatusEnum.ARCHIVED];
    return await this.dataSource
      .getRepository(ProgramRegistration)
      .createQueryBuilder('reg')
      .where('reg.userId = :userId', { userId: targetUserId })
      .andWhere('reg.programId = :programId', { programId })
      .andWhere('reg.registrationStatus NOT IN (:...statuses)', { statuses: INACTIVE_STATUSES })
      .andWhere('reg.deletedAt IS NULL')
      .getOne();
  }

  async transferSeat(
    registrationId: number,
    dto: SeatTransferDto,
    user: User,
  ): Promise<{ registration: ProgramRegistration; targetUser: User }> {
    const actorUserId = user.id;
    this.logger.log(`[SeatTransfer] Initiating seat transfer for registration ${registrationId}`, { registrationId, actorUserId });

    const hasPermission = user.userRoleMaps.some((urm) => SEAT_TRANSFER_ALLOWED_ROLES.includes(urm.role.roleKey));
    if (!hasPermission) {
      throw new InifniBadRequestException(ERROR_CODES.SEAT_TRANSFER_UNAUTHORIZED);
    }

    return await this.dataSource.transaction(async (manager) => {
      try {
        const registration = await this.registrationRepository.findRegistrationById(registrationId);
        if (!registration) {
          throw new InifniNotFoundException(
            ERROR_CODES.REGISTRATION_NOT_FOUND,
            null,
            null,
            registrationId.toString(),
          );
        }

        this.logger.log(`[SeatTransfer] Registration loaded`, {
          registrationId,
          status: registration.registrationStatus,
          seatAllocated: registration.seatAllocated,
          currentUserId: registration.userId,
          programId: registration.programId,
        });

        const isWaitlisted = registration.registrationStatus === RegistrationStatusEnum.WAITLISTED;
        const isSeatAllocated = registration.seatAllocated === true;
        const hasValidPayment = registration.paymentDetails?.some((p) =>
          SEAT_TRANSFER_ALLOWED_PAYMENT_STATUSES.includes(p.paymentStatus),
        );

        if (isWaitlisted || !isSeatAllocated || !hasValidPayment) {
          this.logger.warn(`[SeatTransfer] Precondition failed`, { registrationId, isWaitlisted, isSeatAllocated, hasValidPayment });
          throw new InifniBadRequestException(ERROR_CODES.SEAT_TRANSFER_INVALID_STATUS);
        }

        const targetUser = await this.resolveTransferTargetUser(dto.user);
        this.logger.log(`[SeatTransfer] Target user resolved`, { registrationId, targetUserId: targetUser.id, targetUserName: targetUser.fullName });

        if (dto.answers?.length) {
          const paymentCompleted = registration.paymentDetails?.some(
            (p) =>
              p.paymentStatus === PaymentStatusEnum.ONLINE_COMPLETED ||
              p.paymentStatus === PaymentStatusEnum.OFFLINE_COMPLETED,
          );
          await this.validateTransferAnswers(registration.programId!, dto.answers, paymentCompleted ?? false);
          this.logger.log(`[SeatTransfer] Answers validated`, { registrationId, answerCount: dto.answers.length });
        }

        this.logger.log(`[SeatTransfer] Clearing seat allocations`, { registrationId });
        try {
          await this.allocationClearingService.clearRegistrationAllocations(
            registrationId,
            actorUserId,
            CLEARANCE_REASONS.REGISTRATION_CANCELLED(registrationId),
            manager,
          );
        } catch (clearingError) {
          this.logger.error(`[SeatTransfer] Failed to clear seat allocations`, (clearingError as Error)?.stack, { registrationId });
          throw new InifniBadRequestException(ERROR_CODES.SEAT_TRANSFER_ROOM_UNALLOC_FAILED);
        }
        this.logger.log(`[SeatTransfer] Seat allocations cleared`, { registrationId });

        this.logger.log(`[SeatTransfer] Swapping userId ${registration.userId} → ${targetUser.id}`, { registrationId });
        await this.registrationRepository.updateUserId(
          registrationId,
          targetUser.id,
          actorUserId,
          manager,
        );
        this.logger.log(`[SeatTransfer] userId updated`, { registrationId, newUserId: targetUser.id });

        if (dto.answers?.length) {
          await this.registrationService.applyTransferAnswers(registration, dto.answers, actorUserId, manager);
        }

        const priorTrack = await this.registrationRepository.findLatestSeatTransferTrack(registrationId, manager);
        const sourceUserId = priorTrack?.sourceUserId ?? priorTrack?.originalUserId ?? null;

        await this.registrationRepository.saveSeatTransferTrack(
          {
            programRegistrationId: registrationId,
            originalUserId: registration.userId,
            newUserId: targetUser.id,
            sourceUserId,
            originalUserName: registration.fullName,
            newUserName: targetUser.fullName,
            transferReason: dto.transferReason ?? 'Admin initiated seat transfer',
            adminNotes: dto.adminNotes ?? null,
            actorUserId,
          },
          manager,
        );
        this.logger.log(`[SeatTransfer] Audit track saved`, { registrationId, originalUserId: registration.userId, newUserId: targetUser.id, sourceUserId });

        const finalRegistration = await manager.getRepository(ProgramRegistration).findOne({ where: { id: registrationId } });
        this.logger.log(`[SeatTransfer] Transfer complete`, { registrationId, finalUserId: finalRegistration?.userId });
        return { registration: finalRegistration ?? registration, targetUser };
      } catch (error) {
        this.logger.error(`[SeatTransfer] Transfer failed for registration ${registrationId}`, (error as Error)?.stack, { registrationId, actorUserId });
        handleKnownErrors(ERROR_CODES.SEAT_TRANSFER_FAILED, error);
      }
    });
  }

  private async resolveTransferTargetUser(userDto: SeatTransferUserDto): Promise<User> {
    return await this.userRepository.getUserByField('id', userDto.userId);
  }

  private async lookupUserByPhoneEmail(phone: string, email: string, countryCode: string): Promise<User> {
    const byPhone = await this.userRepository.getUserByPhoneNumber(phone);

    if (byPhone) {
      if (byPhone.email?.toLowerCase().trim() !== email.toLowerCase().trim()) {
        throw new InifniBadRequestException(ERROR_CODES.USER_EMAIL_MISMATCH);
      }
      if (byPhone.countryCode !== countryCode) {
        throw new InifniBadRequestException(ERROR_CODES.USER_PHONE_MISMATCH);
      }
      return byPhone;
    }

    const byEmail = await this.userRepository.getUserByEmail(email);

    if (byEmail) {
      if (byEmail.phoneNumber !== phone) {
        throw new InifniBadRequestException(ERROR_CODES.USER_PHONE_MISMATCH);
      }
      return byEmail;
    }

    throw new InifniNotFoundException(ERROR_CODES.USER_NOTFOUND);
  }

  private static readonly PAYMENT_TABLE_PREFIXES = ['payment', 'invoice'];
  private static readonly PROFORMA_COLUMN_PREFIXES = ['pro_forma'];

  private isAnswerFieldEditable(question: { answerLocation: string | null }, paymentCompleted: boolean): boolean {
    const [tableName, columnName] = (question.answerLocation ?? '').split('.');
    const tableLower = tableName?.toLowerCase() ?? '';
    const columnLower = columnName?.toLowerCase() ?? '';

    const isPaymentLocked = RegistrationActionService.PAYMENT_TABLE_PREFIXES.includes(tableLower);
    const isProformaLocked =
      paymentCompleted && RegistrationActionService.PROFORMA_COLUMN_PREFIXES.some((prefix) => columnLower.startsWith(prefix));

    return !isPaymentLocked && !isProformaLocked;
  }

  private buildSeatTransferAnswerFields(
    questionMappings: ProgramQuestion[],
    paymentCompleted: boolean,
  ): Array<{ questionId: number; label: string; type: string; answerLocation: string | null; isEditable: boolean }> {
    return questionMappings.map((mapping) => ({
      questionId: mapping.question.id,
      label: mapping.question.label,
      type: mapping.question.type,
      answerLocation: mapping.question.answerLocation,
      isEditable: this.isAnswerFieldEditable(mapping.question, paymentCompleted),
    }));
  }

  private async validateTransferAnswers(
    programId: number,
    answers: SeatTransferAnswerDto[],
    paymentCompleted: boolean,
  ): Promise<void> {
    const questionMappings = await this.registrationRepository.findProgramQuestions(programId);
    const questionMap = new Map(questionMappings.map((m) => [m.question.id, m.question]));

    for (const ans of answers) {
      const question = questionMap.get(ans.questionId);
      if (!question) continue;

      if (!this.isAnswerFieldEditable(question, paymentCompleted)) {
        throw new InifniBadRequestException(ERROR_CODES.SEAT_TRANSFER_ANSWER_LOCKED);
      }
    }
  }

  private validateForAction(
    registration: RegistrationActionRow | null,
    actionKey: RegistrationActionKey,
    user: User,
  ): RegistrationActionValidation {
    if (!registration) {
      return this.invalid(ERROR_CODES.PROGRAM_REGISTRATION_NOTFOUND);
    }

    switch (actionKey) {
      case RegistrationActionKey.ARCHIVE: {
        const hasPermission = user.userRoleMaps.some((urm) => ARCHIVE_ALLOWED_ROLES.includes(urm.role.roleKey));
        if (!hasPermission) {
          return this.invalid(ERROR_CODES.REGISTRATION_ARCHIVE_UNAUTHORIZED);
        }

        if (registration.registrationStatus === RegistrationStatusEnum.ARCHIVED) {
          return this.invalid(ERROR_CODES.REGISTRATION_ALREADY_ARCHIVED);
        }

        if (!ARCHIVABLE_STATUSES[actionKey]?.includes(registration.registrationStatus)) {
          return this.invalid(ERROR_CODES.REGISTRATION_ARCHIVE_INVALID_STATUS);
        }

        if (registration.paymentStatus && ARCHIVE_BLOCKED_PAYMENT_STATUSES.includes(registration.paymentStatus)) {
          return this.invalid(ERROR_CODES.REGISTRATION_ARCHIVE_HAS_COMPLETED_PAYMENT);
        }
        break;
      }

      case RegistrationActionKey.WAITLIST_RELEASE: {
        const hasPermission = user.userRoleMaps.some((urm) => WAITLIST_RELEASE_ALLOWED_ROLES.includes(urm.role.roleKey));
        if (!hasPermission) {
          return this.invalid(ERROR_CODES.WAITLIST_ACTION_UNAUTHORIZED);
        }

        if (registration.registrationStatus !== RegistrationStatusEnum.WAITLISTED) {
          return this.invalid(ERROR_CODES.NOT_WAITLISTED);
        }
        break;
      }

      case RegistrationActionKey.WAITLIST_DENY: {
        const hasPermission = user.userRoleMaps.some((urm) => WAITLIST_DENY_ALLOWED_ROLES.includes(urm.role.roleKey));
        if (!hasPermission) {
          return this.invalid(ERROR_CODES.WAITLIST_ACTION_UNAUTHORIZED);
        }

        if (registration.registrationStatus !== RegistrationStatusEnum.WAITLISTED) {
          return this.invalid(ERROR_CODES.NOT_WAITLISTED);
        }
        break;
      }
    }

    return { valid: true };
  }

  private invalid(errorCode: string): RegistrationActionValidation {
    return { valid: false, reason: ERROR_MESSAGES[errorCode], errorCode };
  }

  /**
   * Whether a registration belongs to the given user via ownership OR contact match,
   * mirroring the seeker-facing "ownerOrContact" rule (see user.service.getRegistrationsByUser
   * and registration.repository.findRegistrations). A proxy registration is created with the
   * seeker's email / combined mobile but not necessarily their owner_user_id, so we must also
   * match by email (case-insensitive) and mobile (countryCode + phoneNumber).
   */
  private async registrationBelongsToUser(
    registration: ProgramRegistration,
    userId: number,
  ): Promise<boolean> {
    if (registration.ownerUserId === userId) {
      return true;
    }

    const user = await this.userRepository.getUserByField('id', userId).catch(() => null);
    if (!user) {
      return false;
    }

    const regEmail = registration.emailAddress?.trim().toLowerCase() || null;
    const userEmail = user.email?.trim().toLowerCase() || null;
    if (regEmail && userEmail && regEmail === userEmail) {
      return true;
    }

    const regMobile = registration.mobileNumber || null;
    const userMobile = user.countryCode && user.phoneNumber ? `${user.countryCode}${user.phoneNumber}` : null;
    if (regMobile && userMobile && regMobile === userMobile) {
      return true;
    }

    return false;
  }

  async seekerUploadParentalConsent(
    registrationId: number,
    dto: SeekerUploadParentalConsentDto,
    seekerUserId: number,
  ): Promise<{ message: string; uploadedAt: Date; uploadDeadline: Date | null }> {
    try {
      const registration = await this.registrationRepository.findRegistrationForParentalConsent(registrationId);
      if (!registration) {
        throw new InifniNotFoundException(ERROR_CODES.PROGRAM_REGISTRATION_NOTFOUND, null, null, registrationId.toString());
      }

      const belongsToSeeker = await this.registrationBelongsToUser(registration, seekerUserId);
      if (!belongsToSeeker) {
        throw new InifniBadRequestException(ERROR_CODES.VALIDATION_FAILED, null, null, 'This registration does not belong to the authenticated user');
      }
      if (!registration.parentalFormStatus || registration.parentalFormStatus === ParentalFormStatusEnum.NOT_APPLICABLE) {
        throw new InifniBadRequestException(ERROR_CODES.VALIDATION_FAILED, null, null, 'Parental consent has not been triggered for this registration');
      }
      if (registration.parentalFormStatus === ParentalFormStatusEnum.VERIFIED) {
        throw new InifniBadRequestException(ERROR_CODES.VALIDATION_FAILED, null, null, 'Parental consent form has already been verified and cannot be re-uploaded');
      }
      const deadline = registration.program?.parentalConsentUploadDeadline ?? null;
      if (deadline && new Date() > new Date(deadline)) {
        throw new InifniBadRequestException(ERROR_CODES.VALIDATION_FAILED, null, null, 'Upload deadline has passed');
      }

      const now = new Date();
      await this.registrationRepository.updateRegistration(registrationId, {
        parentalFormPdfUrl: dto.s3Url,
        parentalFormStatus: ParentalFormStatusEnum.SUBMITTED,
        parentalFormUploadedAt: now,
        parentalFormUploadedBy: seekerUserId,
        parentalFormUpdatedAt: now,
      } as any, seekerUserId);

      this.logger.log('[PCF] Seeker uploaded signed consent form', { registrationId });
      return {
        message: 'Parental consent form uploaded successfully. If you uploaded the wrong file, please contact your RM.',
        uploadedAt: now,
        uploadDeadline: deadline,
      };
    } catch (error) {
      this.logger.error('[PCF] seekerUploadParentalConsent error', (error as Error)?.message, { registrationId });
      handleKnownErrors(ERROR_CODES.INTERNAL_SERVER_ERROR, error);
    }
  }

  async adminSendParentalConsentEmail(
    registrationId: number,
    dto: AdminSendParentalConsentDto,
    adminUserId: number,
  ): Promise<{ message: string }> {
    try {
      const registration = await this.registrationRepository.findRegistrationForParentalConsent(registrationId);
      if (!registration) {
        throw new InifniNotFoundException(ERROR_CODES.PROGRAM_REGISTRATION_NOTFOUND, null, null, registrationId.toString());
      }

      const program = registration.program;
      if (!program?.parentalConsentEnabled || !program?.parentalConsentFormContent) {
        throw new InifniBadRequestException(ERROR_CODES.VALIDATION_FAILED, null, null, 'Parental consent is not enabled or form content is missing for this programme');
      }
      if (!isParentalConsentRequired(registration.dob, program)) {
        throw new InifniBadRequestException(ERROR_CODES.VALIDATION_FAILED, null, null, 'Seeker age is not in the parental consent range');
      }
      const hasCompletedPayment = registration.paymentDetails?.some(
        p =>
          p.paymentStatus === PaymentStatusEnum.ONLINE_COMPLETED ||
          p.paymentStatus === PaymentStatusEnum.OFFLINE_COMPLETED ||
          p.paymentStatus === PaymentStatusEnum.OFFLINE_PENDING,
      );
      if (!hasCompletedPayment) {
        throw new InifniBadRequestException(ERROR_CODES.VALIDATION_FAILED, null, null, 'Payment must be completed before sending parental consent email');
      }

      await this.paymentService.sendParentalConsentEmailForRegistration(registrationId, true);

      const now = new Date();
      await this.registrationRepository.updateRegistration(registrationId, {
        parentalFormStatus: ParentalFormStatusEnum.PENDING_UPLOAD,
        parentalFormUpdatedAt: now,
        ...(dto.reason ? { parentalFormAdminNotes: dto.reason } : {}),
      } as any, adminUserId);

      this.logger.log('[PCF] Admin manually sent parental consent email', { registrationId, adminUserId });
      return { message: 'Parental consent email sent successfully' };
    } catch (error) {
      this.logger.error('[PCF] adminSendParentalConsentEmail error', (error as Error)?.message, { registrationId });
      handleKnownErrors(ERROR_CODES.INTERNAL_SERVER_ERROR, error);
    }
  }

  async adminUploadParentalConsent(
    registrationId: number,
    dto: AdminUploadParentalConsentDto,
    adminUserId: number,
  ): Promise<{ message: string }> {
    try {
      const registration = await this.registrationRepository.findRegistrationForParentalConsent(registrationId);
      if (!registration) {
        throw new InifniNotFoundException(ERROR_CODES.PROGRAM_REGISTRATION_NOTFOUND, null, null, registrationId.toString());
      }

      const now = new Date();
      await this.registrationRepository.updateRegistration(registrationId, {
        parentalFormPdfUrl: dto.s3Url,
        parentalFormStatus: ParentalFormStatusEnum.VERIFIED,
        parentalFormUploadedAt: now,
        parentalFormUploadedBy: adminUserId,
        parentalFormUpdatedAt: now,
      } as any, adminUserId);

      this.logger.log('[PCF] Admin uploaded and verified consent form', { registrationId, adminUserId, sourceNote: dto.sourceNote });
      return { message: 'Parental consent form uploaded and verified successfully' };
    } catch (error) {
      this.logger.error('[PCF] adminUploadParentalConsent error', (error as Error)?.message, { registrationId });
      handleKnownErrors(ERROR_CODES.INTERNAL_SERVER_ERROR, error);
    }
  }

  async adminVerifyParentalConsent(registrationId: number, adminUserId: number): Promise<{ message: string }> {
    try {
      const registration = await this.registrationRepository.findRegistrationForParentalConsent(registrationId);
      if (!registration) {
        throw new InifniNotFoundException(ERROR_CODES.PROGRAM_REGISTRATION_NOTFOUND, null, null, registrationId.toString());
      }
      if (registration.parentalFormStatus !== ParentalFormStatusEnum.SUBMITTED) {
        throw new InifniBadRequestException(ERROR_CODES.VALIDATION_FAILED, null, null, 'Only SUBMITTED forms can be verified');
      }

      await this.registrationRepository.updateRegistration(registrationId, {
        parentalFormStatus: ParentalFormStatusEnum.VERIFIED,
        parentalFormUpdatedAt: new Date(),
      } as any, adminUserId);

      this.logger.log('[PCF] Admin verified consent form', { registrationId, adminUserId });
      return { message: 'Parental consent form verified successfully' };
    } catch (error) {
      this.logger.error('[PCF] adminVerifyParentalConsent error', (error as Error)?.message, { registrationId });
      handleKnownErrors(ERROR_CODES.INTERNAL_SERVER_ERROR, error);
    }
  }

  async adminDeleteParentalConsent(registrationId: number, adminUserId: number): Promise<{ message: string }> {
    try {
      const registration = await this.registrationRepository.findRegistrationForParentalConsent(registrationId);
      if (!registration) {
        throw new InifniNotFoundException(ERROR_CODES.PROGRAM_REGISTRATION_NOTFOUND, null, null, registrationId.toString());
      }

      await this.registrationRepository.updateRegistration(registrationId, {
        parentalFormPdfUrl: null,
        parentalFormStatus: ParentalFormStatusEnum.PENDING_UPLOAD,
        parentalFormUploadedAt: null,
        parentalFormUploadedBy: null,
        parentalFormUpdatedAt: new Date(),
      } as any, adminUserId);

      this.logger.log('[PCF] Admin deleted consent record', { registrationId, adminUserId });
      return { message: 'Parental consent record deleted successfully' };
    } catch (error) {
      this.logger.error('[PCF] adminDeleteParentalConsent error', (error as Error)?.message, { registrationId });
      handleKnownErrors(ERROR_CODES.INTERNAL_SERVER_ERROR, error);
    }
  }

  async getParentalConsentList(
    query: ParentalConsentListQueryDto,
    userRoles: string[],
    userId: number,
  ): Promise<{ data: any[]; total: number; limit: number; offset: number }> {
    try {
      const isRM = userRoles.includes('relational_manager') || userRoles.includes('rm');
      const filters = { ...query, rmId: isRM ? userId : query.rmId };
      const { data, total } = await this.registrationRepository.findParentalConsentList(filters);
      const mapped = data.map(reg => ({
        registrationId: reg.id,
        seekerName: reg.fullName,
        age: calculateAge(reg.dob),
        consentStatus: reg.parentalFormStatus,
        dateTriggered: reg.parentalFormUpdatedAt,
        dateLastUploaded: reg.parentalFormUploadedAt,
        uploadedBy: reg.parentalFormUploadedBy,
        programName: reg.program?.name,
      }));
      return { data: mapped, total, limit: query.limit ?? 20, offset: query.offset ?? 0 };
    } catch (error) {
      this.logger.error('[PCF] getParentalConsentList error', (error as Error)?.message);
      handleKnownErrors(ERROR_CODES.INTERNAL_SERVER_ERROR, error);
    }
  }

  async getParentalConsentDetail(registrationId: number): Promise<any> {
    try {
      const registration = await this.registrationRepository.findRegistrationForParentalConsent(registrationId);
      if (!registration) {
        throw new InifniNotFoundException(ERROR_CODES.PROGRAM_REGISTRATION_NOTFOUND, null, null, registrationId.toString());
      }

      return {
        registrationId: registration.id,
        seekerName: registration.fullName,
        age: calculateAge(registration.dob),
        consentStatus: registration.parentalFormStatus,
        dateTriggered: registration.parentalFormUpdatedAt,
        dateLastUploaded: registration.parentalFormUploadedAt,
        uploadedBy: registration.parentalFormUploadedBy,
        uploadDeadline: registration.program?.parentalConsentUploadDeadline ?? null,
        generatedFormUrl: registration.parentalFormGeneratedPdfUrl ?? null,
        formUrl: registration.parentalFormPdfUrl ?? null,
        programName: registration.program?.name,
      };
    } catch (error) {
      this.logger.error('[PCF] getParentalConsentDetail error', (error as Error)?.message, { registrationId });
      handleKnownErrors(ERROR_CODES.INTERNAL_SERVER_ERROR, error);
    }
  }
}
