import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { ExportJobStatus } from 'src/common/enum/export-job-status.enum';
import { JobTypeEnum } from 'src/common/enum/job-type.enum';
import * as QRCode from 'qrcode';
import { QrAttendanceRepository } from './qr-attendance.repository';
import { AwsS3Service } from 'src/common/services/awsS3.service';
import { ExcelService } from 'src/common/services/excel.service';
import { AppLoggerService } from 'src/common/services/logger.service';
import { handleKnownErrors } from 'src/common/utils/handle-error.util';
import { formatDateIST, formatDateTimeIST } from 'src/common/utils/common.util';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { qrAttendanceConstMessages } from 'src/common/constants/strings-constants';
import { InifniNotFoundException } from 'src/common/exceptions/infini-notfound-exception';
import InifniBadRequestException from 'src/common/exceptions/infini-badrequest-exception';
import { ProgramRegistration, ProgramUserAttendance } from 'src/common/entities';
import { RegistrationStatusEnum } from 'src/common/enum/registration-status.enum';
import { AttendanceSourceEnum } from 'src/common/enum/attendance-source.enum';
import { AttendanceEvent } from 'src/common/interfaces/attendance-event.interface';
import { assertAttendanceNotLocked } from 'src/common/utils/attendance-lock.util';
import { PaymentStatusEnum } from 'src/common/enum/payment-status.enum';
import { GenerateQrDto } from './dto/generate-qr.dto';
import { GenerateBulkQrDto } from './dto/generate-bulk.dto';
import { ScanQrDto } from './dto/scan-qr.dto';
import { ManualCheckinDto } from './dto/manual-checkin.dto';
import { BulkManualCheckinDto } from './dto/bulk-manual-checkin.dto';
import { GetAttendanceListDto } from './dto/get-attendance-list.dto';
import { UndoCheckinDto } from './dto/undo-checkin.dto';
import { AttendanceResponseDto } from './dto/attendance-response.dto';

@Injectable()
export class QrAttendanceService {
  constructor(
    private readonly qrAttendanceRepository: QrAttendanceRepository,
    private readonly awsS3Service: AwsS3Service,
    private readonly excelService: ExcelService,
    private readonly logger: AppLoggerService,
    @InjectDataSource()
    private readonly dataSource: DataSource,
  ) {}

  // ─── Generate QR (single) ─────────────────────────────────────────────────────

