import { Injectable } from '@nestjs/common'; import { PrismaService } from '../../shared/prisma/prisma.service'; import { appBadRequest, appForbidden, appNotFound } from '../../shared/common/app-error'; export interface AgentScope { rootAgentId: bigint; rootLevel: number; subtreeIds: bigint[]; descendantIds: bigint[]; directChildIds: bigint[]; subtreeIdSet: Set; } type ClosureRow = { ancestorId?: bigint; descendantId: bigint; depth: number; }; function uniqueBigints(values: bigint[]) { const seen = new Set(); const out: bigint[] = []; for (const value of values) { const key = value.toString(); if (seen.has(key)) continue; seen.add(key); out.push(value); } return out; } @Injectable() export class AgentNetworkService { constructor(private prisma: PrismaService) {} async buildAgentAncestorChainMap(parentAgentIds: (bigint | null | undefined)[]) { return this.buildAncestorChainMap(parentAgentIds, undefined); } async buildScopedAncestorChainMap( parentAgentIds: (bigint | null | undefined)[], rootAgentId: bigint, ) { return this.buildAncestorChainMap(parentAgentIds, rootAgentId); } async resolveScope(rootAgentId: bigint): Promise { const [profile, closureRows] = await Promise.all([ this.prisma.agentProfile.findUnique({ where: { userId: rootAgentId }, }), this.findSubtreeClosureRows(rootAgentId), ]); if (!profile) throw appBadRequest('AGENT_PROFILE_NOT_FOUND'); const hasClosureRows = closureRows.length > 0; const subtreeIds = hasClosureRows ? this.idsFromSubtreeClosure(rootAgentId, closureRows) : await this.getSubtreeAgentIdsFromProfiles(rootAgentId); const descendantIds = subtreeIds.filter((id) => id !== rootAgentId); const directChildIds = hasClosureRows ? uniqueBigints( closureRows .filter((row) => row.depth === 1) .map((row) => row.descendantId), ) : await this.getDirectChildAgentIds(rootAgentId); return { rootAgentId, rootLevel: profile.level, subtreeIds, descendantIds, directChildIds, subtreeIdSet: new Set(subtreeIds.map((id) => id.toString())), }; } assertAgentInScope(scope: AgentScope, agentId: bigint) { if (!scope.subtreeIdSet.has(agentId.toString())) { throw appForbidden('NOT_SUB_AGENT'); } } async requirePlayerInPortalSubtree(rootAgentId: bigint, playerId: bigint) { const scope = await this.resolveScope(rootAgentId); const player = await this.prisma.user.findFirst({ where: { id: playerId, userType: 'PLAYER', deletedAt: null }, include: { wallet: true, preferences: true, auth: true }, }); if (!player?.parentId || !scope.subtreeIdSet.has(player.parentId.toString())) { throw appForbidden('MANAGE_DIRECT_PLAYERS_ONLY'); } return { player, scope, isDirect: player.parentId.toString() === rootAgentId.toString() }; } async getChildAgents(agentId: bigint) { return this.prisma.agentProfile.findMany({ where: { parentAgentId: agentId }, include: { user: true }, }); } async assertDescendantAgent(rootAgentId: bigint, targetAgentId: bigint) { const subtreeIds = await this.getSubtreeAgentIds(rootAgentId); if (!subtreeIds.some((id) => id === targetAgentId)) { throw appForbidden('NOT_SUB_AGENT'); } const profile = await this.prisma.agentProfile.findUnique({ where: { userId: targetAgentId }, }); if (!profile) throw appNotFound('AGENT_NOT_FOUND'); return profile; } async assertDirectChildAgent(parentAgentId: bigint, subAgentId: bigint) { const profile = await this.prisma.agentProfile.findUnique({ where: { userId: subAgentId }, }); if (!profile || profile.parentAgentId !== parentAgentId) { throw appForbidden('NOT_SUB_AGENT'); } return profile; } async getSubtreeAgentIds(agentId: bigint) { const closureRows = await this.findSubtreeClosureRows(agentId); if (closureRows.length > 0) { return this.idsFromSubtreeClosure(agentId, closureRows); } return this.getSubtreeAgentIdsFromProfiles(agentId); } private async buildAncestorChainMap( parentAgentIds: (bigint | null | undefined)[], scopedRootAgentId: bigint | undefined, ) { const agentIds = uniqueBigints(parentAgentIds.filter((id): id is bigint => id != null)); const map = new Map(); if (agentIds.length === 0) return map; const closureRows = await this.prisma.agentClosure.findMany({ where: { descendantId: { in: agentIds } }, select: { ancestorId: true, descendantId: true, depth: true }, orderBy: [{ descendantId: 'asc' }, { depth: 'desc' }], }); const rowsByDescendant = new Map(); for (const row of closureRows) { const key = row.descendantId.toString(); rowsByDescendant.set(key, [...(rowsByDescendant.get(key) ?? []), row]); } const idsNeedingProfileFallback: bigint[] = []; const ancestorIds = new Set(); for (const id of agentIds) { const rows = rowsByDescendant.get(id.toString()); if (!rows || rows.length === 0) { idsNeedingProfileFallback.push(id); continue; } const scopedRows = scopedRootAgentId ? this.sliceRowsToScopedRoot(rows, scopedRootAgentId) : rows; for (const row of scopedRows) { if (row.ancestorId) ancestorIds.add(row.ancestorId); } } const users = ancestorIds.size > 0 ? await this.prisma.user.findMany({ where: { id: { in: [...ancestorIds] } }, select: { id: true, username: true }, }) : []; const usernameMap = new Map(users.map((u) => [u.id.toString(), u.username])); for (const id of agentIds) { const rows = rowsByDescendant.get(id.toString()); if (!rows || rows.length === 0) continue; const scopedRows = scopedRootAgentId ? this.sliceRowsToScopedRoot(rows, scopedRootAgentId) : rows; const chain = scopedRows .map((row) => (row.ancestorId ? usernameMap.get(row.ancestorId.toString()) : undefined)) .filter((username): username is string => Boolean(username)); map.set(id.toString(), chain.length === scopedRows.length ? chain : []); } if (idsNeedingProfileFallback.length > 0) { const fallbackMap = scopedRootAgentId ? await this.buildScopedAncestorChainMapFromProfiles(idsNeedingProfileFallback, scopedRootAgentId) : await this.buildAncestorChainMapFromProfiles(idsNeedingProfileFallback); for (const [key, value] of fallbackMap) { map.set(key, value); } } for (const id of parentAgentIds) { if (id && !map.has(id.toString())) { map.set(id.toString(), []); } } return map; } private sliceRowsToScopedRoot(rows: ClosureRow[], rootAgentId: bigint) { const rootRow = rows.find((row) => row.ancestorId === rootAgentId); if (!rootRow) return []; return rows.filter((row) => row.depth <= rootRow.depth); } private async buildAncestorChainMapFromProfiles(agentIds: bigint[]) { const cache = await this.loadAncestorProfileCache(agentIds, undefined); const build = (startId: bigint | null | undefined): string[] => { const chain: string[] = []; let cur = startId ?? null; while (cur) { const hit = cache.get(cur.toString()); if (!hit) break; chain.unshift(hit.username); cur = hit.parentAgentId; } return chain; }; return new Map(agentIds.map((id) => [id.toString(), build(id)])); } private async buildScopedAncestorChainMapFromProfiles(agentIds: bigint[], rootAgentId: bigint) { const rootKey = rootAgentId.toString(); const cache = await this.loadAncestorProfileCache(agentIds, rootAgentId); const build = (startId: bigint | null | undefined): string[] => { if (!startId) return []; const chain: string[] = []; let cur: bigint | null = startId; let reachedRoot = false; while (cur) { const hit = cache.get(cur.toString()); if (!hit) return []; chain.unshift(hit.username); if (cur.toString() === rootKey) { reachedRoot = true; break; } cur = hit.parentAgentId; } return reachedRoot ? chain : []; }; return new Map(agentIds.map((id) => [id.toString(), build(id)])); } private async loadAncestorProfileCache(agentIds: bigint[], stopBeforeParentOf?: bigint) { const cache = new Map(); const pending = new Set(agentIds); if (stopBeforeParentOf) pending.add(stopBeforeParentOf); const stopKey = stopBeforeParentOf?.toString(); while (pending.size > 0) { const batch = [...pending]; pending.clear(); const profiles = await this.prisma.agentProfile.findMany({ where: { userId: { in: batch } }, select: { userId: true, parentAgentId: true, user: { select: { username: true } }, }, }); for (const profile of profiles) { cache.set(profile.userId.toString(), { username: profile.user.username, parentAgentId: profile.parentAgentId, }); if ( profile.parentAgentId && profile.parentAgentId.toString() !== stopKey && !cache.has(profile.parentAgentId.toString()) ) { pending.add(profile.parentAgentId); } } } return cache; } private async findSubtreeClosureRows(agentId: bigint) { return this.prisma.agentClosure.findMany({ where: { ancestorId: agentId }, select: { descendantId: true, depth: true }, orderBy: [{ depth: 'asc' }, { descendantId: 'asc' }], }); } private idsFromSubtreeClosure(agentId: bigint, rows: ClosureRow[]) { return uniqueBigints([agentId, ...rows.map((row) => row.descendantId)]); } private async getDirectChildAgentIds(agentId: bigint) { const children = await this.prisma.agentProfile.findMany({ where: { parentAgentId: agentId }, select: { userId: true }, orderBy: { createdAt: 'desc' }, }); return children.map((child) => child.userId); } private async getSubtreeAgentIdsFromProfiles(agentId: bigint) { const ids: bigint[] = []; const queue: bigint[] = [agentId]; const seen = new Set(); while (queue.length > 0) { const current = queue.shift()!; const key = current.toString(); if (seen.has(key)) continue; seen.add(key); ids.push(current); const children = await this.prisma.agentProfile.findMany({ where: { parentAgentId: current }, select: { userId: true }, }); for (const child of children) { queue.push(child.userId); } } return ids; } }