import { ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

/** The authenticated principal derived from the session port (token). */
export interface MessagePrincipal {
  userId: string;
  orgId: string;
  appId: string;
  tenantId?: string;
  displayName?: string;
  /** The token's audience (`aud`). Used to enforce realtime delegation: a socket-scoped
   *  token (`iios-message`) must not be a full REST/actor token (`iios-core`), and vice-versa. */
  audience?: string;
}

/**
 * Resolves a principal to kernel rows (scope → source handle → actor) and
 * manages thread participation. Shared by the message + inbox layers so neither
 * re-implements identity resolution. Unknown handles stay UNVERIFIED (the MDM
 * port resolution is applied at ingest; native users get a PORTAL_USER handle).
 */
@Injectable()
export class ActorResolver {
  constructor(private readonly prisma: PrismaService) {}

  async resolveScope(principal: MessagePrincipal) {
    return (
      (await this.findScope(principal)) ??
      (await this.prisma.iiosScope.create({
        data: {
          orgId: principal.orgId,
          appId: principal.appId,
          tenantId: principal.tenantId,
          // Cell-partition hook (P9): assign the tenant scope to a cell.
          cellId: process.env.IIOS_CELL_ID ?? 'cell-default',
        },
      }))
    );
  }

  /** Resolve the caller's scope WITHOUT creating it — a caller with no scope owns nothing. */
  async findScope(principal: MessagePrincipal) {
    return this.prisma.iiosScope.findFirst({
      where: { orgId: principal.orgId, appId: principal.appId, tenantId: principal.tenantId ?? null },
    });
  }

  /**
   * Tenant fence (P9 / KG-02): throw unless the caller's scope owns the resource.
   * Every by-id operation calls this before reading or mutating.
   */
  async assertOwns(principal: MessagePrincipal, resourceScopeId: string) {
    const scope = await this.findScope(principal);
    if (!scope || scope.id !== resourceScopeId) {
      throw new ForbiddenException('resource not in your tenant scope');
    }
    return scope;
  }

  async resolveActor(scopeId: string, principal: MessagePrincipal) {
    const handle = await this.prisma.iiosSourceHandle.upsert({
      where: { scopeId_kind_externalId: { scopeId, kind: 'PORTAL_USER', externalId: principal.userId } },
      create: { scopeId, kind: 'PORTAL_USER', externalId: principal.userId, displayName: principal.displayName },
      update: { lastSeenAt: new Date(), displayName: principal.displayName },
    });
    return (
      (await this.prisma.iiosActorRef.findFirst({ where: { sourceHandleId: handle.id } })) ??
      (await this.prisma.iiosActorRef.create({
        data: { kind: 'HUMAN', sourceHandleId: handle.id, displayName: principal.displayName },
      }))
    );
  }

  async ensureParticipant(threadId: string, actorId: string, participantRole = 'MEMBER'): Promise<void> {
    await this.prisma.iiosThreadParticipant.upsert({
      where: { threadId_actorId: { threadId, actorId } },
      create: { threadId, actorId, participantRole },
      update: {},
    });
  }
}
