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 { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';

/**
 * Event-driven ticket assignment (P4). Consumes support.ticket.created and picks
 * the least-loaded available agent within capacity, sets the ticket OPEN, and
 * adds the agent as a participant on the linked thread(s) so they reply over the
 * P2 socket. The base thread is never mutated. Idempotent via the processed-event
 * ledger; the assignedActorId guard also prevents double-assignment.
 */
@Injectable()
export class AssignmentService implements OnModuleInit {
  private readonly consumer = 'assignment';

  constructor(
    private readonly prisma: PrismaService,
    private readonly bus: OutboxBus,
    private readonly actors: ActorResolver,
  ) {}

  onModuleInit(): void {
    this.bus.on(IIOS_EVENTS.ticketCreated, (p) => void this.onTicketCreated(p as CloudEvent).catch(() => {}));
  }

  async onTicketCreated(event: CloudEvent): Promise<void> {
    if (!(await this.claim(event.id))) return;
    const data = event.data as { ticketId: string };
    await this.assignTicket(data.ticketId);
  }

  /** Assign one ticket to the least-loaded available agent. Returns true if assigned. */
  async assignTicket(ticketId: string): Promise<boolean> {
    const ticket = await this.prisma.iiosTicket.findUnique({
      where: { id: ticketId },
      include: { threadLinks: true },
    });
    if (!ticket || ticket.assignedActorId || !ticket.queueId) return false;

    const candidates = await this.prisma.iiosSupportTeamMember.findMany({
      where: { queueId: ticket.queueId, availabilityState: 'AVAILABLE' },
      orderBy: { activeCount: 'asc' },
    });
    const member = candidates.find((m) => m.activeCount < m.maxActive);
    if (!member) return false;

    const event: CloudEvent = {
      specversion: '1.0',
      id: `evt_ticketstate_${ticketId}_OPEN`,
      type: IIOS_EVENTS.ticketStateChanged,
      source: `iios/support/${ticket.scopeId}`,
      subject: `ticket/${ticketId}`,
      time: new Date().toISOString(),
      datacontenttype: 'application/json',
      insignia: {
        scopeSnapshotId: ticket.scopeId,
        correlationId: ticket.traceId ?? undefined,
        idempotencyKey: `assign:${ticketId}`,
        dataClass: 'internal',
      },
      data: { ticketId, fromState: ticket.state, toState: 'OPEN', assignedActorId: member.actorId },
    };

    await this.prisma.$transaction([
      this.prisma.iiosTicket.update({
        where: { id: ticketId },
        data: { assignedActorId: member.actorId, state: 'OPEN' },
      }),
      this.prisma.iiosSupportTeamMember.update({
        where: { queueId_actorId: { queueId: ticket.queueId, actorId: member.actorId } },
        data: { activeCount: { increment: 1 } },
      }),
      this.prisma.iiosTicketStateHistory.create({
        data: { ticketId, fromState: ticket.state, toState: 'OPEN', actorId: member.actorId, reasonCode: 'assigned' },
      }),
      this.prisma.iiosOutboxEvent.create({
        data: {
          aggregateType: 'ticket',
          aggregateId: ticketId,
          eventType: IIOS_EVENTS.ticketStateChanged,
          cloudEvent: event as unknown as Prisma.InputJsonValue,
          partitionKey: `${ticket.scopeId}:${ticketId}`,
        },
      }),
    ]);

    // Agent joins the linked thread(s) so they can reply over the message socket.
    for (const link of ticket.threadLinks) {
      await this.actors.ensureParticipant(link.threadId, member.actorId);
    }
    return true;
  }

  /** Re-scan: assign all unassigned NEW tickets (optionally in one queue). */
  async assignPending(queueId?: string): Promise<number> {
    const tickets = await this.prisma.iiosTicket.findMany({
      where: { assignedActorId: null, state: 'NEW', ...(queueId ? { queueId } : {}) },
    });
    let assigned = 0;
    for (const t of tickets) if (await this.assignTicket(t.id)) assigned++;
    return assigned;
  }

  // ─── queue / team admin ───────────────────────────────────────────

  async createQueue(principal: MessagePrincipal, name: string) {
    const scope = await this.actors.resolveScope(principal);
    return this.prisma.iiosSupportQueue.create({ data: { scopeId: scope.id, name } });
  }

  /** Find-or-create the scope's default queue and add the caller as a member. */
  async joinDefault(agent: MessagePrincipal) {
    const scope = await this.actors.resolveScope(agent);
    const queue =
      (await this.prisma.iiosSupportQueue.findFirst({ where: { scopeId: scope.id, enabled: true } })) ??
      (await this.prisma.iiosSupportQueue.create({ data: { scopeId: scope.id, name: 'Support' } }));
    return this.addMember(queue.id, agent);
  }

  async addMember(queueId: string, agent: MessagePrincipal, opts?: { maxActive?: number }) {
    const queue = await this.prisma.iiosSupportQueue.findUniqueOrThrow({ where: { id: queueId } });
    const actor = await this.actors.resolveActor(queue.scopeId, agent);
    return this.prisma.iiosSupportTeamMember.upsert({
      where: { queueId_actorId: { queueId, actorId: actor.id } },
      create: { queueId, actorId: actor.id, maxActive: opts?.maxActive ?? 3, availabilityState: 'OFFLINE' },
      update: {},
    });
  }

  async setAvailability(agent: MessagePrincipal, state: string): Promise<{ actorId: string; availabilityState: string }> {
    const scope = await this.actors.resolveScope(agent);
    const actor = await this.actors.resolveActor(scope.id, agent);
    await this.prisma.iiosSupportTeamMember.updateMany({ where: { actorId: actor.id }, data: { availabilityState: state } });
    // Newly-available agents may pick up tickets that were waiting.
    if (state === 'AVAILABLE') await this.assignPending();
    return { actorId: actor.id, availabilityState: state };
  }

  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;
  }
}
