import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, IsNull } from 'typeorm';
import { ProgramSession, SessionCommunicationStatus, BackgroundJob } from 'src/common/entities';
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 { OnlineTypeEnum } from 'src/common/enum/online-type.enum';
import { ZoomWebinarStatus } from 'src/common/enum/zoom-webinar-status.enum';
import { JoinLinkGenerationStatus } from 'src/common/enum/join-link-generation-status.enum';
import { ExportJobStatus } from 'src/common/enum/export-job-status.enum';
import { SessionCommunicationPurposeEnum } from 'src/common/enum/session-communication-purpose.enum';
import { OnlineSessionListResult } from '../interfaces/online-session.interface';
import { ONLINE_SESSION_LOG } from 'src/common/constants/online-session.constants';

/**
 * Provider-agnostic reads over `program_session`. A session is the same entity
 * regardless of provider, so listing/fetching lives here in the orchestrator —
 * only the mutating, provider-specific work is delegated to a provider.
 */
@Injectable()
export class OnlineSessionRepository {
  constructor(
    @InjectRepository(ProgramSession)
    private readonly sessionRepo: Repository<ProgramSession>,
    @InjectRepository(SessionCommunicationStatus)
    private readonly communicationStatusRepo: Repository<SessionCommunicationStatus>,
    @InjectRepository(BackgroundJob)
    private readonly backgroundJobRepo: Repository<BackgroundJob>,
    private readonly logger: AppLoggerService,
  ) {}

  async save(session: ProgramSession): Promise<ProgramSession> {
    try {
      return await this.sessionRepo.save(session);
    } catch (error) {
      this.logger.error(ONLINE_SESSION_LOG.UPDATE_FAILED, error?.stack, { error, id: session?.id });
      handleKnownErrors(ERROR_CODES.ONLINE_SESSION_OPERATION_FAILED, error);
    }
  }

  // Direct column update of the link-generation status. Cascade-safe: never
  // load-and-save the entity here, which would touch the onlineSession relation.
  async setLinkGenerationStatus(id: number, status: JoinLinkGenerationStatus): Promise<void> {
    try {
      await this.sessionRepo.update({ id }, { linkGenerationStatus: status });
    } catch (error) {
      this.logger.error(ONLINE_SESSION_LOG.UPDATE_FAILED, (error as Error)?.stack, { error, id });
      handleKnownErrors(ERROR_CODES.ONLINE_SESSION_OPERATION_FAILED, error);
    }
  }

  async findById(id: number): Promise<ProgramSession | null> {
    try {
      return await this.sessionRepo.findOne({
        where: { id, deletedAt: IsNull() },
        relations: ['onlineSession'],
      });
    } catch (error) {
      this.logger.error(ONLINE_SESSION_LOG.GET_BY_ID_FAILED, error?.stack, { error, id });
      handleKnownErrors(ERROR_CODES.ONLINE_SESSION_GET_FAILED, error);
    }
  }

  async findAll(
    limit: number,
    offset: number,
    status?: ZoomWebinarStatus,
    programId?: number,
  ): Promise<OnlineSessionListResult> {
    try {
      const sessionQuery = this.sessionRepo
        .createQueryBuilder('session')
        .leftJoinAndSelect('session.onlineSession', 'onlineSession', 'onlineSession.deleted_at IS NULL')
        .where('session.deleted_at IS NULL')
        .andWhere('session.online_type IN (:...types)', {
          types: [OnlineTypeEnum.WEBINAR, OnlineTypeEnum.MEETING],
        });
      if (programId) {
        sessionQuery.andWhere('session.program_id = :programId', { programId });
      }
      if (status) {
        sessionQuery.andWhere('onlineSession.status = :status', { status });
      }
      const [data, total] = await sessionQuery
        .orderBy('session.startsAt', 'DESC')
        .addOrderBy('session.id', 'DESC')
        .take(limit)
        .skip(offset)
        .getManyAndCount();
      return { data, total };
    } catch (error) {
      this.logger.error(ONLINE_SESSION_LOG.LIST_FAILED, error?.stack, { error });
      handleKnownErrors(ERROR_CODES.ONLINE_SESSION_GET_FAILED, error);
    }
  }

