/**
 * 文件库树服务(Phase 1 服务层,Phase 2 路由直接调用)。
 *
 * 语义锚定:
 *  - D8  无权限 → 404 不泄露;越权 → 403(loadChain / requireAccess)
 *  - D11 creator 不可变 + 自动 MANAGE grant;独立权限开关语义在 permission.ts
 *  - D12 move = 本节点 MANAGE + 目标父 EDIT+,事务 + pg 咨询锁防并发成环
 *  - D14 命名规则(model.ts)+ 活跃兄弟唯一(DB 部分唯一索引兜底)
 *  - D15 删除只打标本节点;"任一祖先已删"即整支不可见
 *  - D17 breadcrumb 无 View 的祖先只给占位,不泄露名字
 *  - 树表示:parentId 权威;pathIds 为 id 编码的派生物化路径(name 不入路径,
 *    rename 不重写后代;move 用一次前缀重写维护)
 *
 * 网站管理员(D19)= silo org 的 OWNER/ADMIN(契约 C4 的入驻适配):仅 root 创建
 * 与 force_adjust 特权,不隐式穿透内容权限 —— 本文件所有读路径对它同样走
 * effectiveRole,没有 admin 旁路。
 */

import { randomUUID } from "node:crypto";
import path from "node:path";
import { Prisma } from "@prisma/client";
import type { PrismaClient, FileLibNode } from "@prisma/client";
import {
  FileLibError,
  nameKey,
  normalizeNodeName,
  type FileLibRole,
} from "./model.js";
import { checkAccess, effectiveRole } from "./permission.js";
import type { GroupResolver } from "./groupResolver.js";
import type { VersionStore } from "./versionStore.js";
import { FILE_LIB_AUDIT_ACTIONS, writeFileLibAudit } from "./audit.js";

export interface FileLibActor {
  readonly userId: string;
  /** silo org OWNER/ADMIN(契约 C4 适配)。仅 root 创建/force_adjust 用,不给读旁路。 */
  readonly isWebsiteAdmin: boolean;
}

export interface TreeServiceDeps {
  readonly prisma: PrismaClient;
  readonly groupResolver: GroupResolver;
  readonly versionStore: VersionStore;
  /** 文件库归属的 silo org(ADR-0020 租户隔离,一切查询 scope 到它)。 */
  readonly organizationId: string;
  /** 项目 git 仓库的磁盘根目录;项目仓 = <storageRoot>/<nodeId>。 */
  readonly storageRoot: string;
}

/** 权限判定实际需要的最小依赖(grantService 等兄弟模块复用)。 */
export type AccessDeps = Pick<TreeServiceDeps, "organizationId" | "groupResolver">;

export interface InitialGrant {
  readonly principalType: "USER" | "GROUP";
  readonly principalId: string;
  readonly role: FileLibRole;
}

type Tx = Prisma.TransactionClient;

interface Chain {
  readonly node: FileLibNode;
  /** 根在前、直接父在后;不含 node 自身。 */
  readonly ancestors: readonly FileLibNode[];
}

/* ---------------------------------------------------------------- 内部工具 */

/** pathIds = "/rootId/.../selfId";切出祖先 id(不含 self)。 */
function ancestorIdsOf(node: FileLibNode): string[] {
  return node.pathIds.split("/").filter((seg) => seg !== "").slice(0, -1);
}

/** 取节点 + 祖先链(org scope);D15:自身或任一祖先已删 → 404。 */
async function loadVisibleChain(
  tx: Tx,
  organizationId: string,
  nodeId: string,
): Promise<Chain> {
  const node = await tx.fileLibNode.findFirst({ where: { id: nodeId, organizationId } });
  if (node === null) throw new FileLibError(404, "node_not_found", "node not found");
  const ancestorIds = ancestorIdsOf(node);
  const ancestors = ancestorIds.length === 0
    ? []
    : await tx.fileLibNode.findMany({ where: { organizationId, id: { in: ancestorIds } } });
  if (node.deletedAt !== null || ancestors.some((a) => a.deletedAt !== null)) {
    // D15:已删子树对外"不存在"(D8 不泄露)。
    throw new FileLibError(404, "node_not_found", "node not found");
  }
  const byId = new Map(ancestors.map((a) => [a.id, a]));
  const ordered = ancestorIds
    .map((id) => byId.get(id))
    .filter((a): a is FileLibNode => a !== undefined);
  return { node, ancestors: ordered };
}

