import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ProgramSession } from 'src/common/entities';
import { AppLoggerService } from 'src/common/services/logger.service';
import { ModeOfOperationEnum } from 'src/common/enum/mode-of-operation.enum';
import { SessionCommunicationPurposeEnum } from 'src/common/enum/session-communication-purpose.enum';
import { QUEUE_CONSTANTS } from 'src/queue/constants/queue.constants';
import { SessionCommunicationTriggerQueueMessage } from 'src/queue/interfaces/queue-message.interface';
import { AwsSchedulerService } from 'src/aws-scheduler/aws-scheduler.service';
import {
  getSessionCommunicationSchedulerConfig,
  isSessionCommunicationSchedulerEnabled,
} from 'src/common/config/session-communication-scheduler.config';
import {
  SESSION_COMMUNICATION_SCHEDULER,
  SESSION_COMMUNICATION_SCHEDULER_LOG,
  buildWelcomeScheduleName,
  buildCompletionScheduleName,
} from 'src/common/constants/session-communication-scheduler.constants';

/**
 * Registers the EventBridge one-time schedules that auto-trigger the two program-level
 * session communications:
 *   • Welcome    → WELCOME_LEAD_MINUTES (60) before the program's FIRST online session start.
 *   • Completion → when the program completes = the LAST online session's end.
 * Each schedule delivers a `session-communication-trigger` message into the shared event
 * SQS queue; SessionCommunicationTriggerProcessor performs the send at fire time.
 *
 * Best-effort — swallows/logs failures so scheduling never breaks the session lifecycle;
 * disabled/unconfigured → no-op. Idempotent (deterministic per-program schedule names).
 */
@Injectable()
export class SessionCommunicationSchedulerService {
  constructor(
    @InjectRepository(ProgramSession)
    private readonly sessionRepo: Repository<ProgramSession>,
    private readonly awsScheduler: AwsSchedulerService,
    private readonly logger: AppLoggerService,
  ) {}

  /**
   * (Re)compute and upsert the Welcome + Completion schedules for a program from its online
   * sessions. Past fire times are cleared (deleted) rather than scheduled.
   */
  async syncProgramSchedules(programId: number, actorUserId?: number): Promise<void> {
    this.logger.log(
      `[AWS-SCHEDULER] syncProgramSchedules entry (program ${programId}); enabled=${isSessionCommunicationSchedulerEnabled()}, targetQueueArn=${getSessionCommunicationSchedulerConfig().targetQueueArn ?? 'MISSING'}, executionRoleArn=${getSessionCommunicationSchedulerConfig().executionRoleArn ?? 'MISSING'}`,
    );
    if (!this.ensureReady(programId)) {
      this.logger.log(`[AWS-SCHEDULER] ensureReady=false for program ${programId} — not scheduling`);
      return;
    }

    const sessions = await this.sessionRepo.find({
      where: { programId, modeOfOperation: ModeOfOperationEnum.ONLINE },
      order: { startsAt: 'ASC', displayOrder: 'ASC', id: 'ASC' },
      select: ['id', 'startsAt', 'endsAt'],
    });

    if (sessions.length === 0) {
      this.logger.log(SESSION_COMMUNICATION_SCHEDULER_LOG.NO_ONLINE_SESSIONS(programId));
      // No online sessions → make sure no stale schedules survive.
      await this.deleteSchedule(buildWelcomeScheduleName(programId));
      await this.deleteSchedule(buildCompletionScheduleName(programId));
      return;
    }

    const first = sessions[0];
    const last = sessions[sessions.length - 1];
    this.logger.log(
      `[AWS-SCHEDULER] program ${programId}: ${sessions.length} online session(s); ` +
        `first session ${first.id} starts ${first.startsAt ? new Date(first.startsAt).toISOString() : 'null'}, ` +
        `last session ${last.id} ends ${last.endsAt ? new Date(last.endsAt).toISOString() : 'null'}`,
    );

    // Welcome: WELCOME_LEAD_MINUTES before the first session start.
    const welcomeFireAt = first.startsAt
      ? new Date(
          new Date(first.startsAt).getTime() -
            SESSION_COMMUNICATION_SCHEDULER.WELCOME_LEAD_MINUTES * 7 * 1000,
        )
      : null;
    await this.syncOne(
      buildWelcomeScheduleName(programId),
      welcomeFireAt,
      programId,
      SessionCommunicationPurposeEnum.WELCOME,
      actorUserId,
    );

    // Completion: when the program completes = the last session's end (fallback start).
    const completionFireAt = last.endsAt
      ? new Date(last.endsAt)
      : last.startsAt
        ? new Date(last.startsAt)
        : null;
    await this.syncOne(
      buildCompletionScheduleName(programId),
      completionFireAt,
      programId,
      SessionCommunicationPurposeEnum.PROGRAM_COMPLETION,
      actorUserId,
    );
  }

