import type { OutboxMessageId } from "@awp/contracts";
import type { Clock } from "./ports/runtime.js";
import {
  OutboxConsumerReceiptConflictError,
  type ApplicationTransaction,
  type OutboxMessage,
  type UnitOfWork,
} from "./ports/repositories.js";

export interface OutboxPublisher {
  publish(message: OutboxMessage): Promise<void>;
}

export interface OutboxDispatchResult {
  readonly attempted: number;
  readonly published: number;
  readonly failed: number;
}

export class OutboxDispatcher {
  constructor(
    private readonly uow: UnitOfWork,
    private readonly publisher: OutboxPublisher,
    private readonly clock: Clock,
  ) {}

  async dispatchPending(limit = 100): Promise<OutboxDispatchResult> {
    const messages = await this.uow.transaction((tx) => tx.outbox.listPending(limit));
    let published = 0;
    let failed = 0;
    for (const message of messages) {
      await this.uow.transaction((tx) => tx.outbox.recordAttempt(message.id));
      try {
        await this.publisher.publish(message);
        await this.uow.transaction((tx) =>
          tx.outbox.markPublished(message.id, this.clock.now().toISOString()),
        );
        published += 1;
      } catch {
        failed += 1;
      }
    }
    return { attempted: messages.length, published, failed };
  }
}

export interface OutboxConsumeResult<T> {
  readonly processed: boolean;
  readonly result?: T;
}

export class IdempotentOutboxConsumer {
  constructor(
    private readonly uow: UnitOfWork,
    private readonly clock: Clock,
  ) {}

  async consume<T>(
    consumerId: string,
    messageId: OutboxMessageId,
    effect: (tx: ApplicationTransaction) => Promise<T>,
  ): Promise<OutboxConsumeResult<T>> {
    if (!consumerId.trim()) throw new Error("Outbox consumerId must not be empty");
    try {
      return await this.uow.transaction(async (tx) => {
        const existing = await tx.outboxConsumerReceipts.get(consumerId, messageId);
        if (existing) return { processed: false };
        const result = await effect(tx);
        await tx.outboxConsumerReceipts.insert({
          consumerId,
          messageId,
          processedAt: this.clock.now().toISOString(),
        });
        return { processed: true, result };
      });
    } catch (error) {
      if (error instanceof OutboxConsumerReceiptConflictError) return { processed: false };
      throw error;
    }
  }
}
