import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, In, Repository } from 'typeorm';
import { ProgramUserAttendance, ProgramRegistration, BackgroundJob, ProgramSession } from 'src/common/entities';
import { ExportJobStatus } from 'src/common/enum/export-job-status.enum';
import { AppLoggerService } from 'src/common/services/logger.service';
import { handleKnownErrors } from 'src/common/utils/handle-error.util';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { InifniNotFoundException } from 'src/common/exceptions/infini-notfound-exception';
import { GetAttendanceListDto } from './dto/get-attendance-list.dto';
import {
  QR_EXCLUDED_STATUSES,
  QR_PAID_STATUSES,
  REG_SELECT_FIELDS,
  RM_USER_SELECT_FIELDS,
  PROG_SELECT_FIELDS,
  SESSION_SELECT_FIELDS,
  CHECKED_IN_BY_USER_SELECT_FIELDS,
  REG_USER_SELECT_FIELDS,
} from './qr-attendance.constants';

@Injectable()
export class QrAttendanceRepository {
  constructor(
    @InjectRepository(ProgramUserAttendance)
    private readonly attendanceRepo: Repository<ProgramUserAttendance>,
    @InjectRepository(ProgramRegistration)
    private readonly registrationRepo: Repository<ProgramRegistration>,
    @InjectRepository(BackgroundJob)
    private readonly asyncJobRepo: Repository<BackgroundJob>,
    @InjectRepository(ProgramSession)
    private readonly sessionRepo: Repository<ProgramSession>,
    private readonly logger: AppLoggerService,
  ) {}

  // ─── Session ────────────────────────────────────────────────────────────────

  /** Lightweight lock-state lookup — used to block check-in writes once a Coordinator has locked the session. */
  async findSessionLockStatus(sessionId: number): Promise<{ isAttendanceLocked: boolean } | null> {
    try {
      return await this.sessionRepo.findOne({
        where: { id: sessionId },
        select: ['id', 'isAttendanceLocked'],
      });
    } catch (error) {
      this.logger.error('Error finding session lock status', (error as Error)?.stack, { sessionId });
      handleKnownErrors(ERROR_CODES.PROGRAM_SESSION_FIND_BY_ID_FAILED, error);
    }
  }

  // ─── Registration ─────────────────────────────────────────────────────────────

