/**
 * Org project explorer (ADR-0021).
 *
 * Folders are transparent navigation nodes (not ACL resources). Project grants
 * stay on PROJECT. Archive folder refuses when active children/projects remain.
 */
import { Prisma, type PrismaClient } from "@prisma/client";
import {
  archiveFeishuChatBinding,
  createFolder,
  createProjectFromOrgAdmin,
  moveProjectToFolder,
} from "../projectOnboarding.js";
import { lockActiveOrganization } from "./status.js";
import { PrismaPrincipalResolver } from "../permissions/principals.js";

export interface ExplorerFolderNode {
  readonly id: string;
  readonly name: string;
  readonly parentId: string | null;
  readonly sortKey: string;
  readonly projectCount: number;
  readonly childFolderCount: number;
}

export interface ExplorerProjectNode {
  readonly id: string;
  readonly name: string;
  readonly folderId: string | null;
  readonly createdAt: string;
  readonly binding: { readonly chatId: string; readonly createdAt: string } | null;
}

export interface OrgExplorer {
  readonly folders: readonly ExplorerFolderNode[];
  readonly projects: readonly ExplorerProjectNode[];
}

export async function listOrgExplorer(
  prisma: PrismaClient,
  organizationId: string,
): Promise<OrgExplorer> {
  const [folders, projects] = await Promise.all([
    prisma.folder.findMany({
      where: { organizationId, archivedAt: null },
      select: {
        id: true,
        name: true,
        parentId: true,
        sortKey: true,
        _count: {
          select: {
            children: { where: { archivedAt: null } },
            projects: { where: { archivedAt: null } },
          },
        },
      },
      orderBy: [{ sortKey: "asc" }, { name: "asc" }],
    }),
    prisma.project.findMany({
      where: { organizationId, archivedAt: null },
      select: {
        id: true,
        name: true,
        folderId: true,
        createdAt: true,
        groupBindings: {
          where: { archivedAt: null },
          select: { chatId: true, createdAt: true },
          take: 1,
        },
      },
      orderBy: [{ name: "asc" }],
    }),
  ]);

  return {
    folders: folders.map((f) => ({
      id: f.id,
      name: f.name,
      parentId: f.parentId,
      sortKey: f.sortKey,
      projectCount: f._count.projects,
      childFolderCount: f._count.children,
    })),
    projects: projects.map((p) => {
      const binding = p.groupBindings[0];
      return {
        id: p.id,
        name: p.name,
        folderId: p.folderId,
        createdAt: p.createdAt.toISOString(),
        binding:
          binding === undefined
            ? null
            : { chatId: binding.chatId, createdAt: binding.createdAt.toISOString() },
      };
    }),
  };
}

export async function createOrgFolder(
  prisma: PrismaClient,
  input: {
    readonly organizationId: string;
    readonly name: string;
    readonly parentId?: string | undefined;
    readonly sortKey?: string | undefined;
  },
) {
  return createFolder(prisma, input);
}

export async function renameFolder(
  prisma: PrismaClient,
  input: {
    readonly organizationId: string;
    readonly folderId: string;
    readonly name?: string | undefined;
    readonly sortKey?: string | undefined;
    readonly parentId?: string | null | undefined;
  },
) {
  return prisma.$transaction(async (tx) => {
    await lockActiveOrganization(tx, input.organizationId);
    const folder = await requireActiveFolder(tx, input.folderId, input.organizationId);
    if (folder.kind === "SYSTEM_INBOX" && (input.name !== undefined || input.parentId !== undefined)) {
      throw new Error("system Inbox cannot be renamed or moved");
    }
    if (input.parentId !== undefined && input.parentId !== null) {
      if (input.parentId === folder.id) {
        throw new Error("folder cannot be its own parent");
      }
      await requireActiveFolder(tx, input.parentId, input.organizationId);
      const descendant = await tx.$queryRaw<Array<{ found: boolean }>>(Prisma.sql`
        WITH RECURSIVE descendants AS (
          SELECT "id" FROM "Folder" WHERE "parentId" = ${folder.id} AND "archivedAt" IS NULL
          UNION ALL
          SELECT child."id" FROM "Folder" child
          JOIN descendants parent ON child."parentId" = parent."id"
          WHERE child."archivedAt" IS NULL
        )
        SELECT EXISTS(SELECT 1 FROM descendants WHERE "id" = ${input.parentId}) AS found
      `);
      if (descendant[0]?.found === true) throw new Error("folder cannot be moved below its descendant");
    }
    const name =
      input.name !== undefined ? requireNonEmpty(input.name, "folder name") : undefined;
    return tx.folder.update({
      where: { id: folder.id },
      data: {
        ...(name !== undefined ? { name } : {}),
        ...(input.sortKey !== undefined ? { sortKey: input.sortKey } : {}),
        ...(input.parentId !== undefined ? { parentId: input.parentId } : {}),
      },
    });
  });
}

