import { Injectable, NotFoundException } from '@nestjs/common';
import type { CloudEvent } from '@insignia/iios-contracts';
import { PrismaService } from '../prisma/prisma.service';

type ReplayHandler = (event: CloudEvent) => Promise<void>;

export interface DlqRecordInput {
  sourceType: string;
  sourceId: string;
  failureStage: string;
  errorCode?: string;
  payloadRef?: string;
  scopeId?: string;
  consumerName?: string;
}

/**
 * Dead-letter queue (P9, KG-13). Failures are quarantined here — never silently
 * swallowed — and replayed deterministically: a consumer registers its handler, and
 * replay clears the idempotency claim then re-runs exactly that consumer (siblings
 * stay untouched because the claim ledger makes re-processing a no-op for them).
 */
@Injectable()
export class DlqService {
  private readonly handlers = new Map<string, ReplayHandler>();

  constructor(private readonly prisma: PrismaService) {}

  registerHandler(consumer: string, fn: ReplayHandler): void {
    this.handlers.set(consumer, fn);
  }

  /** Quarantine a failure (idempotent per (consumer, sourceId) — repeats bump retryCount). */
  async record(input: DlqRecordInput): Promise<void> {
    const existing = await this.prisma.iiosDlqItem.findFirst({
      where: { consumerName: input.consumerName ?? null, sourceId: input.sourceId },
    });
    if (existing) {
      await this.prisma.iiosDlqItem.update({
        where: { id: existing.id },
        data: { retryCount: { increment: 1 }, lastFailedAt: new Date(), errorCode: input.errorCode ?? existing.errorCode, status: 'QUARANTINED' },
      });
      return;
    }
    await this.prisma.iiosDlqItem.create({
      data: {
        sourceType: input.sourceType,
        sourceId: input.sourceId,
        failureStage: input.failureStage,
        errorCode: input.errorCode,
        payloadRef: input.payloadRef,
        scopeId: input.scopeId,
        consumerName: input.consumerName,
      },
    });
  }

  /** A projector/consumer threw: dead-letter the event AND clear its claim so it can replay. */
  async onConsumerFailure(consumer: string, event: CloudEvent, err: unknown): Promise<void> {
    await this.record({
      sourceType: 'projection',
      sourceId: event.id,
      failureStage: consumer,
      errorCode: (err as Error)?.message?.slice(0, 160),
      payloadRef: event.id,
      scopeId: event?.insignia?.scopeSnapshotId,
      consumerName: consumer,
    });
    await this.prisma.iiosProcessedEvent.deleteMany({ where: { consumerName: consumer, eventId: event.id } });
  }

  async listDlq(scopeId?: string, status?: string) {
    return this.prisma.iiosDlqItem.findMany({
      where: { ...(scopeId ? { scopeId } : {}), ...(status ? { status } : {}) },
      orderBy: { lastFailedAt: 'desc' },
      take: 200,
    });
  }

  /** Re-run the failed consumer (deterministic) or hand an outbox event back to the relay. */
  async replay(id: string): Promise<{ status: string }> {
    const item = await this.prisma.iiosDlqItem.findUnique({ where: { id } });
    if (!item) throw new NotFoundException('dlq item not found');

    // Projection failure: clear the claim + await the registered handler.
    if (item.consumerName && item.payloadRef && this.handlers.has(item.consumerName)) {
      await this.prisma.iiosProcessedEvent.deleteMany({ where: { consumerName: item.consumerName, eventId: item.sourceId } });
      // The DLQ item references the CloudEvent by its own `id` (not the outbox row's cuid),
      // so recover the payload by matching cloudEvent->>'id'.
      const evt = await this.prisma.iiosOutboxEvent.findFirst({ where: { cloudEvent: { path: ['id'], equals: item.payloadRef } } });
      if (!evt) throw new NotFoundException('event payload not found');
      try {
        await this.handlers.get(item.consumerName)!(evt.cloudEvent as unknown as CloudEvent);
      } catch (err) {
        await this.prisma.iiosDlqItem.update({ where: { id }, data: { retryCount: { increment: 1 }, lastFailedAt: new Date(), errorCode: (err as Error)?.message?.slice(0, 160) } });
        return { status: 'QUARANTINED' };
      }
      await this.prisma.iiosDlqItem.update({ where: { id }, data: { status: 'RESOLVED', resolvedAt: new Date() } });
      return { status: 'RESOLVED' };
    }

    // Outbox publish failure: hand the event back to the relay (reset to PENDING).
    if (item.payloadRef) {
      await this.prisma.iiosOutboxEvent.update({ where: { eventId: item.payloadRef }, data: { status: 'PENDING', nextAttemptAt: new Date(), attempts: 0 } });
      await this.prisma.iiosDlqItem.update({ where: { id }, data: { status: 'RESOLVED', resolvedAt: new Date() } });
      return { status: 'RESOLVED' };
    }
    return { status: item.status };
  }
}