  /**
   * Invite/Absent/Value Card bulk-communication status rows for the given sessions, newest
   * first. Cross-domain read of hdb_session_communication_status (owned by session-communication)
   * for a display-only projection on the GET responses; the caller reduces to the latest
   * row per (session, purpose).
   */
  async findSessionBulkStatuses(sessionIds: number[]): Promise<SessionCommunicationStatus[]> {
    if (!sessionIds.length) {
      return [];
    }
    try {
      return await this.communicationStatusRepo
        .createQueryBuilder('status')
        .where('status.session_id IN (:...sessionIds)', { sessionIds })
        .andWhere('status.purpose IN (:...purposes)', {
          purposes: [
            SessionCommunicationPurposeEnum.INVITE,
            SessionCommunicationPurposeEnum.ABSENT,
            SessionCommunicationPurposeEnum.VALUE_CARD,
          ],
        })
        .orderBy('status.created_at', 'DESC')
        .getMany();
    } catch (error) {
      this.logger.error(ONLINE_SESSION_LOG.LIST_FAILED, error?.stack, { error });
      handleKnownErrors(ERROR_CODES.ONLINE_SESSION_GET_FAILED, error);
    }
  }

  /**
   * Program-level bulk-communication status rows (session_id IS NULL) for the given programs,
   * newest first — Welcome and Program Completion. These have no target session, so the caller
   * echoes the status onto every session of the program.
   */
  async findProgramLevelStatuses(programIds: number[]): Promise<SessionCommunicationStatus[]> {
    if (!programIds.length) {
      return [];
    }
    try {
      return await this.communicationStatusRepo
        .createQueryBuilder('status')
        .where('status.program_id IN (:...programIds)', { programIds })
        .andWhere('status.session_id IS NULL')
        .andWhere('status.purpose IN (:...purposes)', {
          purposes: [
            SessionCommunicationPurposeEnum.WELCOME,
            SessionCommunicationPurposeEnum.PROGRAM_COMPLETION,
          ],
        })
        .orderBy('status.created_at', 'DESC')
        .getMany();
    } catch (error) {
      this.logger.error(ONLINE_SESSION_LOG.LIST_FAILED, error?.stack, { error });
      handleKnownErrors(ERROR_CODES.ONLINE_SESSION_GET_FAILED, error);
    }
  }

  /**
   * Still-running background jobs (status PENDING/PROCESSING; any JobTypeEnum) whose
   * `metadata.sessionIds` includes at least one of the given sessions, newest first. Cross-domain
   * read of background_jobs (owned by zoom/qr-attendance) for a display-only "job currently going
   * on" projection on the GET responses; a job can target several sessions at once (e.g. bulk
   * registration across sessions), so scope lives in that jsonb array rather than a dedicated
   * column — the caller reduces to the latest job per session.
   */
  async findPendingJobsBySessionIds(sessionIds: number[]): Promise<BackgroundJob[]> {
    if (!sessionIds.length) {
      return [];
    }
    try {
      const filters = sessionIds.map((sessionId) => JSON.stringify({ sessionIds: [sessionId] }));
      return await this.backgroundJobRepo
        .createQueryBuilder('job')
        .where('job.metadata @> ANY(:filters::jsonb[])', { filters })
        .andWhere('job.status IN (:...statuses)', {
          statuses: [ExportJobStatus.PENDING, ExportJobStatus.PROCESSING],
        })
        .orderBy('job.id', 'DESC')
        .getMany();
    } catch (error) {
      this.logger.error(ONLINE_SESSION_LOG.LIST_FAILED, error?.stack, { error });
      handleKnownErrors(ERROR_CODES.ONLINE_SESSION_GET_FAILED, error);
    }
  }

  /** Online sessions whose end-time reconciliation should re-run (cron fallback). */
  async findCompletedSince(since: Date): Promise<ProgramSession[]> {
    try {
      return await this.sessionRepo
        .createQueryBuilder('session')
        .leftJoinAndSelect(
          'session.onlineSession',
          'onlineSession',
          'onlineSession.deleted_at IS NULL',
        )
        .where('session.deleted_at IS NULL')
        .andWhere('session.online_type IN (:...types)', {
          types: [OnlineTypeEnum.WEBINAR, OnlineTypeEnum.MEETING],
        })
        .andWhere('onlineSession.actual_meeting_ends_at >= :since', { since })
        .getMany();
    } catch (error) {
      this.logger.error(ONLINE_SESSION_LOG.COMPLETED_FETCH_FAILED, error?.stack, { error });
      handleKnownErrors(ERROR_CODES.ONLINE_SESSION_GET_FAILED, error);
    }
  }
}