/**
 * Soft-archive a folder. Refuses if it still has active child folders or projects.
 */
export async function archiveFolder(
  prisma: PrismaClient,
  input: { readonly organizationId: string; readonly folderId: string },
): Promise<{ readonly archived: true; readonly folderId: string }> {
  return prisma.$transaction(async (tx) => {
    await lockActiveOrganization(tx, input.organizationId);
    const folder = await requireActiveFolder(tx, input.folderId, input.organizationId);
    if (folder.kind === "SYSTEM_INBOX") throw new Error("system Inbox cannot be archived");
    const childFolders = await tx.folder.count({
      where: { parentId: folder.id, archivedAt: null },
    });
    if (childFolders > 0) {
      throw new Error(
        `cannot archive folder ${folder.id}: still has ${childFolders} active child folder(s)`,
      );
    }
    const projects = await tx.project.count({
      where: { folderId: folder.id, archivedAt: null },
    });
    if (projects > 0) {
      throw new Error(
        `cannot archive folder ${folder.id}: still has ${projects} active project(s)`,
      );
    }
    await tx.folder.update({
      where: { id: folder.id },
      data: { archivedAt: new Date() },
    });
    return { archived: true as const, folderId: folder.id };
  });
}

export async function createOrgProject(
  prisma: PrismaClient,
  input: {
    readonly organizationId: string;
    readonly actorFeishuOpenId: string;
    readonly name: string;
    readonly workspaceRoot: string;
    readonly folderId?: string | undefined;
  },
) {
  return createProjectFromOrgAdmin(prisma, input);
}

export async function renameProject(
  prisma: PrismaClient,
  input: {
    readonly organizationId: string;
    readonly projectId: string;
    readonly name: string;
  },
) {
  const name = requireNonEmpty(input.name, "project name");
  return prisma.$transaction(async (tx) => {
    await lockActiveOrganization(tx, input.organizationId);
    const project = await requireActiveProject(tx, input.projectId, input.organizationId);
    return tx.project.update({
      where: { id: project.id },
      data: { name },
      select: { id: true, name: true, folderId: true },
    });
  });
}

export async function moveOrgProjectToFolder(
  prisma: PrismaClient,
  input: {
    readonly organizationId: string;
    readonly projectId: string;
    readonly folderId: string | null;
  },
) {
  await requireActiveProject(prisma, input.projectId, input.organizationId);
  return moveProjectToFolder(prisma, {
    projectId: input.projectId,
    folderId: input.folderId,
  });
}

export async function archiveProject(
  prisma: PrismaClient,
  input: { readonly organizationId: string; readonly projectId: string },
): Promise<{ readonly archived: true; readonly projectId: string }> {
  return prisma.$transaction(async (tx) => {
    await lockActiveOrganization(tx, input.organizationId);
    const project = await requireActiveProject(tx, input.projectId, input.organizationId);
    const activeBinding = await tx.projectGroupBinding.findFirst({
      where: { projectId: project.id, archivedAt: null },
      select: { id: true, chatId: true },
    });
    const now = new Date();
    if (activeBinding !== null) {
      await tx.projectGroupBinding.update({
        where: { id: activeBinding.id },
        data: { archivedAt: now },
      });
      await tx.permissionGrant.updateMany({
        where: {
          resourceType: "PROJECT",
          resourceId: project.id,
          principalType: "FEISHU_CHAT",
          principalId: activeBinding.chatId,
          revokedAt: null,
        },
        data: { revokedAt: now },
      });
    }
    await tx.project.update({
      where: { id: project.id },
      data: { archivedAt: now },
    });
    return { archived: true as const, projectId: project.id };
  });
}

