import { randomUUID } from 'node:crypto';
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma, IiosTicketState, IiosTicketPriority } from '@prisma/client';
import { CloudEvent, IIOS_EVENTS, IiosPlatformPorts } from '@insignia/iios-contracts';
import { PrismaService } from '../prisma/prisma.service';
import { PLATFORM_PORTS } from '../platform/platform-ports';
import { decideOrThrow } from '../platform/fail-closed';
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
import { IdempotencyService } from '../idempotency/idempotency.service';

const TICKET_TRANSITIONS: Record<IiosTicketState, IiosTicketState[]> = {
  NEW: ['OPEN', 'CANCELLED'],
  OPEN: ['PENDING_CUSTOMER', 'PENDING_INTERNAL', 'RESOLVED', 'CANCELLED'],
  PENDING_CUSTOMER: ['OPEN', 'RESOLVED', 'CANCELLED'],
  PENDING_INTERNAL: ['OPEN', 'RESOLVED', 'CANCELLED'],
  RESOLVED: ['CLOSED', 'OPEN'],
  CLOSED: ['OPEN'],
  CANCELLED: [],
};

/**
 * Support specialization (P4). Imports the kernel via ActorResolver; NEVER
 * touched by the message/inbox layers. Escalation creates a ticket linked M:N
 * to a generic kernel thread and emits support.ticket.created for the
 * (event-driven) assignment worker.
 */
@Injectable()
export class SupportService {
  constructor(
    private readonly prisma: PrismaService,
    @Inject(PLATFORM_PORTS) private readonly ports: IiosPlatformPorts,
    private readonly actors: ActorResolver,
    private readonly idempotency: IdempotencyService,
  ) {}

  async createTicket(
    principal: MessagePrincipal,
    input: { subject: string; priority?: IiosTicketPriority; threadId?: string; queueId?: string; metadata?: Record<string, unknown> },
    idempotencyKey?: string,
  ) {
    await decideOrThrow(this.ports, { action: 'iios.support.ticket.create', scope: principal });
    const scope = await this.actors.resolveScope(principal);

    const body = async () => {
      const requester = await this.actors.resolveActor(scope.id, principal);
      const traceId = randomUUID();

      // Every ticket gets a queue so assignment has somewhere to route it — find
      // the scope's default queue or create one.
      const queueId =
        input.queueId ??
        (
          (await this.prisma.iiosSupportQueue.findFirst({ where: { scopeId: scope.id, enabled: true } })) ??
          (await this.prisma.iiosSupportQueue.create({ data: { scopeId: scope.id, name: 'Support' } }))
        ).id;

      return this.prisma.$transaction(async (tx) => {
        const ticket = await tx.iiosTicket.create({
          data: {
            scopeId: scope.id,
            queueId,
            requesterActorId: requester.id,
            subject: input.subject,
            priority: input.priority ?? 'P3',
            traceId,
            // Opaque, app-supplied attribute bag (e.g. a caller's external ref) — stored, never interpreted.
            metadata: input.metadata ? (input.metadata as Prisma.InputJsonValue) : undefined,
          },
        });
        await tx.iiosTicketStateHistory.create({
          data: { ticketId: ticket.id, toState: 'NEW', actorId: requester.id, reasonCode: 'created' },
        });
        if (input.threadId) {
          await tx.iiosTicketThreadLink.create({ data: { ticketId: ticket.id, threadId: input.threadId } });
        }
        const event: CloudEvent = {
          specversion: '1.0',
          id: `evt_${ticket.id}`,
          type: IIOS_EVENTS.ticketCreated,
          source: `iios/support/${scope.id}`,
          subject: `ticket/${ticket.id}`,
          time: new Date().toISOString(),
          datacontenttype: 'application/json',
          insignia: { scopeSnapshotId: scope.id, correlationId: traceId, idempotencyKey: `ticket:${ticket.id}`, dataClass: 'internal' },
          data: { ticketId: ticket.id, scopeId: scope.id, queueId, threadId: input.threadId },
        };
        await tx.iiosOutboxEvent.create({
          data: {
            aggregateType: 'ticket',
            aggregateId: ticket.id,
            eventType: IIOS_EVENTS.ticketCreated,
            cloudEvent: event as unknown as Prisma.InputJsonValue,
            partitionKey: `${scope.id}:${ticket.id}`,
          },
        });
        return ticket;
      });
    };

    if (!idempotencyKey) return body();
    return this.idempotency.run({ scopeId: scope.id, commandName: 'support.ticket.create', key: idempotencyKey, request: input }, body);
  }

  /** Escalate a chat thread to support: create a ticket linked to that thread. `metadata` is opaque. */
  async escalate(threadId: string, principal: MessagePrincipal, subject?: string, metadata?: Record<string, unknown>) {
    const thread = await this.prisma.iiosThread.findUnique({ where: { id: threadId } });
    if (!thread) throw new NotFoundException('thread not found');
    // Inherit the thread's opaque bag so the ticket carries the same app context, unless overridden.
    const inherited = { ...((thread.metadata as Record<string, unknown> | null) ?? {}), ...(metadata ?? {}) };
    return this.createTicket(principal, {
      subject: subject ?? thread.subject ?? 'Support request',
      threadId,
      metadata: Object.keys(inherited).length > 0 ? inherited : undefined,
    });
  }

  async linkThread(ticketId: string, threadId: string, relationKind = 'PRIMARY'): Promise<void> {
    await this.prisma.iiosTicketThreadLink.upsert({
      where: { ticketId_threadId: { ticketId, threadId } },
      create: { ticketId, threadId, relationKind },
      update: {},
    });
  }