  async generateQr(dto: GenerateQrDto) {
    this.logger.log(qrAttendanceConstMessages.GENERATE_QR_REQUEST, { registrationId: dto.registrationId });
    return this.dataSource.transaction(async (manager) => {
      try {
        const registration = await this.qrAttendanceRepository.findRegistrationById(dto.registrationId);
        if (!registration) {
          throw new InifniNotFoundException(ERROR_CODES.PROGRAM_ATTENDANCE_REGISTRATION_NOTFOUND, null, null, dto.registrationId.toString());
        }

        const notExcluded =
          registration.registrationStatus !== RegistrationStatusEnum.REJECTED &&
          registration.registrationStatus !== RegistrationStatusEnum.SAVE_AS_DRAFT;
        const isPaid = registration.paymentDetails?.some(
          (p) =>
            p.paymentStatus === PaymentStatusEnum.ONLINE_COMPLETED ||
            p.paymentStatus === PaymentStatusEnum.OFFLINE_COMPLETED,
        );
        const isEligible =
          notExcluded &&
          (registration.program?.requiresPayment === false ||
            registration.isFreeSeat === true ||
            isPaid === true);
        if (!isEligible) {
          throw new InifniBadRequestException(ERROR_CODES.PROGRAM_ATTENDANCE_REGISTRATION_NOT_ELIGIBLE);
        }

        const existing = await this.qrAttendanceRepository.findByRegistrationId(dto.registrationId, manager);
        if (existing) {
          this.logger.log(qrAttendanceConstMessages.QR_ALREADY_EXISTS, { registrationId: dto.registrationId });
          return existing;
        }

        const result = await this.generateAttendanceRecord(registration, dto.sessionId, dto.createdBy, manager, this.buildFolderName());
        this.logger.log(qrAttendanceConstMessages.QR_GENERATED, { registrationId: dto.registrationId });
        return result;
      } catch (error) {
        if (error instanceof InifniNotFoundException) throw error;
        this.logger.error('Error generating QR', (error as Error)?.stack, { dto });
        handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_QR_TOKEN_GENERATION_FAILED, error);
      }
    });
  }

  // ─── Generate QR (bulk — fires background job, returns immediately) ──────────

  async startBulkGeneration(dto: GenerateBulkQrDto) {
    this.logger.log(qrAttendanceConstMessages.BULK_QR_STARTED, { programId: dto.programId });
    try {
      const folderName = this.buildFolderName();
      const s3Url = this.awsS3Service.getS3Url(folderName);
      const registrations = await this.qrAttendanceRepository.findRegistrationsWithoutQr(dto.programId);
      const job = await this.qrAttendanceRepository.createBulkJob({
        type: JobTypeEnum.BULK_QR_GENERATION,
        status: ExportJobStatus.PROCESSING,
        total: registrations.length,
        generated: 0,
        skipped: 0,
        failed: 0,
        programId: dto.programId,
        folderName,
        s3Url,
        metadata: {
          sessionId: dto.sessionId,
          batchSize: dto.batchSize ?? 10,
        },
      });
      setImmediate(() => this.runBulkGeneration(job.id, registrations, dto, folderName));
      this.logger.log(qrAttendanceConstMessages.BULK_QR_GENERATED, { jobId: job.id, total: registrations.length });
      return { jobId: job.id, status: job.status, total: job.total, folderName, s3Url };
    } catch (error) {
      this.logger.error('Error starting bulk QR generation', (error as Error)?.stack, { dto });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_BULK_GENERATION_FAILED, error);
    }
  }

  async getBulkJobStatus(jobId: number) {
    const job = await this.qrAttendanceRepository.findBulkJobById(jobId);
    if (!job) {
      throw new InifniNotFoundException(ERROR_CODES.PROGRAM_ATTENDANCE_BULK_JOB_NOTFOUND, null, null, jobId.toString());
    }
    this.logger.log(qrAttendanceConstMessages.BULK_QR_STATUS_FETCHED, { jobId });
    return job;
  }

  private async runBulkGeneration(jobId: number, registrations: ProgramRegistration[], dto: GenerateBulkQrDto, folderName: string) {
    let generated = 0, skipped = 0, failed = 0;
    try {
      const BATCH_SIZE = dto.batchSize ?? 10;
      this.logger.log(qrAttendanceConstMessages.BULK_QR_START, { jobId, total: registrations.length, batchSize: BATCH_SIZE });

      for (let i = 0; i < registrations.length; i += BATCH_SIZE) {
        const batch = registrations.slice(i, i + BATCH_SIZE);
        await Promise.all(batch.map(async (registration) => {
          try {
            await this.dataSource.transaction(async (manager) => {
              const existing = await this.qrAttendanceRepository.findByRegistrationId(registration.id, manager);
              if (existing) { skipped++; return; }
              await this.generateAttendanceRecord(registration, dto.sessionId, undefined, manager, folderName);
              generated++;
            });
          } catch (err) {
            this.logger.error('Bulk QR failed for registration', (err as Error)?.stack, { registrationId: registration.id });
            failed++;
          }
        }));

        await this.qrAttendanceRepository.updateBulkJob(jobId, { generated, skipped, failed });
        this.logger.log(qrAttendanceConstMessages.BULK_QR_BATCH_DONE, {
          jobId,
          batch: Math.floor(i / BATCH_SIZE) + 1,
          processed: Math.min(i + BATCH_SIZE, registrations.length),
          total: registrations.length,
        });
      }

      await this.qrAttendanceRepository.updateBulkJobStatus(jobId, ExportJobStatus.COMPLETED, { generated, skipped, failed });
      this.logger.log(qrAttendanceConstMessages.BULK_QR_DONE, { jobId, generated, skipped, failed });
    } catch (error) {
      await this.qrAttendanceRepository.updateBulkJobStatus(jobId, ExportJobStatus.FAILED, { generated, skipped, failed });
      this.logger.error('Bulk QR generation job failed', (error as Error)?.stack, { jobId });
    }
  }

  // ─── Scan QR ─────────────────────────────────────────────────────────────────

  async scanQr(dto: ScanQrDto) {
    this.logger.log(qrAttendanceConstMessages.CHECKIN_REQUEST, { registrationId: dto.registrationId });
    return this.dataSource.transaction(async (manager) => {
      try {
        const attendance = await this.qrAttendanceRepository.findByRegistrationId(dto.registrationId, manager);
        if (!attendance) {
          throw new InifniBadRequestException(ERROR_CODES.PROGRAM_ATTENDANCE_INVALID_QR_TOKEN);
        }
        // Self-scan has no role context — always blocked once the Coordinator locks the session.
        await this.assertSessionNotLocked(attendance.sessionId, undefined);

        if (attendance.isAttended) {
          this.logger.log(qrAttendanceConstMessages.ALREADY_ATTENDED, { registrationId: dto.registrationId });
          return attendance;
        }

        attendance.isAttended = true;
        attendance.isManuallyCheckedIn = dto.isManuallyCheckedIn ?? false;
        attendance.checkedInAt = new Date();
        attendance.checkedInByUserId = dto.checkedInByUserId ?? null;
        this.appendAttendanceEvent(
          attendance,
          AttendanceSourceEnum.QR_SCAN,
          dto.checkedInByUserId ?? null,
        );

        const result = await this.qrAttendanceRepository.saveAttendance(attendance, manager);
        this.logger.log(qrAttendanceConstMessages.CHECKIN_SUCCESS, { registrationId: dto.registrationId });
        return result;
      } catch (error) {
        if (error instanceof InifniBadRequestException) throw error;
        this.logger.error('Error scanning QR', (error as Error)?.stack);
        handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_SAVE_FAILED, error);
      }
    });
  }

  // ─── Manual Check-in ─────────────────────────────────────────────────────────

  async manualCheckin(dto: ManualCheckinDto) {
    this.logger.log(qrAttendanceConstMessages.MANUAL_CHECKIN_REQUEST, { registrationId: dto.registrationId });
    return this.dataSource.transaction(async (manager) => {
      try {
        const attendance = await this.qrAttendanceRepository.findByRegistrationId(dto.registrationId, manager);
        if (!attendance) {
          throw new InifniNotFoundException(ERROR_CODES.PROGRAM_ATTENDANCE_NOTFOUND, null, null, dto.registrationId.toString());
        }
        await this.assertSessionNotLocked(dto.sessionId ?? attendance.sessionId, dto.roles);

        attendance.isAttended = true;
        attendance.isManuallyCheckedIn = true;
        attendance.checkedInAt = new Date();
        attendance.checkedInByUserId = dto.checkedInByUserId ?? null;
        if (dto.sessionId) attendance.sessionId = dto.sessionId;
        this.appendAttendanceEvent(
          attendance,
          AttendanceSourceEnum.MANUAL_ADMIN,
          dto.checkedInByUserId ?? null,
        );

        const result = await this.qrAttendanceRepository.saveAttendance(attendance, manager);
        this.logger.log(qrAttendanceConstMessages.MANUAL_CHECKIN_SUCCESS, { registrationId: dto.registrationId });
        return result;
      } catch (error) {
        if (error instanceof InifniNotFoundException) throw error;
        this.logger.error('Error manual check-in', (error as Error)?.stack, { dto });
        handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_SAVE_FAILED, error);
      }
    });
  }

  // ─── Bulk Manual Check-in ────────────────────────────────────────────────────

  async bulkManualCheckin(dto: BulkManualCheckinDto) {
    this.logger.log(qrAttendanceConstMessages.BULK_MANUAL_CHECKIN_REQUEST, {
      attendanceIds: dto.attendanceIds,
    });
    return this.dataSource.transaction(async (manager) => {
      try {
        const records = await this.qrAttendanceRepository.findExistingAttendancesByIds(
          dto.attendanceIds,
          manager,
        );
        let alreadyAttended = 0;
        for (const record of records) {
          await this.assertSessionNotLocked(dto.sessionId ?? record.sessionId, dto.roles);
          if (record.isAttended) alreadyAttended += 1;
          record.isAttended = true;
          record.isManuallyCheckedIn = true;
          record.checkedInAt = new Date();
          record.checkedInByUserId = dto.checkedInByUserId ?? null;
          if (dto.sessionId) record.sessionId = dto.sessionId;
          this.appendAttendanceEvent(
            record,
            AttendanceSourceEnum.MANUAL_ADMIN,
            dto.checkedInByUserId ?? null,
          );
          await this.qrAttendanceRepository.saveAttendance(record, manager);
        }
        this.logger.log(qrAttendanceConstMessages.BULK_MANUAL_CHECKIN_SUCCESS, {
          updated: records.length,
        });
        return {
          updated: records.length,
          alreadyAttended,
          notFound: dto.attendanceIds.length - records.length,
        };
      } catch (error) {
        this.logger.error('Error bulk manual check-in', (error as Error)?.stack, { dto });
        handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_SAVE_FAILED, error);
      }
    });
  }

  // ─── Undo Check-in ───────────────────────────────────────────────────────────

  async undoCheckin(dto: UndoCheckinDto) {
    this.logger.log(qrAttendanceConstMessages.UNDO_CHECKIN_REQUEST, { attendanceIds: dto.attendanceIds });
    return this.dataSource.transaction(async (manager) => {
      try {
        const records = await this.qrAttendanceRepository.findAttendancesByIds(dto.attendanceIds, manager);
        for (const record of records) {
          record.isAttended = false;
          record.isManuallyCheckedIn = false;
          record.checkedInAt = null;
          record.checkedInByUserId = null;
          await this.qrAttendanceRepository.saveAttendance(record, manager);
        }
        this.logger.log(qrAttendanceConstMessages.UNDO_CHECKIN_SUCCESS, { attendanceIds: dto.attendanceIds });
      } catch (error) {
        if (error instanceof InifniNotFoundException) throw error;
        this.logger.error('Error undoing check-in', (error as Error)?.stack, { dto });
        handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_SAVE_FAILED, error);
      }
    });
  }

  /**
   * Appends an attendance event to the append-only audit log so all sources
   * (QR_SCAN, MANUAL_ADMIN, …) are reflected in program_user_attendance.attendance_events.
   */
  private appendAttendanceEvent(
    attendance: ProgramUserAttendance,
    source: AttendanceSourceEnum,
    performedBy: number | null,
  ): void {
    const event: AttendanceEvent = {
      source,
      occurredAt: new Date().toISOString(),
      performedBy,
    };
    attendance.attendanceEvents = [...(attendance.attendanceEvents ?? []), event];
  }

  // ─── Queries ─────────────────────────────────────────────────────────────────

  async findById(id: number) {
    try {
      const result = await this.qrAttendanceRepository.findById(id);
      this.logger.log(qrAttendanceConstMessages.ATTENDANCE_FETCHED, { id });
      return result;
    } catch (error) {
      if (error instanceof InifniNotFoundException) throw error;
      this.logger.error('Error finding attendance', (error as Error)?.stack, { id });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  async findList(query: GetAttendanceListDto) {
    try {
      if (query.isDownload) {
        return await this.downloadAsExcel(query);
      }

      const [data, total] = await this.qrAttendanceRepository.findList(query);
      const counts = await this.qrAttendanceRepository.getAttendanceCounts(query.programId, query.sessionId);
      this.logger.log(qrAttendanceConstMessages.ATTENDANCE_LIST_FETCHED, { total, programId: query.programId });

      return {
        data: data.map(AttendanceResponseDto.from),
        total,
        limit: query.limit ?? 20,
        offset: query.offset ?? 0,
        statusCounts: [
          { status: 'All', count: counts.total },
          { status: 'Checked In', count: counts.checkedIn },
          { status: 'Yet to Check In', count: counts.yetToCheckIn },
        ],
      };
    } catch (error) {
      this.logger.error('Error finding attendance list', (error as Error)?.stack, { query });
      handleKnownErrors(ERROR_CODES.PROGRAM_ATTENDANCE_GET_FAILED, error);
    }
  }

  /** Blocks the write once a Coordinator has locked the session (see `assertAttendanceNotLocked`). */
  private async assertSessionNotLocked(sessionId: number | null | undefined, roles?: string[]): Promise<void> {
    if (!sessionId) return;
    const session = await this.qrAttendanceRepository.findSessionLockStatus(sessionId);
    if (!session) return;
    assertAttendanceNotLocked(session, roles);
  }

  // ─── Private helpers ─────────────────────────────────────────────────────────

  private async generateAttendanceRecord(
    registration: ProgramRegistration,
    sessionId?: number,
    createdBy?: number,
    manager?: any,
    folderName?: string,
  ) {
    const timestamp = this.buildFolderTimestamp();
    const folder = folderName ?? `assets/qr-attendance-portal/${timestamp}`;
    const qrBuffer = await this.generateQrBuffer(registration.id.toString());
    const s3Key = `${folder}/${registration.id}_${timestamp}.jpeg`;
    const qrUrl = await this.awsS3Service.uploadToS3(s3Key, qrBuffer, 'image/jpeg');

    return await this.qrAttendanceRepository.createAttendance({
      userId: Number(registration.userId),
      registrationId: registration.id,
      registrationSeqNumber: registration.registrationSeqNumber,
      programId: Number(registration.programId) || undefined,
      sessionId: sessionId ?? undefined,
      uuid: registration.user?.['uuid'] ?? undefined,
      status: registration['registrationStatus'] ?? undefined,
      fullName: registration.fullName ?? registration.user?.['fullName'] ?? undefined,
      email: registration.emailAddress ?? registration.user?.['email'] ?? undefined,
      mobile: registration.mobileNumber ?? registration.user?.['mobile'] ?? undefined,
      rmName: registration.rmContactUser?.['orgUsrName'] ?? undefined,
      otherRmName: registration?.['otherRmName'] ?? undefined,
      qrUrl,
      isAttended: false,
      isManuallyCheckedIn: false,
      createdBy: createdBy ?? undefined,
      updatedBy: createdBy ?? undefined,
    }, manager);
  }

  private async downloadAsExcel(query: GetAttendanceListDto): Promise<{ fileUrl: string }> {
    this.logger.log(qrAttendanceConstMessages.EXCEL_DOWNLOAD_STARTED, { programId: query.programId });
    const downloadQuery = { ...query, limit: 10000, offset: 0, isDownload: false };
    const [data] = await this.qrAttendanceRepository.findList(downloadQuery);

    const excelData = data.map((item, index) => ({
      'S.No.': index + 1,
      'Seq Number': item.registrationSeqNumber ?? '',
      'Full Name': item.registration?.fullName ?? item.fullName ?? '',
      'Email': item.registration?.emailAddress ?? item.email ?? '',
      'Mobile': item.registration?.mobileNumber ?? item.mobile ?? '',
      'Gender': item.registration?.gender ?? '',
      'Date of Birth': formatDateIST(item.registration?.dob),
      'City': item.registration?.city ?? '',
      'Country': item.registration?.countryName ?? '',
      'Program Name': item.program?.name ?? '',
      'Session Name': item.session?.name ?? '',
      'Session Start': formatDateTimeIST(item.session?.startsAt),
      'Session End': formatDateTimeIST(item.session?.endsAt),
      'Registration Date': formatDateTimeIST(item.registration?.registrationDate),
      'Registration Status': item.registration?.registrationStatus ?? item.status ?? '',
      'Check-in Status': item.isAttended ? 'Checked In' : 'Yet to Check In',
      'Manual Check-in': item.isManuallyCheckedIn ? 'Yes' : 'No',
      'Checked In At': formatDateTimeIST(item.checkedInAt),
      'Checked In By': item.checkedInByUser?.orgUsrName ?? item.checkedInByUser?.fullName ?? item.checkedInByUser?.firstName ?? item.checkedInByUser?.lastName ?? '',
      'RM Name': item.registration?.rmContactUser?.['orgUsrName'] ?? item.rmName ?? '',
      'QR URL': item.qrUrl ?? '',
    }));

    const folder = this.buildFolderTimestamp();
    const filename = `attendance/${folder}/CheckInData_${folder}.xlsx`;

    const fileUrl = await this.excelService.jsonToExcelAndUpload(
      excelData,
      filename,
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      { sheetName: 'Attendance', makeHeaderBold: true, autoWidth: true },
    );
    this.logger.log(qrAttendanceConstMessages.EXCEL_DOWNLOAD_SUCCESS, { fileUrl });

    return { fileUrl };
  }

  private buildFolderTimestamp(): string {
    const now = new Date();
    return now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
  }

  private buildFolderName(): string {
    return `assets/qr-attendance-portal/${this.buildFolderTimestamp()}`;
  }

  private generateQrBuffer(text: string): Promise<Buffer> {
    return new Promise((resolve, reject) => {
      QRCode.toDataURL(text, {
        errorCorrectionLevel: 'H',
        type: 'image/jpeg',
        margin: 1,
        color: { dark: '#000', light: '#FFF' },
      }, (err, url) => {
        if (err) return reject(err);
        const base64 = url.replace(/^data:image\/\w+;base64,/, '');
        resolve(Buffer.from(base64, 'base64'));
      });
    });
  }
}
