import { createHash, randomUUID } from "node:crypto";
import { mkdir, rm } from "node:fs/promises";
import { dirname, relative, resolve } from "node:path";
import type { Folder, OrganizationMemberRole, PermissionRole, Prisma, PrismaClient } from "@prisma/client";
import { createPermissionAuthorizer } from "./permission.js";
import { writeAudit } from "./audit.js";
import { lockActiveOrganization, requireActiveOrganizationStatus } from "./org/status.js";
import { scopedFeishuPrincipalId } from "./feishu/identityNamespace.js";

export interface OrganizationProjectPolicy {
  readonly organizationId: string;
  readonly membersCanCreateProjects: boolean;
}

export interface ProjectOnboardingResult {
  readonly projectId: string;
  readonly organizationId: string;
  readonly folderId: string | null;
  readonly workspaceDir: string;
  readonly chatId?: string | undefined;
}

export interface CreateOrgAdminProjectInput {
  readonly organizationId: string;
  readonly actorFeishuOpenId: string;
  readonly name: string;
  readonly workspaceRoot: string;
  readonly folderId?: string | undefined;
  readonly sortKey?: string | undefined;
  /** Stable internal identifier for resumable imports; ordinary callers must omit it. */
  readonly projectId?: string | undefined;
}

export interface CreateFeishuChatProjectInput {
  readonly organizationId: string;
  readonly actorFeishuOpenId: string;
  readonly chatId: string;
  readonly name: string;
  readonly workspaceRoot: string;
  readonly folderId?: string | undefined;
  readonly sortKey?: string | undefined;
}

export interface BindFeishuChatToProjectInput {
  readonly projectId: string;
  readonly actorFeishuOpenId: string;
  readonly chatId: string;
}

export interface ArchiveFeishuChatBindingInput {
  readonly projectId?: string | undefined;
  readonly chatId?: string | undefined;
  readonly actorFeishuOpenId: string;
}

export interface RenameProjectForActorInput {
  readonly organizationId: string;
  readonly projectId: string;
  readonly actorFeishuOpenId: string;
  readonly name: string;
}

export interface CreateFolderInput {
  readonly organizationId: string;
  readonly name: string;
  readonly parentId?: string | undefined;
  readonly sortKey?: string | undefined;
}

export interface MoveProjectToFolderInput {
  readonly projectId: string;
  readonly folderId: string | null;
}

export async function ensureOrganizationProjectSettings(
  prisma: PrismaClient,
  organizationId: string,
): Promise<OrganizationProjectPolicy> {
  return prisma.$transaction(async (tx) => ensureOrganizationProjectSettingsTx(tx, organizationId));
}

export async function setMembersCanCreateProjects(
  prisma: PrismaClient,
  input: { readonly organizationId: string; readonly enabled: boolean },
): Promise<OrganizationProjectPolicy> {
  return prisma.$transaction(async (tx) => {
    await requireActiveOrganizationTx(tx, input.organizationId);
    return tx.organizationProjectSettings.upsert({
      where: { organizationId: input.organizationId },
      update: { membersCanCreateProjects: input.enabled },
      create: {
        organizationId: input.organizationId,
        membersCanCreateProjects: input.enabled,
      },
      select: { organizationId: true, membersCanCreateProjects: true },
    });
  });
}

export async function createFolder(prisma: PrismaClient, input: CreateFolderInput): Promise<Folder> {
  const name = requireNonEmpty(input.name, "folder name");
  return prisma.$transaction(async (tx) => {
    await requireActiveOrganizationTx(tx, input.organizationId);
    if (input.parentId !== undefined) {
      await assertFolderInOrganization(tx, input.parentId, input.organizationId);
    }
    return tx.folder.create({
      data: {
        organizationId: input.organizationId,
        name,
        sortKey: input.sortKey ?? "",
        ...(input.parentId !== undefined ? { parentId: input.parentId } : {}),
      },
    });
  });
}