  /** Remove both schedules for a program (e.g. program/online sessions deleted). */
  async deleteProgramSchedules(programId: number): Promise<void> {
    if (!isSessionCommunicationSchedulerEnabled()) {
      return;
    }
    await this.deleteSchedule(buildWelcomeScheduleName(programId));
    await this.deleteSchedule(buildCompletionScheduleName(programId));
  }

  /** Upsert a schedule when the fire time is in the future; otherwise delete any stale one. */
  private async syncOne(
    name: string,
    fireAt: Date | null,
    programId: number,
    purpose:
      | SessionCommunicationPurposeEnum.WELCOME
      | SessionCommunicationPurposeEnum.PROGRAM_COMPLETION,
    actorUserId?: number,
  ): Promise<void> {
    if (!fireAt || fireAt.getTime() <= Date.now()) {
      this.logger.log(
        `[AWS-SCHEDULER][${purpose}] fire time ${fireAt ? fireAt.toISOString() : 'null'} is not in the future — clearing schedule "${name}" (program ${programId})`,
      );
      await this.deleteSchedule(name);
      return;
    }

    const cfg = getSessionCommunicationSchedulerConfig();
    const fireAtIso = fireAt.toISOString();
    const message = this.buildMessage(name, fireAtIso, programId, purpose, actorUserId);
    // Console-visible trace of exactly what will land on the event SQS queue and when.
    this.logger.log(
      `[AWS-SCHEDULER][${purpose}] upserting schedule "${name}" (program ${programId}) → group="${cfg.groupName}", targetQueueArn="${cfg.targetQueueArn}", fireAt=${fireAtIso}, payload=${JSON.stringify(message)}`,
    );
    try {
      await this.awsScheduler.upsertOneTimeSchedule({
        name,
        fireAtIso,
        groupName: cfg.groupName,
        target: {
          arn: cfg.targetQueueArn as string,
          roleArn: cfg.executionRoleArn as string,
          input: JSON.stringify(message),
          retry: { maxAttempts: cfg.maxRetryAttempts, maxEventAgeSeconds: cfg.maxEventAgeSeconds },
        },
      });
      this.logger.log(
        `[AWS-SCHEDULER][${purpose}] schedule "${name}" registered on group "${cfg.groupName}" for ${fireAtIso}`);
      this.logger.log(SESSION_COMMUNICATION_SCHEDULER_LOG.CREATED(name, fireAtIso));
    } catch (error) {
      this.logFailure(SESSION_COMMUNICATION_SCHEDULER_LOG.UPSERT_FAILED, name, error);
    }
  }

  private async deleteSchedule(name: string): Promise<void> {
    const groupName = getSessionCommunicationSchedulerConfig().groupName;
    this.logger.log(`[AWS-SCHEDULER] deleting schedule "${name}" from group "${groupName}"`);
    try {
      await this.awsScheduler.deleteSchedule(name, groupName);
      this.logger.log(`[AWS-SCHEDULER] deleted schedule "${name}"`);
    } catch (error) {
      this.logFailure(SESSION_COMMUNICATION_SCHEDULER_LOG.DELETE_FAILED, name, error);
    }
  }

  private buildMessage(
    name: string,
    fireAtIso: string,
    programId: number,
    purpose:
      | SessionCommunicationPurposeEnum.WELCOME
      | SessionCommunicationPurposeEnum.PROGRAM_COMPLETION,
    actorUserId?: number,
  ): SessionCommunicationTriggerQueueMessage {
    return {
      queueType: QUEUE_CONSTANTS.QUEUE_TYPES.EVENT,
      subType: QUEUE_CONSTANTS.EVENT_TYPES.SESSION_COMMUNICATION_TRIGGER,
      timestamp: fireAtIso,
      correlationId: name,
      data: { programId, purpose, actorUserId },
    };
  }

  private ensureReady(programId: number): boolean {
    if (!isSessionCommunicationSchedulerEnabled()) {
      this.logger.log(SESSION_COMMUNICATION_SCHEDULER_LOG.DISABLED, { programId });
      return false;
    }
    const { targetQueueArn, executionRoleArn } = getSessionCommunicationSchedulerConfig();
    if (!targetQueueArn || !executionRoleArn) {
      this.logger.warn(SESSION_COMMUNICATION_SCHEDULER_LOG.MISSING_CONFIG, { programId });
      return false;
    }
    return true;
  }

  private logFailure(message: string, scheduleName: string, error: unknown): void {
    this.logger.error(message, error instanceof Error ? error.stack : '', {
      scheduleName,
      error: error instanceof Error ? error.message : String(error),
    });
  }
}