/** 数据获取层:把 chain、grants、groups、toggle 装配成纯 reducer 的输入。 */
async function resolveRole(
  tx: Tx,
  deps: AccessDeps,
  actor: FileLibActor,
  chain: Chain,
): Promise<FileLibRole | null> {
  const chainIds = [...chain.ancestors.map((a) => a.id), chain.node.id];
  const grants = await tx.fileLibGrant.findMany({
    where: { organizationId: deps.organizationId, revokedAt: null, nodeId: { in: chainIds } },
    select: { nodeId: true, principalType: true, principalId: true, role: true, isCreatorGrant: true },
  });
  let independentPermissionsEnabled = false;
  if (chain.node.kind === "PROJECT") {
    const settings = await tx.fileLibProjectSettings.findUnique({
      where: { nodeId: chain.node.id },
      select: { independentPermissionsEnabled: true },
    });
    independentPermissionsEnabled = settings?.independentPermissionsEnabled ?? false;
  }
  const groupIds = await deps.groupResolver.resolveMemberGroupIds(actor.userId);
  return effectiveRole({
    nodeId: chain.node.id,
    nodeKind: chain.node.kind,
    ancestorIds: chain.ancestors.map((a) => a.id),
    independentPermissionsEnabled,
    userId: actor.userId,
    groupIds,
    grants,
  });
}

/** D8 门禁:loadVisibleChain + resolveRole + checkAccess,失败抛 FileLibError。 */
async function requireAccess(
  tx: Tx,
  deps: AccessDeps,
  actor: FileLibActor,
  nodeId: string,
  minRole: FileLibRole,
): Promise<Chain & { readonly role: FileLibRole }> {
  const chain = await loadVisibleChain(tx, deps.organizationId, nodeId);
  const role = await resolveRole(tx, deps, actor, chain);
  const verdict = checkAccess(role, minRole);
  if (!verdict.allowed) {
    throw verdict.reason === "not_found"
      ? new FileLibError(404, "node_not_found", "node not found")
      : new FileLibError(403, "forbidden", `requires ${minRole}`);
  }
  return { ...chain, role: verdict.role };
}

/**
 * 兄弟模块(grantService 等)共用的 tx 内门禁:在调用方自己的事务里做
 * 权限校验,校验与后续写同一根事务绳,避免 check-tx / write-tx 之间的竞态。
 */
export async function requireAccessInTx(
  tx: Tx,
  deps: AccessDeps,
  actor: FileLibActor,
  nodeId: string,
  minRole: FileLibRole,
): Promise<Chain & { readonly role: FileLibRole }> {
  return requireAccess(tx, deps, actor, nodeId, minRole);
}

/** P2002(活跃兄弟名部分唯一索引)→ 409。 */
function rethrowNameConflict(error: unknown, name: string): never {
  if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
    throw new FileLibError(409, "name_conflict", `an active sibling named "${name}" already exists`);
  }
  throw error;
}

function nodeAction(kind: FileLibNode["kind"], verb: "Create" | "Rename" | "Move" | "Delete"): string {
  const table = kind === "PROJECT"
    ? { Create: FILE_LIB_AUDIT_ACTIONS.projectCreate, Rename: FILE_LIB_AUDIT_ACTIONS.projectRename, Move: FILE_LIB_AUDIT_ACTIONS.projectMove, Delete: FILE_LIB_AUDIT_ACTIONS.projectDelete }
    : { Create: FILE_LIB_AUDIT_ACTIONS.folderCreate, Rename: FILE_LIB_AUDIT_ACTIONS.folderRename, Move: FILE_LIB_AUDIT_ACTIONS.folderMove, Delete: FILE_LIB_AUDIT_ACTIONS.folderDelete };
  return table[verb];
}