export async function moveProjectToFolder(
  prisma: PrismaClient,
  input: MoveProjectToFolderInput,
): Promise<{ readonly projectId: string; readonly folderId: string | null }> {
  return prisma.$transaction(async (tx) => {
    const project = await tx.project.findUnique({
      where: { id: input.projectId },
      select: { id: true, organizationId: true },
    });
    if (project === null) throw new Error(`project not found: ${input.projectId}`);
    await requireActiveOrganizationTx(tx, project.organizationId);
    if (input.folderId !== null) {
      await assertFolderInOrganization(tx, input.folderId, project.organizationId);
    }
    const updated = await tx.project.update({
      where: { id: input.projectId },
      data: { folderId: input.folderId },
      select: { id: true, folderId: true },
    });
    return { projectId: updated.id, folderId: updated.folderId };
  });
}

export async function renameProjectForActor(
  prisma: PrismaClient,
  input: RenameProjectForActorInput,
): Promise<{ readonly projectId: string; readonly name: string }> {
  const name = requireNonEmpty(input.name, "project name");
  const project = await prisma.project.findUnique({
    where: { id: input.projectId },
    select: { id: true, organizationId: true, name: true },
  });
  if (project === null) throw new Error(`project not found: ${input.projectId}`);
  if (project.organizationId !== input.organizationId) {
    throw new Error(`project ${project.id} does not belong to organization ${input.organizationId}`);
  }
  const actor = await requireProjectManager(prisma, project.id, project.organizationId, input.actorFeishuOpenId);
  const updated = await prisma.$transaction(async (tx) => {
    await lockActiveOrganization(tx, project.organizationId);
    const current = await tx.project.findUniqueOrThrow({
      where: { id: project.id },
      select: { name: true },
    });
    const renamed = await tx.project.update({
      where: { id: project.id },
      data: { name },
      select: { id: true, name: true },
    });
    await tx.auditEntry.create({
      data: {
        organizationId: project.organizationId,
        projectId: project.id,
        actorUserId: actor.userId,
        action: "project.renamed",
        metadata: { oldName: current.name, newName: name, actorVia: actor.via },
      },
    });
    return renamed;
  });
  return { projectId: updated.id, name: updated.name };
}

export async function createProjectFromOrgAdmin(
  prisma: PrismaClient,
  input: CreateOrgAdminProjectInput,
): Promise<ProjectOnboardingResult> {
  const name = requireNonEmpty(input.name, "project name");
  const actor = await requireActiveOrgMember(prisma, input.organizationId, input.actorFeishuOpenId);
  if (!isOrgAdminRole(actor.role)) {
    throw new Error(`org admin project creation requires OWNER or ADMIN in organization ${input.organizationId}`);
  }
  return createManagedProject(prisma, {
    organizationId: input.organizationId,
    actorUserId: actor.userId,
    actorPrincipalId: actor.principalId,
    feishuConnectionId: actor.connectionId,
    name,
    workspaceRoot: input.workspaceRoot,
    folderId: input.folderId,
    sortKey: input.sortKey,
    projectId: input.projectId,
    chatId: undefined,
  });
}

export async function createProjectFromFeishuChat(
  prisma: PrismaClient,
  input: CreateFeishuChatProjectInput,
): Promise<ProjectOnboardingResult> {
  const chatId = requireNonEmpty(input.chatId, "chat id");
  const name = requireNonEmpty(input.name, "project name");
  const actor = await requireActiveOrgMember(prisma, input.organizationId, input.actorFeishuOpenId);
  const settings = await ensureOrganizationProjectSettings(prisma, input.organizationId);
  if (!settings.membersCanCreateProjects && !isOrgAdminRole(actor.role)) {
    throw new Error(`members cannot create projects in organization ${input.organizationId}`);
  }
  const activeBinding = await prisma.projectGroupBinding.findFirst({
    where: { chatId, archivedAt: null },
    select: { projectId: true },
  });
  if (activeBinding !== null) {
    throw new Error(`Feishu chat ${chatId} is already bound to project ${activeBinding.projectId}`);
  }
  return createManagedProject(prisma, {
    organizationId: input.organizationId,
    actorUserId: actor.userId,
    actorPrincipalId: actor.principalId,
    feishuConnectionId: actor.connectionId,
    name,
    workspaceRoot: input.workspaceRoot,
    folderId: input.folderId,
    sortKey: input.sortKey,
    chatId,
  });
}

