import {
  Inject,
  Injectable,
  Logger,
  OnModuleDestroy,
  OnModuleInit,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { SchedulerRegistry } from "@nestjs/schedule";
import { NotificationOutbox } from "./entities/notification-outbox.entity";
import { OutboxRepository } from "./outbox.repository";
import {
  PAYMENT_EVENT_PUBLISHER,
  PaymentEventPublisher,
} from "./publisher/payment-event-publisher";

const RELAY_INTERVAL_NAME = "outbox-relay";
/** Retry backoff: base doubles per attempt, capped. */
const BACKOFF_BASE_MS = 10_000;
const BACKOFF_CAP_MS = 10 * 60_000;

/**
 * Drains the transactional outbox: PENDING rows are published (HTTP now, RabbitMQ later),
 * marked SENT on ack, retried with exponential backoff on failure, and flagged FAILED after
 * OUTBOX_MAX_ATTEMPTS (an alertable condition — delivery is at-least-once, never dropped
 * silently). A crash between commit and publish only delays delivery.
 */
@Injectable()
export class OutboxRelayService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(OutboxRelayService.name);
  private readonly intervalMs: number;
  private readonly maxAttempts: number;
  private readonly batchSize: number;
  private draining = false;

  constructor(
    config: ConfigService,
    private readonly outboxRepository: OutboxRepository,
    private readonly schedulerRegistry: SchedulerRegistry,
    @Inject(PAYMENT_EVENT_PUBLISHER)
    private readonly publisher: PaymentEventPublisher,
  ) {
    this.intervalMs = config.get<number>("notifier.relayIntervalMs") ?? 5_000;
    this.maxAttempts = config.get<number>("notifier.maxAttempts") ?? 10;
    this.batchSize = config.get<number>("notifier.relayBatchSize") ?? 20;
  }

  onModuleInit(): void {
    const interval = setInterval(() => void this.drain(), this.intervalMs);
    this.schedulerRegistry.addInterval(RELAY_INTERVAL_NAME, interval);
  }

  onModuleDestroy(): void {
    if (this.schedulerRegistry.doesExist("interval", RELAY_INTERVAL_NAME)) {
      this.schedulerRegistry.deleteInterval(RELAY_INTERVAL_NAME);
    }
  }

  /** One relay pass; re-entrant ticks are skipped so slow deliveries don't overlap. */
  async drain(): Promise<void> {
    if (this.draining) return;
    this.draining = true;
    try {
      const due = await this.outboxRepository.findDue(this.batchSize);
      for (const row of due) {
        await this.deliver(row);
      }
    } catch (err) {
      this.logger.error(
        `relay pass failed: ${err instanceof Error ? err.message : String(err)}`,
      );
    } finally {
      this.draining = false;
    }
  }

  private async deliver(row: NotificationOutbox): Promise<void> {
    try {
      await this.publisher.publish(row.payload);
      await this.outboxRepository.markSent(row.id);
    } catch (err) {
      const message = this.describeError(err);
      const attempts = row.attempts + 1;
      const exhausted = attempts >= this.maxAttempts;
      const backoffMs = Math.min(
        BACKOFF_BASE_MS * 2 ** row.attempts,
        BACKOFF_CAP_MS,
      );
      await this.outboxRepository.markAttemptFailed(
        row,
        message,
        exhausted ? null : new Date(Date.now() + backoffMs),
        exhausted,
      );
      if (exhausted) {
        // ALERT: a paid order may not be confirmed in the owning app — needs operator action.
        this.logger.error(
          `outbox ${row.id} (${row.eventType} intent=${row.intentId}) FAILED after ${attempts} attempts: ${message}`,
        );
      } else {
        this.logger.warn(
          `outbox ${row.id} delivery attempt ${attempts} failed (retry in ${backoffMs}ms): ${message}`,
        );
      }
    }
  }

  /** Connection failures surface as AggregateError with an empty message — dig out the code. */
  private describeError(err: unknown): string {
    if (err instanceof Error) {
      if (err.message) return err.message;
      const code = (err as { code?: string }).code;
      if (code) return code;
      const inner = (err as { errors?: unknown[] }).errors?.[0];
      if (inner instanceof Error && inner.message) return inner.message;
    }
    return String(err);
  }
}