function validateInitialGrants(actor: FileLibActor, grants: readonly InitialGrant[]): void {
  const seen = new Set<string>();
  for (const grant of grants) {
    const key = `${grant.principalType}:${grant.principalId}`;
    if (seen.has(key)) throw new FileLibError(400, "duplicate_principal", `duplicate grant principal: ${key}`);
    seen.add(key);
    if (grant.principalType === "USER" && grant.principalId === actor.userId) {
      throw new FileLibError(400, "duplicate_principal", "creator already holds MANAGE via the creator grant");
    }
    // v1:不校验 group 存在性(C2 未提供批量校验口;给不存在 group 的授权天然无效,不危害)。
  }
}

/* ---------------------------------------------------------------- 公共操作 */

export interface CreateNodeInput {
  readonly parentId: string | null;
  readonly kind: "FOLDER" | "PROJECT";
  readonly name: string;
  readonly description?: string | undefined;
  readonly grants?: readonly InitialGrant[] | undefined;
}

/**
 * 创建文件夹/项目。root 创建仅网站管理员(契约 2.1);非 root 需父节点 EDIT+。
 * creator 自动 MANAGE(D11);项目走 provisioning 状态机:PROVISIONING → init → READY。
 */
export async function createNode(
  deps: TreeServiceDeps,
  actor: FileLibActor,
  input: CreateNodeInput,
): Promise<FileLibNode> {
  const name = normalizeNodeName(input.name);
  const initialGrants = input.grants ?? [];
  validateInitialGrants(actor, initialGrants);

  const id = randomUUID();
  let pathIds: string;
  let storageDir: string | null = null;

  const created = await deps.prisma.$transaction(async (tx) => {
    if (input.parentId === null) {
      if (!actor.isWebsiteAdmin) {
        throw new FileLibError(403, "forbidden", "root creation requires website administrator");
      }
      pathIds = `/${id}`;
    } else {
      const parent = await requireAccess(tx, deps, actor, input.parentId, "EDIT");
      if (parent.node.kind !== "FOLDER") {
        throw new FileLibError(400, "invalid_parent", "projects cannot have children");
      }
      pathIds = `${parent.node.pathIds}/${id}`;
    }

    if (input.kind === "PROJECT") {
      storageDir = path.join(deps.storageRoot, id);
    }

    let node: FileLibNode;
    try {
      node = await tx.fileLibNode.create({
        data: {
          id,
          organizationId: deps.organizationId,
          parentId: input.parentId,
          kind: input.kind,
          name,
          nameLower: nameKey(name),
          pathIds,
          creatorId: actor.userId,
          provisionStatus: input.kind === "PROJECT" ? "PROVISIONING" : "READY",
          storageDir,
          ...(input.description !== undefined && input.description.trim() !== ""
            ? { description: input.description.trim() }
            : {}),
        },
      });
    } catch (error) {
      rethrowNameConflict(error, name);
    }

    await tx.fileLibGrant.create({
      data: {
        organizationId: deps.organizationId,
        nodeId: id,
        principalType: "USER",
        principalId: actor.userId,
        role: "MANAGE",
        isCreatorGrant: true,
        createdByUserId: actor.userId,
      },
    });
    for (const grant of initialGrants) {
      await tx.fileLibGrant.create({
        data: {
          organizationId: deps.organizationId,
          nodeId: id,
          principalType: grant.principalType,
          principalId: grant.principalId,
          role: grant.role,
          createdByUserId: actor.userId,
        },
      });
    }
    if (input.kind === "PROJECT") {
      await tx.fileLibProjectSettings.create({
        data: { nodeId: id, independentPermissionsEnabled: false },
      });
    }

    await writeFileLibAudit(tx, {
      action: nodeAction(input.kind, "Create"),
      actorUserId: actor.userId,
      organizationId: deps.organizationId,
      objectType: input.kind === "PROJECT" ? "project" : "folder",
      objectId: id,
      objectPath: pathIds,
      detail: { name, parentId: input.parentId, initialGrants: initialGrants.length },
    });
    for (const grant of initialGrants) {
      await writeFileLibAudit(tx, {
        action: FILE_LIB_AUDIT_ACTIONS.permissionGrant,
        actorUserId: actor.userId,
        organizationId: deps.organizationId,
        objectType: "grant",
        objectId: id,
        objectPath: pathIds,
        detail: { principalType: grant.principalType, principalId: grant.principalId, role: grant.role },
      });
    }
    return node;
  });

  // provisioning 状态机(Metis 风险#1):DB 行已持久,init 失败 → FAILED 可重试/对账。
  if (input.kind === "PROJECT" && storageDir !== null) {
    try {
      await deps.versionStore.init(storageDir);
      return await deps.prisma.fileLibNode.update({
        where: { id: created.id },
        data: { provisionStatus: "READY" },
      });
    } catch (error) {
      await deps.prisma.fileLibNode
        .update({ where: { id: created.id }, data: { provisionStatus: "FAILED" } })
        .catch(() => undefined);
      throw new FileLibError(500, "provision_failed", `repository initialization failed: ${String(error)}`);
    }
  }
  return created;
}