export async function bindFeishuChatToProject(
  prisma: PrismaClient,
  input: BindFeishuChatToProjectInput,
): Promise<ProjectOnboardingResult> {
  const chatId = requireNonEmpty(input.chatId, "chat id");
  const project = await prisma.project.findUnique({
    where: { id: input.projectId },
    select: { id: true, organizationId: true, folderId: true, workspaceDir: true },
  });
  if (project === null) throw new Error(`project not found: ${input.projectId}`);
  const actor = await requireProjectManager(prisma, project.id, project.organizationId, input.actorFeishuOpenId);
  await prisma.$transaction(async (tx) => {
    await requireActiveOrganizationTx(tx, project.organizationId);
    const defaultRole = await requireDefaultAgentRole(tx, project.organizationId);
    const activeProjectBinding = await tx.projectGroupBinding.findFirst({
      where: { projectId: project.id, archivedAt: null },
      select: { chatId: true },
    });
    if (activeProjectBinding !== null) {
      throw new Error(`project ${project.id} is already bound to Feishu chat ${activeProjectBinding.chatId}`);
    }
    const activeChatBinding = await tx.projectGroupBinding.findFirst({
      where: { chatId, archivedAt: null },
      select: { projectId: true },
    });
    if (activeChatBinding !== null) {
      throw new Error(`Feishu chat ${chatId} is already bound to project ${activeChatBinding.projectId}`);
    }
    await tx.projectGroupBinding.create({
      data: {
        organizationId: project.organizationId,
        projectId: project.id,
        chatId,
        createdByUserId: actor.userId,
        selectedAgentRoleId: defaultRole.id,
      },
    });
    await replaceProjectGrant(tx, {
      projectId: project.id,
      principalType: "FEISHU_CHAT",
      principalId: scopedChatPrincipalId(actor.connectionId, chatId),
      role: "EDIT",
      createdByUserId: actor.userId,
    });
  });
  await writeAudit(prisma, {
    projectId: project.id,
    actorUserId: actor.userId,
    action: "project.chat_bound",
    metadata: { chatId, actorVia: actor.via },
  });
  return {
    projectId: project.id,
    organizationId: project.organizationId,
    folderId: project.folderId,
    workspaceDir: project.workspaceDir,
    chatId,
  };
}

export async function archiveFeishuChatBinding(
  prisma: PrismaClient,
  input: ArchiveFeishuChatBindingInput,
): Promise<{ readonly archived: boolean; readonly projectId?: string | undefined; readonly chatId?: string | undefined }> {
  if ((input.projectId === undefined || input.projectId === "") && (input.chatId === undefined || input.chatId === "")) {
    throw new Error("archiveFeishuChatBinding requires projectId or chatId");
  }
  if (input.projectId !== undefined && input.projectId !== "" && input.chatId !== undefined && input.chatId !== "") {
    throw new Error("archiveFeishuChatBinding accepts only one of projectId or chatId");
  }
  const binding = await prisma.projectGroupBinding.findFirst({
    where: {
      archivedAt: null,
      ...(input.projectId !== undefined && input.projectId !== "" ? { projectId: input.projectId } : { chatId: input.chatId! }),
    },
    select: { id: true, projectId: true, chatId: true, project: { select: { organizationId: true } } },
  });
  if (binding === null) return { archived: false };
  const actor = await requireProjectManager(prisma, binding.projectId, binding.project.organizationId, input.actorFeishuOpenId);
  await prisma.$transaction(async (tx) => {
    await requireActiveOrganizationTx(tx, binding.project.organizationId);
    await tx.projectGroupBinding.update({
      where: { id: binding.id },
      data: { archivedAt: new Date() },
    });
    await tx.permissionGrant.updateMany({
      where: {
        resourceType: "PROJECT",
        resourceId: binding.projectId,
        principalType: "FEISHU_CHAT",
        principalId: scopedChatPrincipalId(actor.connectionId, binding.chatId),
        revokedAt: null,
      },
      data: { revokedAt: new Date() },
    });
  });
  await writeAudit(prisma, {
    projectId: binding.projectId,
    actorUserId: actor.userId,
    action: "project.chat_binding_archived",
    metadata: { chatId: binding.chatId, actorVia: actor.via },
  });
  return { archived: true, projectId: binding.projectId, chatId: binding.chatId };
}

