import { Injectable, OnModuleInit } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { CloudEvent, IIOS_EVENTS } from '@insignia/iios-contracts';
import { PrismaService } from '../prisma/prisma.service';
import { OutboxBus } from '../outbox/outbox.bus';
import { DlqService } from '../outbox/dlq.service';
import { ProjectionCursorService } from '../projection/projection-cursor.service';

interface MessageSentData {
  interactionId: string;
  threadId: string;
  senderActorId: string;
  mentions?: string[]; // opaque app notify-list (userIds); the kernel never parses "@"
}
interface MessageReadData {
  threadId: string;
  actorId: string;
  interactionId: string;
}

/**
 * Projects message events into Inbox work-items (P3). Inbox reacts to events —
 * it never imports the message layer. Deterministic + idempotent (collapse one
 * OPEN NEEDS_REPLY per owner+thread; dedupe each event via the processed-event
 * ledger). Reading a thread resolves the owner's item to DONE.
 */
@Injectable()
export class InboxProjector implements OnModuleInit {
  private readonly consumer = 'inbox-projector';

  constructor(
    private readonly prisma: PrismaService,
    private readonly bus: OutboxBus,
    private readonly dlq: DlqService,
    private readonly cursor: ProjectionCursorService,
  ) {}

  onModuleInit(): void {
    this.dlq.registerHandler(this.consumer, (e) => this.onMessageSent(e));
    this.bus.on(IIOS_EVENTS.messageSent, (p) => void this.onMessageSent(p as CloudEvent).catch((err) => this.dlq.onConsumerFailure(this.consumer, p as CloudEvent, err)));
    this.bus.on(IIOS_EVENTS.messageRead, (p) => void this.onMessageRead(p as CloudEvent).catch((err) => this.dlq.onConsumerFailure(this.consumer, p as CloudEvent, err)));
  }

  async onMessageSent(event: CloudEvent): Promise<void> {
    if (!(await this.claim(event.id))) return; // duplicate → no apply, no cursor advance
    await this.applyMessageSent(event);
    await this.cursor.advance(this.consumer, event);
  }

  private async applyMessageSent(event: CloudEvent): Promise<void> {
    const data = event.data as MessageSentData;
    const traceId = event.insignia?.correlationId;
    const thread = await this.prisma.iiosThread.findUnique({ where: { id: data.threadId } });
    if (!thread) return;

    const participants = await this.prisma.iiosThreadParticipant.findMany({ where: { threadId: data.threadId } });
    for (const p of participants) {
      if (p.actorId === data.senderActorId) continue;
      await this.upsertNeedsReply(thread.scopeId, p.actorId, data.threadId, data.interactionId, traceId, thread.subject);
    }

    // Targeted mention notifications from the app-supplied opaque notify-list.
    const mentions = data.mentions ?? [];
    if (mentions.length > 0) {
      const src = await this.prisma.iiosInteraction.findUnique({
        where: { id: data.interactionId },
        include: { parts: { where: { kind: 'TEXT' }, take: 1 }, actor: { include: { sourceHandle: true } } },
      });
      const senderName = src?.actor?.sourceHandle?.externalId ?? 'Someone';
      const text = src?.parts[0]?.bodyText ?? undefined;
      for (const userId of mentions) {
        await this.createMention(thread.scopeId, userId, data.threadId, data.interactionId, data.senderActorId, senderName, text, traceId);
      }
    }
  }

  /** One MENTION item per (owner, source message) for a mentioned participant (never the sender). */
  private async createMention(
    scopeId: string,
    userId: string,
    threadId: string,
    sourceInteractionId: string,
    senderActorId: string,
    senderName: string,
    summary: string | undefined,
    traceId: string | undefined,
  ): Promise<void> {
    const handle = await this.prisma.iiosSourceHandle.findUnique({
      where: { scopeId_kind_externalId: { scopeId, kind: 'PORTAL_USER', externalId: userId } },
    });
    if (!handle) return;
    const actor = await this.prisma.iiosActorRef.findFirst({ where: { sourceHandleId: handle.id } });
    if (!actor || actor.id === senderActorId) return; // resolvable, and never notify the sender
    const isMember = await this.prisma.iiosThreadParticipant.findUnique({ where: { threadId_actorId: { threadId, actorId: actor.id } } });
    if (!isMember) return; // only thread participants get mention items
    const existing = await this.prisma.iiosInboxItem.findFirst({ where: { ownerActorId: actor.id, sourceInteractionId, kind: 'MENTION' } });
    if (existing) return; // idempotent per source message

    const item = await this.prisma.iiosInboxItem.create({
      data: {
        scopeId,
        ownerActorId: actor.id,
        kind: 'MENTION',
        title: `${senderName} mentioned you`,
        summary,
        priority: 'HIGH',
        threadId,
        sourceInteractionId,
        traceId,
      },
    });
    await this.prisma.iiosInboxItemStateHistory.create({
      data: { inboxItemId: item.id, toState: 'OPEN', reasonCode: 'mentioned' },
    });
  }