/** 重命名(需本节点 MANAGE,契约 8.2)。id 路径不含 name,后代无需重写。 */
export async function renameNode(
  deps: TreeServiceDeps,
  actor: FileLibActor,
  nodeId: string,
  rawName: string,
): Promise<FileLibNode> {
  const name = normalizeNodeName(rawName);
  return deps.prisma.$transaction(async (tx) => {
    const { node } = await requireAccess(tx, deps, actor, nodeId, "MANAGE");
    let updated: FileLibNode;
    try {
      updated = await tx.fileLibNode.update({
        where: { id: node.id },
        data: { name, nameLower: nameKey(name) },
      });
    } catch (error) {
      rethrowNameConflict(error, name);
    }
    await writeFileLibAudit(tx, {
      action: nodeAction(node.kind, "Rename"),
      actorUserId: actor.userId,
      organizationId: deps.organizationId,
      objectType: node.kind === "PROJECT" ? "project" : "folder",
      objectId: node.id,
      objectPath: node.pathIds,
      detail: { from: node.name, to: name },
    });
    return updated;
  });
}

/**
 * 移动(D12):本节点 MANAGE + 目标父 EDIT+(移到 root 需网站管理员);
 * 事务 + org 级咨询锁防并发成环;后代 pathIds 一次前缀重写。
 */