async function createManagedProject(
  prisma: PrismaClient,
  input: {
    readonly organizationId: string;
    readonly actorUserId: string;
    readonly actorPrincipalId: string;
    readonly feishuConnectionId: string | undefined;
    readonly name: string;
    readonly workspaceRoot: string;
    readonly folderId: string | undefined;
    readonly sortKey?: string | undefined;
    readonly projectId?: string | undefined;
    readonly chatId: string | undefined;
  },
): Promise<ProjectOnboardingResult> {
  const organization = await prisma.organization.findUnique({
    where: { id: input.organizationId },
    select: { id: true, slug: true, status: true },
  });
  if (organization === null) throw new Error(`organization not found: ${input.organizationId}`);
  requireActiveOrganizationStatus(organization.id, organization.status);

  const projectId = input.projectId ?? createProjectId();
  const workspaceDir = projectWorkspaceDir({
    workspaceRoot: input.workspaceRoot,
    organizationSlug: organization.slug,
    projectId,
  });
  let workspaceCreated = false;
  let project: { readonly id: string; readonly organizationId: string; readonly folderId: string | null; readonly workspaceDir: string };
  try {
    project = await prisma.$transaction(async (tx) => {
      await ensureOrganizationProjectSettingsTx(tx, organization.id);
      const folderId = input.folderId ?? (await ensureInboxFolder(tx, organization.id)).id;
      await assertFolderInOrganization(tx, folderId, organization.id);

      await mkdir(dirname(workspaceDir), { recursive: true });
      await mkdir(workspaceDir);
      workspaceCreated = true;

      const created = await tx.project.create({
        data: {
          id: projectId,
          organizationId: organization.id,
          folderId,
          name: input.name,
          workspaceDir,
          createdByUserId: input.actorUserId,
        },
        select: { id: true, organizationId: true, folderId: true, workspaceDir: true },
      });
      await tx.permissionSettings.create({
        data: defaultProjectPermissionSettings(created.id),
      });
      await replaceProjectGrant(tx, {
        projectId: created.id,
        principalType: "USER",
        principalId: input.actorPrincipalId,
        role: "MANAGE",
        createdByUserId: input.actorUserId,
      });
      if (input.chatId !== undefined) {
        const defaultRole = await requireDefaultAgentRole(tx, organization.id);
        await tx.projectGroupBinding.create({
          data: {
            organizationId: organization.id,
            projectId: created.id,
            chatId: input.chatId,
            createdByUserId: input.actorUserId,
            selectedAgentRoleId: defaultRole.id,
          },
        });
        await replaceProjectGrant(tx, {
          projectId: created.id,
          principalType: "FEISHU_CHAT",
          principalId: scopedChatPrincipalId(input.feishuConnectionId, input.chatId),
          role: "EDIT",
          createdByUserId: input.actorUserId,
        });
      }
      return created;
    });
  } catch (error) {
    if (workspaceCreated) {
      try {
        await rm(workspaceDir, { recursive: true });
      } catch (cleanupError) {
        throw new AggregateError(
          [error, cleanupError],
          `project creation failed and workspace cleanup also failed: ${workspaceDir}`,
        );
      }
    }
    throw error;
  }

  await writeAudit(prisma, {
    projectId: project.id,
    actorUserId: input.actorUserId,
    action: input.chatId === undefined ? "project.created" : "project.created_from_feishu_chat",
    metadata: { folderId: project.folderId, workspaceDir, chatId: input.chatId ?? null },
  });

  return {
    projectId: project.id,
    organizationId: project.organizationId,
    folderId: project.folderId,
    workspaceDir: project.workspaceDir,
    ...(input.chatId !== undefined ? { chatId: input.chatId } : {}),
  };
}

async function requireProjectManager(
  prisma: PrismaClient,
  projectId: string,
  organizationId: string,
  actorFeishuOpenId: string,
): Promise<{
  readonly userId: string;
  readonly role: OrganizationMemberRole;
  readonly principalId: string;
  readonly connectionId: string | undefined;
  readonly via: "org-admin" | "project-manage";
}> {
  const actor = await requireActiveOrgMember(prisma, organizationId, actorFeishuOpenId);
  if (isOrgAdminRole(actor.role)) {
    return { ...actor, via: "org-admin" };
  }
  const decision = await createPermissionAuthorizer(prisma).can({
    actor: { feishuOpenId: actorFeishuOpenId },
    action: "collaborator.manage",
    resource: { type: "PROJECT", id: projectId },
  });
  if (!decision.allowed) {
    throw new Error(`project ${projectId} requires MANAGE for ${actorFeishuOpenId}: ${decision.reason}`);
  }
  return { ...actor, via: "project-manage" };
}