  async findRegistrationById(registrationId: number): Promise<ProgramRegistration | null> {
    try {
      return await this.registrationRepo.findOne({
        where: { id: registrationId },
        relations: ['user', 'program', 'paymentDetails', 'rmContactUser'],
      });
    } catch (error) {
      this.logger.error('Error finding registration', (error as Error)?.stack, { registrationId });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  async findRegistrationsWithoutQr(programId: number): Promise<ProgramRegistration[]> {
    try {
      return await this.registrationRepo
        .createQueryBuilder('reg')
        .leftJoin('reg.user', 'user')
        .addSelect([...REG_USER_SELECT_FIELDS])
        .leftJoin('reg.rmContactUser', 'rmContactUser')
        .addSelect([...RM_USER_SELECT_FIELDS])
        .leftJoin('reg.program', 'prog')
        .addSelect(['prog.id', 'prog.requiresPayment'])
        .where('reg.program_id = :programId', { programId })
        .andWhere('reg.registration_status NOT IN (:...excludedStatuses)', { excludedStatuses: [...QR_EXCLUDED_STATUSES] })
        .andWhere(
          `(
            prog.requires_payment = false
            OR reg.is_free_seat = true
            OR EXISTS (
              SELECT 1 FROM hdb_registration_payment_detail pd
              WHERE pd.registration_id = reg.id
              AND pd.payment_status IN (:...paidStatuses)
              AND pd.deleted_at IS NULL
            )
          )`,
          { paidStatuses: [...QR_PAID_STATUSES] },
        )
        .andWhere(qb => {
          const sub = qb
            .subQuery()
            .select('pua.registration_id')
            .from(ProgramUserAttendance, 'pua')
            .getQuery();
          return 'reg.id NOT IN ' + sub;
        })
        .getMany();
    } catch (error) {
      this.logger.error('Error finding registrations without QR', (error as Error)?.stack, { programId });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  // ─── Attendance lookups ───────────────────────────────────────────────────────

  async findByRegistrationId(registrationId: number, manager?: EntityManager): Promise<ProgramUserAttendance | null> {
    try {
      const repo = manager ? manager.getRepository(ProgramUserAttendance) : this.attendanceRepo;
      return await repo.findOne({ where: { registrationId } });
    } catch (error) {
      this.logger.error('Error finding attendance by registration', (error as Error)?.stack, { registrationId });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  async findBySeqNumber(registrationSeqNumber: string, manager?: EntityManager): Promise<ProgramUserAttendance | null> {
    try {
      const repo = manager ? manager.getRepository(ProgramUserAttendance) : this.attendanceRepo;
      return await repo.findOne({ where: { registrationSeqNumber } });
    } catch (error) {
      this.logger.error('Error finding attendance by seq number', (error as Error)?.stack, { registrationSeqNumber });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  async findById(id: number): Promise<ProgramUserAttendance> {
    try {
      const attendance = await this.attendanceRepo
        .createQueryBuilder('pa')
        .leftJoin('pa.user', 'u')
        .addSelect(['u.id', 'u.fullName', 'u.orgUsrName'])
        .leftJoinAndMapOne('pa.session', ProgramSession, 's', 's.id = pa.session_id')
        .addSelect([...SESSION_SELECT_FIELDS])
        .where('pa.id = :id', { id })
        .getOne();
      if (!attendance) {
        throw new InifniNotFoundException(ERROR_CODES.PROGRAM_ATTENDANCE_NOTFOUND, null, null, id.toString());
      }
      return attendance;
    } catch (error) {
      if (error instanceof InifniNotFoundException) throw error;
      this.logger.error('Error finding attendance by id', (error as Error)?.stack, { id });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  async findList(query: GetAttendanceListDto): Promise<[ProgramUserAttendance[], number]> {
    try {
      const qb = this.attendanceRepo.createQueryBuilder('pa')
        .leftJoin('pa.registration', 'reg')
        .addSelect([...REG_SELECT_FIELDS])
        .leftJoin('reg.rmContactUser', 'rmContactUser')
        .addSelect([...RM_USER_SELECT_FIELDS])
        .leftJoin('pa.program', 'prog')
        .addSelect([...PROG_SELECT_FIELDS])
        .leftJoinAndMapOne('pa.session', ProgramSession, 's', 's.id = pa.session_id')
        .addSelect([...SESSION_SELECT_FIELDS])
        .leftJoin('pa.checkedInByUser', 'checkedInByUser')
        .addSelect([...CHECKED_IN_BY_USER_SELECT_FIELDS]);

      if (query.sessionId) qb.andWhere('pa.session_id = :sessionId', { sessionId: query.sessionId });
      if (query.programId) qb.andWhere('pa.program_id = :programId', { programId: query.programId });
      if (query.isAttended === true || query.isAttended === false) qb.andWhere('pa.is_attended = :isAttended', { isAttended: query.isAttended });
      if (query.checkedInByUserId) qb.andWhere('pa.checked_in_by_user_id = :checkedInByUserId', { checkedInByUserId: query.checkedInByUserId });
      if (query.search) {
        qb.andWhere(
          '(pa.full_name ILIKE :search OR pa.email ILIKE :search OR pa.registration_seq_number ILIKE :search)',
          { search: `%${query.search}%` },
        );
      }

      return await qb
        .orderBy('pa.createdAt', 'DESC')
        .take(query.limit ?? 20)
        .skip(query.offset ?? 0)
        .getManyAndCount();
    } catch (error) {
      this.logger.error('Error finding attendance list', (error as Error)?.stack, { query });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  async getAttendanceCounts(programId?: number, sessionId?: number): Promise<{ total: number; checkedIn: number; yetToCheckIn: number }> {
    try {
      const base = this.attendanceRepo.createQueryBuilder('pa');
      if (programId) base.andWhere('pa.program_id = :programId', { programId });
      if (sessionId) base.andWhere('pa.session_id = :sessionId', { sessionId });

      const total = await base.getCount();
      const checkedIn = await base.clone().andWhere('pa.is_attended = true').getCount();

      return { total, checkedIn, yetToCheckIn: total - checkedIn };
    } catch (error) {
      this.logger.error('Error getting attendance counts', (error as Error)?.stack, { programId, sessionId });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  // ─── Attendance writes ────────────────────────────────────────────────────────

  async createAttendance(data: Partial<ProgramUserAttendance>, manager?: EntityManager): Promise<ProgramUserAttendance> {
    try {
      const repo = manager ? manager.getRepository(ProgramUserAttendance) : this.attendanceRepo;
      const attendance = repo.create(data);
      return await repo.save(attendance);
    } catch (error) {
      this.logger.error('Error creating attendance', (error as Error)?.stack);
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_SAVE_FAILED, error);
    }
  }

  async saveAttendance(attendance: ProgramUserAttendance, manager?: EntityManager): Promise<ProgramUserAttendance> {
    try {
      const repo = manager ? manager.getRepository(ProgramUserAttendance) : this.attendanceRepo;
      return await repo.save(attendance);
    } catch (error) {
      this.logger.error('Error saving attendance', (error as Error)?.stack);
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_SAVE_FAILED, error);
    }
  }

  async findAttendanceById(id: number, manager?: EntityManager): Promise<ProgramUserAttendance | null> {
    try {
      const repo = manager ? manager.getRepository(ProgramUserAttendance) : this.attendanceRepo;
      return await repo.findOne({ where: { id } });
    } catch (error) {
      this.logger.error('Error finding attendance', (error as Error)?.stack, { id });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  async findAttendancesByIds(ids: number[], manager?: EntityManager): Promise<ProgramUserAttendance[]> {
    try {
      const repo = manager ? manager.getRepository(ProgramUserAttendance) : this.attendanceRepo;
      const records: ProgramUserAttendance[] = [];
      for (const id of ids) {
        const record = await repo.findOne({ where: { id } });
        if (!record) {
          throw new InifniNotFoundException(ERROR_CODES.PROGRAM_ATTENDANCE_NOTFOUND, null, null, id.toString());
        }
        records.push(record);
      }
      return records;
    } catch (error) {
      if (error instanceof InifniNotFoundException) throw error;
      this.logger.error('Error finding attendances by ids', (error as Error)?.stack, { ids });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  /** Like findAttendancesByIds but tolerant of missing ids (no throw). */
  async findExistingAttendancesByIds(
    ids: number[],
    manager?: EntityManager,
  ): Promise<ProgramUserAttendance[]> {
    try {
      const repo = manager ? manager.getRepository(ProgramUserAttendance) : this.attendanceRepo;
      return await repo.find({ where: { id: In(ids) } });
    } catch (error) {
      this.logger.error('Error finding existing attendances by ids', (error as Error)?.stack, {
        ids,
      });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  // ─── Bulk QR Jobs ─────────────────────────────────────────────────────────────

  async createBulkJob(data: Partial<BackgroundJob>): Promise<BackgroundJob> {
    try {
      const job = this.asyncJobRepo.create(data);
      return await this.asyncJobRepo.save(job);
    } catch (error) {
      this.logger.error('Error creating bulk QR job', (error as Error)?.stack);
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_SAVE_FAILED, error);
    }
  }

  async findBulkJobById(id: number): Promise<BackgroundJob | null> {
    try {
      return await this.asyncJobRepo.findOne({ where: { id } });
    } catch (error) {
      this.logger.error('Error finding bulk QR job', (error as Error)?.stack, { id });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  async updateBulkJob(id: number, updates: Partial<BackgroundJob>): Promise<void> {
    try {
      await this.asyncJobRepo.update(id, updates);
    } catch (error) {
      this.logger.error('Error updating bulk QR job', (error as Error)?.stack, { id });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_SAVE_FAILED, error);
    }
  }

  async updateBulkJobStatus(id: number, status: ExportJobStatus, extra?: Partial<BackgroundJob>): Promise<void> {
    try {
      await this.asyncJobRepo.update(id, {
        status,
        ...(extra ?? {}),
        ...(status === ExportJobStatus.COMPLETED || status === ExportJobStatus.FAILED
          ? { completedAt: new Date() }
          : {}),
      });
    } catch (error) {
      this.logger.error('Error updating bulk QR job status', (error as Error)?.stack, { id, status });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_SAVE_FAILED, error);
    }
  }
}