export async function getOrgProjectDetail(
  prisma: PrismaClient,
  input: { readonly organizationId: string; readonly projectId: string },
) {
  const project = await prisma.project.findFirst({
    where: {
      id: input.projectId,
      organizationId: input.organizationId,
    },
    select: {
      id: true,
      name: true,
      folderId: true,
      workspaceDir: true,
      createdAt: true,
      archivedAt: true,
      createdBy: { select: { id: true, displayName: true, feishuOpenId: true } },
      folder: { select: { id: true, name: true } },
      groupBindings: {
        where: { archivedAt: null },
        select: { chatId: true, createdAt: true },
        take: 1,
      },
    },
  });
  if (project === null) {
    throw new Error(`project not found: ${input.projectId}`);
  }
  const binding = project.groupBindings[0];
  return {
    id: project.id,
    name: project.name,
    folderId: project.folderId,
    folder: project.folder,
    workspaceDir: project.workspaceDir,
    createdAt: project.createdAt.toISOString(),
    archivedAt: project.archivedAt?.toISOString() ?? null,
    createdBy: project.createdBy,
    binding:
      binding === undefined
        ? null
        : { chatId: binding.chatId, createdAt: binding.createdAt.toISOString() },
  };
}

export async function archiveOrgProjectChatBinding(
  prisma: PrismaClient,
  input: {
    readonly organizationId: string;
    readonly projectId: string;
    readonly actorFeishuOpenId: string;
  },
) {
  await requireActiveProject(prisma, input.projectId, input.organizationId);
  return archiveFeishuChatBinding(prisma, {
    projectId: input.projectId,
    actorFeishuOpenId: input.actorFeishuOpenId,
  });
}

/**
 * Projects an org member can see via their resolved principals' READ+ grants
 * (spec `PermissionGrant` / ADR-0004). Used by the member-facing project list
 * — org-admin oversight uses `listOrgExplorer` instead.
 */
export async function listMyProjects(
  prisma: PrismaClient,
  input: { readonly organizationId: string; readonly actorFeishuOpenId: string },
): Promise<readonly ExplorerProjectNode[]> {
  const resolver = new PrismaPrincipalResolver(prisma);
  const resolution = await resolver.resolveActor(
    { feishuOpenId: input.actorFeishuOpenId },
    { organizationId: input.organizationId },
  );
  // Match exact (type, id) pairs — independent IN filters form a cartesian
  // product and can attribute another principal's grants to this actor.
  if (resolution.principals.length === 0) return [];
  const grants = await prisma.permissionGrant.findMany({
    where: {
      resourceType: "PROJECT",
      revokedAt: null,
      OR: resolution.principals.map((p) => ({
        principalType: p.type,
        principalId: p.id,
      })),
    },
    select: { resourceId: true },
    distinct: ["resourceId"],
  });
  const projectIds = grants.map((g) => g.resourceId);
  if (projectIds.length === 0) return [];
  const projects = await prisma.project.findMany({
    where: { id: { in: projectIds }, organizationId: input.organizationId, archivedAt: null },
    select: {
      id: true,
      name: true,
      folderId: true,
      createdAt: true,
      groupBindings: {
        where: { archivedAt: null },
        select: { chatId: true, createdAt: true },
        take: 1,
      },
    },
    orderBy: { name: "asc" },
  });
  return projects.map((p) => {
    const binding = p.groupBindings[0];
    return {
      id: p.id,
      name: p.name,
      folderId: p.folderId,
      createdAt: p.createdAt.toISOString(),
      binding: binding === undefined ? null : { chatId: binding.chatId, createdAt: binding.createdAt.toISOString() },
    };
  });
}

async function requireActiveFolder(
  prisma: PrismaClient | Prisma.TransactionClient,
  folderId: string,
  organizationId: string,
): Promise<{ readonly id: string; readonly organizationId: string; readonly kind: "REGULAR" | "SYSTEM_INBOX" }> {
  const folder = await prisma.folder.findUnique({
    where: { id: folderId },
    select: { id: true, organizationId: true, kind: true, archivedAt: true },
  });
  if (folder === null || folder.archivedAt !== null || folder.organizationId !== organizationId) {
    throw new Error(`active folder not found: ${folderId}`);
  }
  return folder;
}

async function requireActiveProject(
  prisma: PrismaClient | Prisma.TransactionClient,
  projectId: string,
  organizationId: string,
): Promise<{ readonly id: string; readonly organizationId: string }> {
  const project = await prisma.project.findUnique({
    where: { id: projectId },
    select: { id: true, organizationId: true, archivedAt: true },
  });
  if (project === null || project.archivedAt !== null || project.organizationId !== organizationId) {
    throw new Error(`active project not found: ${projectId}`);
  }
  return project;
}

function requireNonEmpty(value: string, label: string): string {
  const trimmed = value.trim();
  if (trimmed === "") throw new Error(`${label} is required`);
  return trimmed;
}