async function requireActiveOrgMember(
  prisma: PrismaClient,
  organizationId: string,
  feishuOpenId: string,
): Promise<{
  readonly userId: string;
  readonly role: OrganizationMemberRole;
  readonly principalId: string;
  readonly connectionId: string | undefined;
}> {
  await requireActiveOrganization(prisma, organizationId);
  const identity = await prisma.feishuUserIdentity.findFirst({
    where: { openId: feishuOpenId, connection: { organizationId, status: "ACTIVE" } },
    select: {
      connectionId: true,
      user: { select: {
        id: true,
        organizationMemberships: {
          where: { organizationId, revokedAt: null },
          select: { role: true },
          take: 1,
        },
      } },
    },
  });
  if (identity !== null) {
    const membership = identity.user.organizationMemberships[0];
    if (membership === undefined) {
      throw new Error(`Feishu user ${feishuOpenId} is not an active member of organization ${organizationId}`);
    }
    return {
      userId: identity.user.id,
      role: membership.role,
      principalId: scopedFeishuPrincipalId("USER", identity.connectionId, feishuOpenId),
      connectionId: identity.connectionId,
    };
  }
  // Explicit compatibility path for pre-ADR-0024 rows while the Silo branch
  // completes migration. New bootstrap/member writes always create identities.
  const user = process.env["NODE_ENV"] === "production" ? null : await prisma.user.findUnique({
    where: { feishuOpenId },
    select: {
      id: true,
      organizationMemberships: {
        where: { organizationId, revokedAt: null },
        select: { role: true },
        take: 1,
      },
    },
  });
  if (user === null) {
    throw new Error(`Feishu user ${feishuOpenId} must log in before project onboarding`);
  }
  const membership = user.organizationMemberships[0];
  if (membership === undefined) {
    throw new Error(`Feishu user ${feishuOpenId} is not an active member of organization ${organizationId}`);
  }
  return { userId: user.id, role: membership.role, principalId: feishuOpenId, connectionId: undefined };
}

function scopedChatPrincipalId(connectionId: string | undefined, chatId: string): string {
  return connectionId === undefined ? chatId : scopedFeishuPrincipalId("CHAT", connectionId, chatId);
}

async function ensureOrganizationProjectSettingsTx(
  prisma: Prisma.TransactionClient,
  organizationId: string,
): Promise<OrganizationProjectPolicy> {
  await requireActiveOrganizationTx(prisma, organizationId);
  return prisma.organizationProjectSettings.upsert({
    where: { organizationId },
    update: {},
    create: { organizationId, membersCanCreateProjects: true },
    select: { organizationId: true, membersCanCreateProjects: true },
  });
}

async function ensureInboxFolder(prisma: Prisma.TransactionClient, organizationId: string): Promise<{ readonly id: string }> {
  const existing = await prisma.folder.findFirst({
    where: { organizationId, kind: "SYSTEM_INBOX", archivedAt: null },
    select: { id: true },
  });
  if (existing !== null) return existing;

  // Bootstrap once created a root "Inbox" without kind=SYSTEM_INBOX. Sibling-name
  // uniqueness then makes a second create fail. Recover by promoting that row.
  const legacyRootInbox = await prisma.folder.findFirst({
    where: {
      organizationId,
      parentId: null,
      name: "Inbox",
      archivedAt: null,
    },
    select: { id: true },
  });
  if (legacyRootInbox !== null) {
    return prisma.folder.update({
      where: { id: legacyRootInbox.id },
      data: { kind: "SYSTEM_INBOX" },
      select: { id: true },
    });
  }

  return prisma.folder.create({
    data: { organizationId, name: "Inbox", kind: "SYSTEM_INBOX", sortKey: "000000" },
    select: { id: true },
  });
}

