import { Injectable } from '@nestjs/common';
import { AppLoggerService } from 'src/common/services/logger.service';
import { QUEUE_CONSTANTS } from 'src/queue/constants/queue.constants';
import { JoinLinkGenerationQueueMessage } from 'src/queue/interfaces/queue-message.interface';
import { AwsSchedulerService } from 'src/aws-scheduler/aws-scheduler.service';
import { getSchedulerConfig, isJoinLinkSchedulerEnabled } from 'src/common/config/join-link-scheduler.config';
import { JOIN_LINK_SCHEDULER_LOG, buildScheduleName } from 'src/common/constants/join-link-scheduler.constants';
import { JoinLinkScheduleInput } from './interfaces/join-link-scheduler.interface';

/**
 * Domain layer over AwsSchedulerService: owns the flag, target/retry config,
 * schedule name, and message shape. Best-effort — swallows/logs failures so
 * scheduling never breaks the session lifecycle; disabled/unconfigured → no-op.
 */
@Injectable()
export class JoinLinkSchedulerService {
  constructor(
    private readonly awsScheduler: AwsSchedulerService,
    private readonly logger: AppLoggerService,
  ) {}

  /** Create the schedule for a session (idempotent — replaces any existing one). */
  async createLinkSchedule(input: JoinLinkScheduleInput): Promise<void> {
    await this.upsert(input, JOIN_LINK_SCHEDULER_LOG.CREATE_FAILED);
  }

  /** Reschedule the session's schedule to a new fire time (creates if absent). */
  async updateLinkSchedule(input: JoinLinkScheduleInput): Promise<void> {
    await this.upsert(input, JOIN_LINK_SCHEDULER_LOG.UPDATE_FAILED);
  }

  /** Delete the session's schedule (on session removal). Missing schedule → no-op. */
  async deleteLinkSchedule(programSessionId: number): Promise<void> {
    if (!isJoinLinkSchedulerEnabled()) return;

    const name = buildScheduleName(programSessionId);
    try {
      await this.awsScheduler.deleteSchedule(name, getSchedulerConfig().groupName);
      this.logger.log(JOIN_LINK_SCHEDULER_LOG.DELETED(name));
    } catch (error) {
      this.logFailure(JOIN_LINK_SCHEDULER_LOG.DELETE_FAILED, name, error);
    }
  }

  /** Shared create/update path — build the message + config and delegate. */
  private async upsert(input: JoinLinkScheduleInput, failLog: string): Promise<void> {
    if (!this.ensureReady(input.programSessionId)) return;

    const name = buildScheduleName(input.programSessionId);
    const cfg = getSchedulerConfig();
    try {
      await this.awsScheduler.upsertOneTimeSchedule({
        name,
        fireAtIso: input.fireAt,
        groupName: cfg.groupName,
        target: {
          arn: cfg.targetQueueArn as string,
          roleArn: cfg.executionRoleArn as string,
          input: JSON.stringify(this.buildMessage(name, input)),
          retry: { maxAttempts: cfg.maxRetryAttempts, maxEventAgeSeconds: cfg.maxEventAgeSeconds },
        },
      });
      this.logger.log(JOIN_LINK_SCHEDULER_LOG.CREATED(name, input.fireAt));
    } catch (error) {
      this.logFailure(failLog, name, error);
    }
  }

  /** The `join-link-generation` queue message the schedule delivers into SQS. */
  private buildMessage(name: string, input: JoinLinkScheduleInput): JoinLinkGenerationQueueMessage {
    return {
      queueType: QUEUE_CONSTANTS.QUEUE_TYPES.EVENT,
      subType: QUEUE_CONSTANTS.EVENT_TYPES.JOIN_LINK_GENERATION,
      timestamp: input.fireAt,
      correlationId: name,
      data: {
        programSessionId: input.programSessionId,
        role: input.role,
        batchSize: input.batchSize,
        actorUserId: input.actorUserId,
      },
    };
  }

  // Flag + required-config guard; false → caller no-ops.
  private ensureReady(programSessionId: number): boolean {
    if (!isJoinLinkSchedulerEnabled()) {
      this.logger.log(JOIN_LINK_SCHEDULER_LOG.DISABLED, { programSessionId });
      return false;
    }
    const { targetQueueArn, executionRoleArn } = getSchedulerConfig();
    if (!targetQueueArn || !executionRoleArn) {
      this.logger.warn(JOIN_LINK_SCHEDULER_LOG.MISSING_CONFIG, { programSessionId });
      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),
    });
  }
}
