import { Injectable } from '@nestjs/common';
import { AppLoggerService } from 'src/common/services/logger.service';
import { OnlineSessionService } from 'src/online-session/services/online-session.service';
import { ProgramSessionService } from 'src/program-session/program-session.service';
import { ParticipantRole } from 'src/common/enum/participant-role.enum';
import { JoinLinkGenerationStatus } from 'src/common/enum/join-link-generation-status.enum';
import { ExportJobStatus } from 'src/common/enum/export-job-status.enum';
import { GenerateForSessionOptions } from './interfaces/join-link-generation.interface';
import { JOIN_LINK_GENERATION } from 'src/common/constants/join-link-generation.constants';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { InifniInternalServerErrorException } from 'src/common/exceptions/infini-internalservererror-exception';

/** Bulk registration for one session + lifecycle status. */
@Injectable()
export class JoinLinkGenerationService {
  constructor(
    private readonly onlineSessionService: OnlineSessionService,
    private readonly programSessionService: ProgramSessionService,
    private readonly logger: AppLoggerService,
  ) {}

  /**
   * Atomically claims the session (SCHEDULED/FAILED, or stale IN_PROGRESS) →
   * IN_PROGRESS, runs the bulk registration, then marks COMPLETED; on failure
   * marks FAILED and rethrows so the SQS message is retried. If the claim is
   * lost, another worker (the AWS event, a duplicate delivery, or the fallback
   * cron) already owns the run, so we no-op and return — this is the single gate
   * that keeps the event and the cron from ever double-running a session.
   * bulkRegister itself is also idempotent (already-registered pairs are skipped;
   * the registration↔session unique index blocks duplicates) as a second guard.
   */
  async generateForSession(
    programSessionId: number,
    opts: GenerateForSessionOptions = {},
  ): Promise<void> {
    const staleBefore = new Date(Date.now() - JOIN_LINK_GENERATION.STALE_MS);
    const claimed = await this.programSessionService.claimLinkGeneration(
      programSessionId,
      staleBefore,
    );
    if (!claimed) {
      this.logger.log(
        `join-link generation skipped; session already claimed by another worker: ` +
          `programSessionId=${programSessionId}`,
      );
      return;
    }

    try {
      const result = await this.onlineSessionService.bulkRegister(
        {
          sessionId: programSessionId,
          role: (opts.role as ParticipantRole) ?? ParticipantRole.ATTENDEE,
          batchSize: opts.batchSize,
        },
        opts.actorUserId,
      );

      this.logger.log(
        `join-link generation dispatched: programSessionId=${programSessionId}, ` +
          `jobId=${result.jobId}, total=${result.total}`,
      );

      // bulkRegister is fire-and-forget (returns a jobId; the work runs in the
      // background). Wait for the job's ACTUAL terminal state before marking the
      // session — a COMPLETED stamped on dispatch would hide a failed run from
      // the fallback cron (COMPLETED is excluded from the sweep), so it could
      // never be retried.
      const outcome = await this.waitForJobOutcome(result.jobId);

      if (!outcome) {
        // Still running past the poll cap (unusually large job). Leave the
        // session IN_PROGRESS so the stale-reclaim backstop re-attempts it if it
        // never finishes; a duplicate attempt is safe (bulkRegister is idempotent).
        this.logger.warn(
          `join-link generation still running past poll cap; leaving IN_PROGRESS: ` +
            `programSessionId=${programSessionId}, jobId=${result.jobId}`,
        );
        return;
      }

      if (outcome === ExportJobStatus.FAILED) {
        // Surface as an error so the catch marks FAILED (claimable by the cron /
        // SQS redrive) and rethrows for the message to be retried.
        throw new InifniInternalServerErrorException(
          ERROR_CODES.JOIN_LINK_GENERATION_FAILED,
          null,
          null,
          String(programSessionId),
        );
      }

      await this.programSessionService.markLinkGenerationStatus(
        programSessionId,
        JoinLinkGenerationStatus.COMPLETED,
      );
      this.logger.log(
        `join-link generation completed: programSessionId=${programSessionId}, ` +
          `jobId=${result.jobId}`,
      );
    } catch (error) {
      await this.programSessionService.markLinkGenerationStatus(
        programSessionId,
        JoinLinkGenerationStatus.FAILED,
      );
      throw error;
    }
  }

  /**
   * Polls the bulk job until it reaches a terminal state (COMPLETED / FAILED) or
   * the poll cap elapses. Returns the terminal status, or null if still running
   * past the cap. The job runs in this same process (setImmediate), so awaiting
   * here lets it progress; the atomic claim keeps any redelivered message safe.
   */
  private async waitForJobOutcome(jobId: number): Promise<ExportJobStatus | null> {
    for (let attempt = 0; attempt < JOIN_LINK_GENERATION.JOB_POLL_MAX_ATTEMPTS; attempt++) {
      const status = await this.onlineSessionService.getBulkRegistrationStatus(jobId);
      if (
        status.status === ExportJobStatus.COMPLETED ||
        status.status === ExportJobStatus.FAILED
      ) {
        return status.status;
      }
      await this.delay(JOIN_LINK_GENERATION.JOB_POLL_INTERVAL_MS);
    }
    return null;
  }

  private delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}
