import { Prisma, type PrismaClient } from "@prisma/client";
import { lockActiveProjectOrganization } from "../org/status.js";

export interface ProjectConsoleState {
  readonly organizationId: string;
  readonly projectId: string;
  readonly projectName: string;
  readonly folderId: string | null;
  readonly breadcrumb: string;
  readonly selectedRole: { readonly id: string; readonly roleId: string; readonly label: string };
  readonly roles: readonly { readonly id: string; readonly roleId: string; readonly label: string }[];
  readonly currentSession: {
    readonly id: string;
    readonly title: string | null;
    readonly updatedAt: Date;
    readonly runCount: number;
    readonly sdkSessionReady: boolean;
  } | null;
}

export interface FolderDestinationPage {
  readonly folderId: string | null;
  readonly parentFolderId: string | null;
  readonly breadcrumb: string;
  readonly childFolders: readonly { readonly id: string; readonly name: string }[];
}

export async function browseFolderDestinations(
  prisma: PrismaClient,
  input: { readonly organizationId: string; readonly folderId: string | null },
): Promise<FolderDestinationPage> {
  const folder = input.folderId === null ? null : await prisma.folder.findFirst({
    where: { id: input.folderId, organizationId: input.organizationId, archivedAt: null },
    select: { id: true, parentId: true },
  });
  if (input.folderId !== null && folder === null) throw new Error(`active folder not found: ${input.folderId}`);
  const childFolders = await prisma.folder.findMany({
    where: {
      organizationId: input.organizationId,
      parentId: input.folderId,
      archivedAt: null,
      kind: { not: "SYSTEM_INBOX" },
    },
    orderBy: [{ sortKey: "asc" }, { name: "asc" }, { id: "asc" }],
    select: { id: true, name: true },
  });
  return {
    folderId: input.folderId,
    parentFolderId: folder?.parentId ?? null,
    breadcrumb: input.folderId === null ? "根目录" : await folderBreadcrumb(prisma, input.folderId),
    childFolders,
  };
}

export async function createFolderAndMoveProject(
  prisma: PrismaClient,
  input: {
    readonly organizationId: string;
    readonly projectId: string;
    readonly parentFolderId: string | null;
    readonly name: string;
    readonly actorUserId?: string | undefined;
  },
): Promise<{ readonly folderId: string; readonly folderName: string }> {
  const name = input.name.trim();
  if (name === "") throw new Error("folder name is required");
  if (name.length > 100) throw new Error("folder name must not exceed 100 characters");
  return prisma.$transaction(async (tx) => {
    await lockActiveProjectOrganization(tx, input.projectId);
    const project = await tx.project.findFirst({
      where: { id: input.projectId, organizationId: input.organizationId, archivedAt: null },
      select: { id: true, folderId: true },
    });
    if (project === null) throw new Error("active project not found in Organization");
    if (input.parentFolderId !== null) {
      const parent = await tx.folder.findFirst({
        where: {
          id: input.parentFolderId,
          organizationId: input.organizationId,
          archivedAt: null,
          kind: { not: "SYSTEM_INBOX" },
        },
        select: { id: true },
      });
      if (parent === null) throw new Error("active destination folder not found in Organization");
    }
    const folder = await tx.folder.create({
      data: {
        organizationId: input.organizationId,
        parentId: input.parentFolderId,
        name,
      },
      select: { id: true, name: true },
    });
    await tx.project.update({ where: { id: project.id }, data: { folderId: folder.id } });
    await tx.auditEntry.create({
      data: {
        organizationId: input.organizationId,
        projectId: project.id,
        ...(input.actorUserId !== undefined ? { actorUserId: input.actorUserId } : {}),
        action: "folder.created_and_project_moved_from_feishu",
        metadata: {
          folderId: folder.id,
          parentFolderId: input.parentFolderId,
          previousFolderId: project.folderId,
          name: folder.name,
        },
      },
    });
    return { folderId: folder.id, folderName: folder.name };
  });
}