  async unlinkThread(ticketId: string, threadId: string): Promise<void> {
    await this.prisma.iiosTicketThreadLink
      .delete({ where: { ticketId_threadId: { ticketId, threadId } } })
      .catch(() => undefined);
  }

  /**
   * Manually assign a ticket to a specific actor (by userId) — a generic override of the
   * event-driven auto-assignment. The target is resolved-or-created as an actor in the ticket's
   * scope (so you can assign someone who hasn't logged in yet) and joined to the linked thread(s)
   * so they can reply over the socket. Fail-closed via policy `iios.support.ticket.assign`.
   */
  async assignTo(ticketId: string, principal: MessagePrincipal, targetUserId: string) {
    const ticket = await this.prisma.iiosTicket.findUnique({ where: { id: ticketId }, include: { threadLinks: true } });
    if (!ticket) throw new NotFoundException('ticket not found');
    await decideOrThrow(this.ports, { action: 'iios.support.ticket.assign', ticketId, scopeId: ticket.scopeId, targetUserId });

    const target = await this.actors.resolveActor(ticket.scopeId, {
      userId: targetUserId,
      appId: principal.appId,
      orgId: principal.orgId,
      tenantId: principal.tenantId,
      displayName: targetUserId,
    });
    const toState: IiosTicketState = ticket.state === 'NEW' ? 'OPEN' : ticket.state;
    const event: CloudEvent = {
      specversion: '1.0',
      id: `evt_assign_${ticketId}_${target.id}`,
      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}:${target.id}`, dataClass: 'internal' },
      data: { ticketId, fromState: ticket.state, toState, assignedActorId: target.id },
    };
    await this.prisma.$transaction([
      this.prisma.iiosTicket.update({ where: { id: ticketId }, data: { assignedActorId: target.id, state: toState } }),
      this.prisma.iiosTicketStateHistory.create({ data: { ticketId, fromState: ticket.state, toState, actorId: target.id, 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}`,
        },
      }),
    ]);
    for (const link of ticket.threadLinks) await this.actors.ensureParticipant(link.threadId, target.id);
    return this.prisma.iiosTicket.findUnique({ where: { id: ticketId } });
  }

  async transition(ticketId: string, principal: MessagePrincipal, toState: IiosTicketState, reason?: string) {
    const ticket = await this.prisma.iiosTicket.findUnique({ where: { id: ticketId } });
    if (!ticket) throw new NotFoundException('ticket not found');
    await decideOrThrow(this.ports, { action: 'iios.support.ticket.transition', ticketId, scopeId: ticket.scopeId });
    if (!TICKET_TRANSITIONS[ticket.state].includes(toState)) {
      throw new BadRequestException(`illegal transition ${ticket.state} -> ${toState}`);
    }
    const actor = await this.actors.resolveActor(ticket.scopeId, principal);
    const event: CloudEvent = {
      specversion: '1.0',
      id: `evt_ticketstate_${ticketId}_${toState}`,
      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: `ticketstate:${ticketId}:${toState}`,
        dataClass: 'internal',
      },
      data: { ticketId, fromState: ticket.state, toState },
    };
    const [updated] = await this.prisma.$transaction([
      this.prisma.iiosTicket.update({ where: { id: ticketId }, data: { state: toState } }),
      this.prisma.iiosTicketStateHistory.create({
        data: { ticketId, fromState: ticket.state, toState, actorId: actor.id, reasonCode: reason },
      }),
      this.prisma.iiosOutboxEvent.create({
        data: {
          aggregateType: 'ticket',
          aggregateId: ticketId,
          eventType: IIOS_EVENTS.ticketStateChanged,
          cloudEvent: event as unknown as Prisma.InputJsonValue,
          partitionKey: `${ticket.scopeId}:${ticketId}`,
        },
      }),
    ]);
    return updated;
  }

  async requestCallback(
    principal: MessagePrincipal,
    input: { preferChannel: string; preferredTime?: Date; notes?: string; ticketId?: string },
    idempotencyKey?: string,
  ) {
    await decideOrThrow(this.ports, { action: 'iios.support.callback.request', scope: principal });
    const scope = await this.actors.resolveScope(principal);

    const body = async () => {
      const requester = await this.actors.resolveActor(scope.id, principal);
      return this.prisma.iiosCallbackRequest.create({
        data: {
          scopeId: scope.id,
          requesterActorId: requester.id,
          preferChannel: input.preferChannel,
          preferredTime: input.preferredTime,
          notes: input.notes,
          ticketId: input.ticketId,
          status: input.preferredTime ? 'SCHEDULED' : 'PENDING',
        },
      });
    };

    if (!idempotencyKey) return body();
    return this.idempotency.run({ scopeId: scope.id, commandName: 'support.callback.request', key: idempotencyKey, request: input }, body);
  }

  async listTickets(principal: MessagePrincipal, opts: { scope: 'mine' | 'assigned' }) {
    await decideOrThrow(this.ports, { action: 'iios.support.ticket.list', scope: principal });
    const scope = await this.actors.resolveScope(principal);
    const actor = await this.actors.resolveActor(scope.id, principal);
    const where =
      opts.scope === 'assigned' ? { assignedActorId: actor.id } : { requesterActorId: actor.id };
    return this.prisma.iiosTicket.findMany({
      where,
      orderBy: { updatedAt: 'desc' },
      include: { threadLinks: true },
    });
  }
}