  async onMessageRead(event: CloudEvent): Promise<void> {
    if (!(await this.claim(event.id))) return; // duplicate → no apply, no cursor advance
    await this.applyMessageRead(event);
    await this.cursor.advance(this.consumer, event);
  }

  private async applyMessageRead(event: CloudEvent): Promise<void> {
    const data = event.data as MessageReadData;
    // Reading the thread clears the reader's activity AND mention items for it.
    const items = await this.prisma.iiosInboxItem.findMany({
      where: {
        threadId: data.threadId,
        ownerActorId: data.actorId,
        kind: { in: ['NEEDS_REPLY', 'MENTION'] },
        state: { in: ['OPEN', 'SNOOZED'] },
      },
    });
    if (items.length === 0) return;
    await this.prisma.$transaction(
      items.flatMap((item) => [
        this.prisma.iiosInboxItem.update({ where: { id: item.id }, data: { state: 'DONE' } }),
        this.prisma.iiosInboxItemStateHistory.create({
          data: { inboxItemId: item.id, fromState: item.state, toState: 'DONE', reasonCode: 'message_read' },
        }),
      ]),
    );
  }

  private async upsertNeedsReply(
    scopeId: string,
    ownerActorId: string,
    threadId: string,
    sourceInteractionId: string,
    traceId: string | undefined,
    subject: string | null,
  ): Promise<void> {
    const existing = await this.prisma.iiosInboxItem.findFirst({
      where: { ownerActorId, threadId, kind: 'NEEDS_REPLY', state: { in: ['OPEN', 'SNOOZED'] } },
    });
    if (existing) {
      await this.prisma.iiosInboxItem.update({
        where: { id: existing.id },
        data: { sourceInteractionId, traceId: traceId ?? existing.traceId, state: 'OPEN' },
      });
      return;
    }

    const item = await this.prisma.iiosInboxItem.create({
      data: {
        scopeId,
        ownerActorId,
        kind: 'NEEDS_REPLY',
        title: subject ?? 'New messages',
        threadId,
        sourceInteractionId,
        traceId,
      },
    });
    await this.prisma.iiosInboxItemStateHistory.create({
      data: { inboxItemId: item.id, toState: 'OPEN', reasonCode: 'message_sent' },
    });

    const created: CloudEvent = {
      specversion: '1.0',
      id: `evt_${item.id}`,
      type: IIOS_EVENTS.inboxItemCreated,
      source: `iios/inbox/${scopeId}`,
      subject: `inbox_item/${item.id}`,
      time: new Date().toISOString(),
      datacontenttype: 'application/json',
      insignia: { scopeSnapshotId: scopeId, correlationId: traceId, idempotencyKey: `inbox:${item.id}`, dataClass: 'internal' },
      data: { inboxItemId: item.id, ownerActorId, threadId, kind: 'NEEDS_REPLY' },
    };
    await this.prisma.iiosOutboxEvent.create({
      data: {
        aggregateType: 'inbox_item',
        aggregateId: item.id,
        eventType: IIOS_EVENTS.inboxItemCreated,
        cloudEvent: created as unknown as Prisma.InputJsonValue,
        partitionKey: `${scopeId}:${threadId}`,
      },
    });
  }

  /** Idempotent claim: false if this event id was already processed. */
  private async claim(eventId: string): Promise<boolean> {
    const res = await this.prisma.iiosProcessedEvent.createMany({
      data: [{ consumerName: this.consumer, eventId }],
      skipDuplicates: true,
    });
    return res.count > 0;
  }
}