export async function loadProjectConsole(
  prisma: PrismaClient,
  input: { readonly projectId: string; readonly chatId: string },
): Promise<ProjectConsoleState> {
  const binding = await prisma.projectGroupBinding.findFirst({
    where: { projectId: input.projectId, chatId: input.chatId, archivedAt: null },
    select: {
      selectedRole: { select: { id: true, roleId: true, label: true, disabledAt: true, organizationId: true } },
      project: {
        select: {
          id: true,
          name: true,
          folderId: true,
          organizationId: true,
          archivedAt: true,
          organization: {
            select: {
              status: true,
              agentRoles: {
                where: { disabledAt: null },
                orderBy: [{ sortOrder: "asc" }, { roleId: "asc" }],
                select: { id: true, roleId: true, label: true },
              },
            },
          },
        },
      },
    },
  });
  if (binding === null) throw new Error("project console requires an active binding to this chat");
  const { project, selectedRole } = binding;
  if (project.archivedAt !== null) throw new Error(`project ${project.id} is archived`);
  if (project.organization.status !== "ACTIVE") {
    throw new Error(`organization ${project.organizationId} is ${project.organization.status}`);
  }
  if (selectedRole.disabledAt !== null || selectedRole.organizationId !== project.organizationId) {
    throw new Error("project group selected role is unavailable or cross-Organization");
  }
  const model = await selectedRoleModel(prisma, project.organizationId, selectedRole.id);
  const currentSession = await prisma.agentSession.findFirst({
    where: {
      projectId: project.id,
      roleId: selectedRole.roleId,
      ...(model === null ? {} : { model }),
      archivedAt: null,
    },
    orderBy: { updatedAt: "desc" },
    select: { id: true, title: true, updatedAt: true, metadata: true, _count: { select: { runs: true } } },
  });
  return {
    organizationId: project.organizationId,
    projectId: project.id,
    projectName: project.name,
    folderId: project.folderId,
    breadcrumb: project.folderId === null ? "根目录" : await folderBreadcrumb(prisma, project.folderId),
    selectedRole: { id: selectedRole.id, roleId: selectedRole.roleId, label: selectedRole.label },
    roles: project.organization.agentRoles,
    currentSession: currentSession === null ? null : {
      id: currentSession.id,
      title: currentSession.title,
      updatedAt: currentSession.updatedAt,
      runCount: currentSession._count.runs,
      sdkSessionReady: hasClaudeSessionId(currentSession.metadata),
    },
  };
}

function hasClaudeSessionId(metadata: unknown): boolean {
  if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) return false;
  const value = (metadata as Record<string, unknown>)["claudeSessionId"];
  return typeof value === "string" && value !== "";
}

function isUserResumable(metadata: unknown): boolean {
  return typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) &&
    (metadata as Record<string, unknown>)["userResumable"] === true;
}

function userResumableMetadata(metadata: unknown, value: boolean): Prisma.InputJsonObject {
  const base = typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)
    ? metadata as Prisma.JsonObject
    : {};
  return { ...base, userResumable: value };
}

export async function selectProjectGroupRole(
  prisma: PrismaClient,
  input: {
    readonly projectId: string;
    readonly chatId: string;
    readonly agentRoleId: string;
    readonly actorUserId: string;
  },
): Promise<void> {
  await prisma.$transaction(async (tx) => {
    await lockActiveProjectOrganization(tx, input.projectId);
    const binding = await tx.projectGroupBinding.findFirst({
      where: { projectId: input.projectId, chatId: input.chatId, archivedAt: null },
      select: { id: true, project: { select: { organizationId: true } } },
    });
    if (binding === null) throw new Error("role selection requires an active binding to this chat");
    const role = await tx.organizationAgentRole.findFirst({
      where: {
        id: input.agentRoleId,
        organizationId: binding.project.organizationId,
        disabledAt: null,
      },
      select: { id: true, roleId: true },
    });
    if (role === null) throw new Error(`active role not found: ${input.agentRoleId}`);
    await tx.projectGroupBinding.update({
      where: { id: binding.id },
      data: { selectedAgentRoleId: role.id },
    });
    await tx.auditEntry.create({
      data: {
        projectId: input.projectId,
        actorUserId: input.actorUserId,
        action: "project_group.role_selected",
        metadata: { chatId: input.chatId, roleId: role.roleId },
      },
    });
  });
}

export async function archiveCurrentRoleSession(
  prisma: PrismaClient,
  input: { readonly projectId: string; readonly chatId: string; readonly actorUserId: string },
): Promise<boolean> {
  return prisma.$transaction(async (tx) => {
    await lockActiveProjectOrganization(tx, input.projectId);
    const activeRun = await tx.agentRun.findFirst({
      where: { projectId: input.projectId, status: { in: ["ACTIVE", "WAITING_FOR_USER"] } },
      select: { id: true },
    });
    if (activeRun !== null) throw new Error(`cannot archive a session while run ${activeRun.id} is active`);
    const binding = await tx.projectGroupBinding.findFirst({
      where: { projectId: input.projectId, chatId: input.chatId, archivedAt: null },
      select: { selectedRole: { select: { roleId: true } } },
    });
    if (binding === null) throw new Error("session operation requires an active binding to this chat");
    const session = await tx.agentSession.findFirst({
      where: { projectId: input.projectId, roleId: binding.selectedRole.roleId, archivedAt: null },
      orderBy: { updatedAt: "desc" },
      select: { id: true, metadata: true },
    });
    if (session !== null) {
      await tx.agentSession.update({
        where: { id: session.id },
        data: { archivedAt: new Date(), metadata: userResumableMetadata(session.metadata, true) },
      });
    }
    await tx.auditEntry.create({
      data: {
        projectId: input.projectId,
        actorUserId: input.actorUserId,
        action: "agent_session.new_requested",
        metadata: { chatId: input.chatId, roleId: binding.selectedRole.roleId, archivedSessionId: session?.id ?? null },
      },
    });
    return session !== null;
  });
}