async function assertFolderInOrganization(
  prisma: Prisma.TransactionClient,
  folderId: string,
  organizationId: string,
): Promise<void> {
  const folder = await prisma.folder.findUnique({
    where: { id: folderId },
    select: { organizationId: true, archivedAt: true },
  });
  if (folder === null || folder.archivedAt !== null) {
    throw new Error(`active folder not found: ${folderId}`);
  }
  if (folder.organizationId !== organizationId) {
    throw new Error(`folder ${folderId} is in ${folder.organizationId}, not organization ${organizationId}`);
  }
}

async function requireDefaultAgentRole(
  tx: Prisma.TransactionClient,
  organizationId: string,
): Promise<{ readonly id: string }> {
  const roles = await tx.organizationAgentRole.findMany({
    where: { organizationId, isDefault: true, disabledAt: null },
    select: { id: true },
    take: 2,
  });
  if (roles.length !== 1) {
    throw new Error(`organization ${organizationId} must have exactly one active default Agent role`);
  }
  return roles[0]!;
}

async function requireActiveOrganization(prisma: PrismaClient, organizationId: string): Promise<void> {
  const organization = await prisma.organization.findUnique({
    where: { id: organizationId },
    select: { id: true, status: true },
  });
  if (organization === null) throw new Error(`organization not found: ${organizationId}`);
  requireActiveOrganizationStatus(organization.id, organization.status);
}

async function requireActiveOrganizationTx(prisma: Prisma.TransactionClient, organizationId: string): Promise<void> {
  await lockActiveOrganization(prisma, organizationId);
}

async function replaceProjectGrant(
  prisma: Prisma.TransactionClient,
  input: {
    readonly projectId: string;
    readonly principalType: "USER" | "FEISHU_CHAT";
    readonly principalId: string;
    readonly role: PermissionRole;
    readonly createdByUserId: string;
  },
): Promise<void> {
  await prisma.permissionGrant.updateMany({
    where: {
      resourceType: "PROJECT",
      resourceId: input.projectId,
      principalType: input.principalType,
      principalId: input.principalId,
      revokedAt: null,
    },
    data: { revokedAt: new Date() },
  });
  await prisma.permissionGrant.create({
    data: {
      resourceType: "PROJECT",
      resourceId: input.projectId,
      principalType: input.principalType,
      principalId: input.principalId,
      role: input.role,
      createdByUserId: input.createdByUserId,
    },
  });
}

function defaultProjectPermissionSettings(projectId: string): Prisma.PermissionSettingsCreateInput {
  return {
    resourceType: "PROJECT",
    resourceId: projectId,
    externalShare: "DISABLED",
    comment: "ROLE",
    copyDownload: "ROLE",
    collaboratorMgmt: "MANAGE_ONLY",
    agentTrigger: "ROLE",
    agentCancel: "MANAGE_ONLY",
  };
}

function isOrgAdminRole(role: OrganizationMemberRole): boolean {
  return role === "OWNER" || role === "ADMIN";
}

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

function createProjectId(): string {
  return `project_${randomUUID().replaceAll("-", "")}`;
}

function projectWorkspaceDir(input: {
  readonly workspaceRoot: string;
  readonly organizationSlug: string;
  readonly projectId: string;
}): string {
  const root = resolve(requireNonEmpty(input.workspaceRoot, "workspace root"));
  const dir = resolve(
    root,
    compactWorkspaceSegment("o", input.organizationSlug, "organization slug"),
    compactWorkspaceSegment("p", input.projectId, "project id"),
  );
  const rel = relative(root, dir);
  if (rel === "" || rel.startsWith("..")) {
    throw new Error(`allocated workspace escapes root: ${dir}`);
  }
  return dir;
}

function compactWorkspaceSegment(prefix: "o" | "p", value: string, label: string): string {
  const normalized = safePathSegment(value, label);
  const digest = createHash("sha256").update(normalized).digest("base64url").slice(0, 16);
  return `${prefix}_${digest}`;
}

function safePathSegment(value: string, label: string): string {
  const segment = requireNonEmpty(value, label).replace(/[^A-Za-z0-9._-]+/g, "_");
  if (segment === "." || segment === ".." || segment === "") {
    throw new Error(`${label} is not a safe path segment`);
  }
  return segment;
}