export async function moveNode(
  deps: TreeServiceDeps,
  actor: FileLibActor,
  nodeId: string,
  newParentId: string | null,
): Promise<FileLibNode> {
  return deps.prisma.$transaction(async (tx) => {
    await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${"filelib:tree:" + deps.organizationId}))`;

    const { node } = await requireAccess(tx, deps, actor, nodeId, "MANAGE");
    if (node.parentId === newParentId) return node;

    let newPathIds: string;
    if (newParentId === null) {
      if (!actor.isWebsiteAdmin) {
        throw new FileLibError(403, "forbidden", "moving to root requires website administrator");
      }
      newPathIds = `/${node.id}`;
    } else {
      const parent = await requireAccess(tx, deps, actor, newParentId, "EDIT");
      if (parent.node.kind !== "FOLDER") {
        throw new FileLibError(400, "invalid_parent", "projects cannot have children");
      }
      if (parent.node.id === node.id || parent.node.pathIds.startsWith(`${node.pathIds}/`)) {
        throw new FileLibError(400, "move_into_own_subtree", "cannot move a node into its own subtree");
      }
      newPathIds = `${parent.node.pathIds}/${node.id}`;
    }

    const oldPrefix = node.pathIds;
    let updated: FileLibNode;
    try {
      updated = await tx.fileLibNode.update({
        where: { id: node.id },
        data: { parentId: newParentId, pathIds: newPathIds },
      });
      // 派生列维护:整支后代的前缀重写(id 编码,与 name 无关)。
      await tx.$executeRaw`
        UPDATE "FileLibNode"
        SET "pathIds" = ${newPathIds} || substring("pathIds" from ${oldPrefix.length + 1}::int)
        WHERE "organizationId" = ${deps.organizationId}
          AND "pathIds" LIKE ${oldPrefix + "/%"}
      `;
    } catch (error) {
      rethrowNameConflict(error, node.name);
    }
    await writeFileLibAudit(tx, {
      action: nodeAction(node.kind, "Move"),
      actorUserId: actor.userId,
      organizationId: deps.organizationId,
      objectType: node.kind === "PROJECT" ? "project" : "folder",
      objectId: node.id,
      objectPath: newPathIds,
      detail: { fromParentId: node.parentId, toParentId: newParentId },
    });
    return updated;
  });
}

/** 软删除(D15):只打标本节点,后代靠"任一祖先已删"过滤;需 MANAGE。 */
export async function softDeleteNode(
  deps: TreeServiceDeps,
  actor: FileLibActor,
  nodeId: string,
): Promise<void> {
  await deps.prisma.$transaction(async (tx) => {
    const { node } = await requireAccess(tx, deps, actor, nodeId, "MANAGE");
    await tx.fileLibNode.update({ where: { id: node.id }, data: { deletedAt: new Date() } });
    await writeFileLibAudit(tx, {
      action: nodeAction(node.kind, "Delete"),
      actorUserId: actor.userId,
      organizationId: deps.organizationId,
      objectType: node.kind === "PROJECT" ? "project" : "folder",
      objectId: node.id,
      objectPath: node.pathIds,
      detail: { name: node.name },
    });
  });
}

/** 自查生效权限(契约 9.2 effective-permission)。D8:null 角色即不可见,404。 */
export async function getEffectiveRole(
  deps: TreeServiceDeps,
  actor: FileLibActor,
  nodeId: string,
): Promise<FileLibRole | null> {
  return deps.prisma.$transaction(async (tx) => {
    const chain = await loadVisibleChain(tx, deps.organizationId, nodeId);
    const role = await resolveRole(tx, deps, actor, chain);
    if (role === null) throw new FileLibError(404, "node_not_found", "node not found");
    return role;
  });
}

export interface BreadcrumbEntry {
  readonly depth: number;
  /** D17:无 View 的祖先 id/name 都为 null(不泄露)。 */
  readonly id: string | null;
  readonly name: string | null;
  readonly kind: "FOLDER" | "PROJECT";
}

/** D17 面包屑:需 self VIEW;链上每个节点单独算权限,无 View 只留占位。 */
export async function breadcrumb(
  deps: TreeServiceDeps,
  actor: FileLibActor,
  nodeId: string,
): Promise<readonly BreadcrumbEntry[]> {
  return deps.prisma.$transaction(async (tx) => {
    const chain = await loadVisibleChain(tx, deps.organizationId, nodeId);
    const selfRole = await resolveRole(tx, deps, actor, chain);
    if (checkAccess(selfRole, "VIEW").allowed !== true) {
      throw new FileLibError(404, "node_not_found", "node not found");
    }
    const groupIds = await deps.groupResolver.resolveMemberGroupIds(actor.userId);
    const chainNodes = [...chain.ancestors, chain.node];
    const chainIds = chainNodes.map((n) => n.id);
    const allGrants = await tx.fileLibGrant.findMany({
      where: { organizationId: deps.organizationId, revokedAt: null, nodeId: { in: chainIds } },
      select: { nodeId: true, principalType: true, principalId: true, role: true, isCreatorGrant: true },
    });
    const settings = chain.node.kind === "PROJECT"
      ? await tx.fileLibProjectSettings.findUnique({
          where: { nodeId: chain.node.id },
          select: { independentPermissionsEnabled: true },
        })
      : null;

    return chainNodes.map((current, depth) => {
      const role = effectiveRole({
        nodeId: current.id,
        nodeKind: current.kind,
        ancestorIds: chainNodes.slice(0, depth).map((n) => n.id),
        independentPermissionsEnabled:
          current.id === chain.node.id ? settings?.independentPermissionsEnabled ?? false : false,
        userId: actor.userId,
        groupIds,
        grants: allGrants,
      });
      const visible = role !== null;
      return {
        depth,
        id: visible ? current.id : null,
        name: visible ? current.name : null,
        kind: current.kind,
      };
    });
  });
}

export interface ChildNodeDto {
  readonly id: string;
  readonly parentId: string | null;
  readonly kind: "FOLDER" | "PROJECT";
  readonly name: string;
  readonly role: FileLibRole;
  readonly createdAt: Date;
  readonly updatedAt: Date;
}

/** 列子节点(parentId=null 列 root);只返回调用者有 View 的(D8/P7)。 */
export async function listChildren(
  deps: TreeServiceDeps,
  actor: FileLibActor,
  parentId: string | null,
): Promise<readonly ChildNodeDto[]> {
  return deps.prisma.$transaction(async (tx) => {
    let parentAncestorIds: string[] = [];
    if (parentId !== null) {
      const parent = await requireAccess(tx, deps, actor, parentId, "VIEW");
      parentAncestorIds = [...parent.ancestors.map((a) => a.id), parent.node.id];
    }
    const children = await tx.fileLibNode.findMany({
      where: { organizationId: deps.organizationId, parentId, deletedAt: null },
      orderBy: [{ kind: "asc" }, { nameLower: "asc" }],
    });
    if (children.length === 0) return [];

    // D13:一次请求只 resolve 一次组、拉一次 grant 集,批量计算,不做 per-child 往返。
    const idsToFetch = [...parentAncestorIds, ...children.map((c) => c.id)];
    const allGrants = await tx.fileLibGrant.findMany({
      where: { organizationId: deps.organizationId, revokedAt: null, nodeId: { in: idsToFetch } },
      select: { nodeId: true, principalType: true, principalId: true, role: true, isCreatorGrant: true },
    });
    const projectIds = children.filter((c) => c.kind === "PROJECT").map((c) => c.id);
    const settingsRows = projectIds.length === 0
      ? []
      : await tx.fileLibProjectSettings.findMany({
          where: { nodeId: { in: projectIds } },
          select: { nodeId: true, independentPermissionsEnabled: true },
        });
    const toggleByNode = new Map(settingsRows.map((s) => [s.nodeId, s.independentPermissionsEnabled]));
    const groupIds = await deps.groupResolver.resolveMemberGroupIds(actor.userId);

    const out: ChildNodeDto[] = [];
    for (const child of children) {
      const role = effectiveRole({
        nodeId: child.id,
        nodeKind: child.kind,
        ancestorIds: parentAncestorIds,
        independentPermissionsEnabled: toggleByNode.get(child.id) ?? false,
        userId: actor.userId,
        groupIds,
        grants: allGrants,
      });
      if (role === null) continue;
      out.push({
        id: child.id,
        parentId: child.parentId,
        kind: child.kind,
        name: child.name,
        role,
        createdAt: child.createdAt,
        updatedAt: child.updatedAt,
      });
    }
    return out;
  });
}

/** 更新节点简介（需 EDIT+；不记审计，非权限敏感的内容字段）。 */
export async function updateNodeDescription(
  deps: TreeServiceDeps,
  actor: FileLibActor,
  nodeId: string,
  description: string | null,
): Promise<FileLibNode> {
  return deps.prisma.$transaction(async (tx) => {
    const { node } = await requireAccessInTx(tx, deps, actor, nodeId, "EDIT");
    return tx.fileLibNode.update({
      where: { id: node.id },
      data: { description: description?.trim() || null },
    });
  });
}