export async function listRoleSessionHistory(
  prisma: PrismaClient,
  input: { readonly projectId: string; readonly chatId: string },
): Promise<readonly { readonly id: string; readonly title: string | null; readonly updatedAt: Date; readonly runCount: number }[]> {
  const binding = await prisma.projectGroupBinding.findFirst({
    where: { projectId: input.projectId, chatId: input.chatId, archivedAt: null },
    select: { selectedRole: { select: { roleId: true } } },
  });
  if (binding === null) throw new Error("session history requires an active binding to this chat");
  const sessions = await prisma.agentSession.findMany({
    where: { projectId: input.projectId, roleId: binding.selectedRole.roleId, archivedAt: { not: null } },
    orderBy: { updatedAt: "desc" },
    take: 10,
    select: { id: true, title: true, updatedAt: true, metadata: true, _count: { select: { runs: true } } },
  });
  return sessions.filter((session) => isUserResumable(session.metadata)).map((session) => ({
    id: session.id,
    title: session.title,
    updatedAt: session.updatedAt,
    runCount: session._count.runs,
  }));
}

export async function resumeRoleSession(
  prisma: PrismaClient,
  input: {
    readonly projectId: string;
    readonly chatId: string;
    readonly sessionId: string;
    readonly actorUserId: string;
  },
): Promise<void> {
  await prisma.$transaction(async (tx) => {
    await lockActiveProjectOrganization(tx, input.projectId);
    const activeRun = await tx.agentRun.findFirst({
      where: { projectId: input.projectId, status: { in: ["ACTIVE", "WAITING_FOR_USER"] } },
      select: { id: true },
    });
    if (activeRun !== null) throw new Error(`cannot resume a session while run ${activeRun.id} is active`);
    const binding = await tx.projectGroupBinding.findFirst({
      where: { projectId: input.projectId, chatId: input.chatId, archivedAt: null },
      select: { selectedRole: { select: { roleId: true } } },
    });
    if (binding === null) throw new Error("session resume requires an active binding to this chat");
    const target = await tx.agentSession.findFirst({
      where: { id: input.sessionId, projectId: input.projectId, roleId: binding.selectedRole.roleId, archivedAt: { not: null } },
      select: { id: true, provider: true, model: true, metadata: true },
    });
    if (target === null || !isUserResumable(target.metadata)) {
      throw new Error("user-resumable archived session not found for the selected role");
    }
    const activeSessions = await tx.agentSession.findMany({
      where: {
        projectId: input.projectId,
        roleId: binding.selectedRole.roleId,
        provider: target.provider,
        model: target.model,
        archivedAt: null,
      },
      select: { id: true, metadata: true },
    });
    const archivedAt = new Date();
    for (const session of activeSessions) {
      await tx.agentSession.update({
        where: { id: session.id },
        data: { archivedAt, metadata: userResumableMetadata(session.metadata, true) },
      });
    }
    await tx.agentSession.update({
      where: { id: target.id },
      data: { archivedAt: null, metadata: userResumableMetadata(target.metadata, false) },
    });
    await tx.auditEntry.create({
      data: {
        projectId: input.projectId,
        actorUserId: input.actorUserId,
        action: "agent_session.resumed",
        metadata: { chatId: input.chatId, roleId: binding.selectedRole.roleId, sessionId: target.id },
      },
    });
  });
}

async function selectedRoleModel(
  prisma: PrismaClient,
  organizationId: string,
  roleId: string,
): Promise<string | null> {
  const role = await prisma.organizationAgentRole.findFirst({
    where: { id: roleId, organizationId, disabledAt: null },
    select: { defaultModel: true },
  });
  if (role === null) throw new Error(`selected role not found: ${roleId}`);
  return role.defaultModel;
}

async function folderBreadcrumb(prisma: PrismaClient, folderId: string): Promise<string> {
  const rows = await prisma.$queryRaw<Array<{ breadcrumb: string }>>(Prisma.sql`
    SELECT cph_folder_breadcrumb(${folderId}) AS breadcrumb
  `);
  const breadcrumb = rows[0]?.breadcrumb;
  if (breadcrumb === undefined) throw new Error(`failed to resolve folder breadcrumb: ${folderId}`);
  return breadcrumb;
}
