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

type ClosureRow = {
  ancestorId?: bigint;
  descendantId: bigint;
  depth: number;
};

function uniqueBigints(values: bigint[]) {
  const seen = new Set<string>();
  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<AgentScope> {
    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<string, string[]>();
    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<string, ClosureRow[]>();
    for (const row of closureRows) {
      const key = row.descendantId.toString();
      rowsByDescendant.set(key, [...(rowsByDescendant.get(key) ?? []), row]);
    }

    const idsNeedingProfileFallback: bigint[] = [];
    const ancestorIds = new Set<bigint>();
    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<string, { username: string; parentAgentId: bigint | null }>();
    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<string>();

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