/**
 * /database/api/* 树与授权端点(契约 9.1/9.2 + C2 过渡搜索)。
 * 约定:绝对路径;actorOrNull 前置;业务全走 filelib 服务层;错误统一 sendRouteError。
 */

import type { FastifyInstance } from "fastify";
import {
  breadcrumb,
  createNode,
  getEffectiveRole,
  listChildren,
  moveNode,
  renameNode,
  softDeleteNode,
  updateNodeDescription,
  type FileLibActor,
  type InitialGrant,
} from "../filelib/treeService.js";
import {
  forceAdjustGrants,
  listGrants,
  putGrants,
  revokeGrant,
  setIndependentPermission,
} from "../filelib/grantService.js";
import { FileLibError } from "../filelib/model.js";
import {
  actorOrNull,
  bodyObject,
  optionalString,
  requireString,
  sendRouteError,
  treeDeps,
  type FileLibRouteDeps,
} from "../filelib/routeShared.js";

export async function registerFileLibRoutes(
  app: FastifyInstance,
  deps: FileLibRouteDeps,
): Promise<void> {
  const tree = treeDeps(deps);
  const grantDeps = { prisma: deps.prisma, organizationId: deps.organizationId, groupResolver: deps.groupResolver };

  /* ------------------------------------------------------------ 身份 */

  // 前端判断能力面用:是否网站管理员(root 创建按钮显隐)。
  app.get("/database/api/me", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    return { userId: actor.userId, isWebsiteAdmin: actor.isWebsiteAdmin };
  });

  /* ------------------------------------------------------------ 树节点 */

  app.get("/database/api/nodes", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const query = request.query as { parentId?: string };
      const parentId = query.parentId === undefined || query.parentId === "" ? null : query.parentId;
      return { nodes: await listChildren(tree, actor, parentId) };
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.post("/database/api/nodes", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const body = bodyObject(request.body);
      const parentIdRaw = body["parentId"];
      if (parentIdRaw !== null && typeof parentIdRaw !== "string") {
        throw new FileLibError(400, "invalid_request", "parentId must be a string or null");
      }
      const kindRaw = requireString(body, "kind");
      if (kindRaw !== "FOLDER" && kindRaw !== "PROJECT") {
        throw new FileLibError(400, "invalid_request", "kind must be FOLDER or PROJECT");
      }
      const grants = parseGrants(body["grants"]);
      const description = optionalString(body, "description");
      const node = await createNode(tree, actor, {
        parentId: parentIdRaw,
        kind: kindRaw,
        name: requireString(body, "name"),
        description: description || undefined,
        grants,
      });
      return reply.status(201).send({ node });
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.get("/database/api/nodes/:id", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const role = await getEffectiveRole(tree, actor, id); // 无权限即 404(D8)
      const node = await deps.prisma.fileLibNode.findFirst({
        where: { id, organizationId: deps.organizationId },
      });
      if (node === null) throw new FileLibError(404, "node_not_found", "node not found");
      const settings = node.kind === "PROJECT"
        ? await deps.prisma.fileLibProjectSettings.findUnique({ where: { nodeId: node.id } })
        : null;
      return {
        node: {
          id: node.id,
          parentId: node.parentId,
          kind: node.kind,
          name: node.name,
          description: node.description,
          role,
          provisionStatus: node.provisionStatus,
          independentPermission: settings?.independentPermissionsEnabled ?? false,
          createdAt: node.createdAt,
          updatedAt: node.updatedAt,
        },
      };
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.patch("/database/api/nodes/:id", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const body = bodyObject(request.body);
      const name = optionalString(body, "name");
      const hasParent = Object.prototype.hasOwnProperty.call(body, "parentId");
      const hasDesc = Object.prototype.hasOwnProperty.call(body, "description");
      if (name === undefined && !hasParent && !hasDesc) {
        throw new FileLibError(400, "invalid_request", "nothing to update (name? parentId? description?)");
      }
      let node;
      if (name !== undefined) node = await renameNode(tree, actor, id, name);
      if (hasParent) {
        const parentId = body["parentId"];
        if (parentId !== null && typeof parentId !== "string") {
          throw new FileLibError(400, "invalid_request", "parentId must be a string or null");
        }
        node = await moveNode(tree, actor, id, parentId);
      }
      if (hasDesc) {
        const d = body["description"];
        if (d !== null && typeof d !== "string") {
          throw new FileLibError(400, "invalid_request", "description must be a string or null");
        }
        node = await updateNodeDescription(tree, actor, id, typeof d === "string" ? d : null);
      }
      return { node };
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.delete("/database/api/nodes/:id", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      await softDeleteNode(tree, actor, id);
      return reply.status(204).send();
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.get("/database/api/nodes/:id/breadcrumb", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      return { breadcrumb: await breadcrumb(tree, actor, id) };
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.get("/database/api/nodes/:id/effective-permission", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      return { role: await getEffectiveRole(tree, actor, id) };
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  /* ------------------------------------------------------------ 授权 */

  app.get("/database/api/nodes/:id/grants", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      return { grants: await listGrants(grantDeps, actor, id) };
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.put("/database/api/nodes/:id/grants", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const body = bodyObject(request.body);
      const items = parseGrants(body["grants"]);
      if (items === undefined) throw new FileLibError(400, "invalid_request", "missing field: grants");
      return await putGrants(grantDeps, actor, id, items);
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.delete("/database/api/nodes/:id/grants/:grantId", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id, grantId } = request.params as { id: string; grantId: string };
      await revokeGrant(grantDeps, actor, id, grantId);
      return reply.status(204).send();
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  // D19 高危通道:网站管理员凭 id 强制调整,全部留 admin.force_adjust 审计。
  app.put("/database/api/admin/nodes/:id/grants", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const body = bodyObject(request.body);
      const items = parseGrants(body["grants"]);
      if (items === undefined) throw new FileLibError(400, "invalid_request", "missing field: grants");
      return await forceAdjustGrants(grantDeps, actor, id, items);
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.put("/database/api/projects/:id/independent-permission", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const body = bodyObject(request.body);
      if (typeof body["enabled"] !== "boolean") {
        throw new FileLibError(400, "invalid_request", "enabled must be a boolean");
      }
      return await setIndependentPermission(grantDeps, actor, id, body["enabled"]);
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  /* ------------------------------------------------------------ Group 搜索(过渡) */

  // 过渡实现:读 hub Team(C2 /groups/search 由 Group 团队交付后切换)。
  app.get("/database/api/groups/search", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const q = ((request.query as { q?: string }).q ?? "").trim();
      const teams = await deps.prisma.team.findMany({
        where: {
          organizationId: deps.organizationId,
          archivedAt: null,
          ...(q === "" ? {} : { name: { contains: q, mode: "insensitive" as const } }),
        },
        take: 20,
        orderBy: { name: "asc" },
        select: { id: true, name: true },
      });
      return { groups: teams.map((t) => ({ id: t.id, name: t.name, breadcrumb: t.name })) };
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });
}

function parseGrants(raw: unknown): InitialGrant[] | undefined {
  if (raw === undefined) return undefined;
  if (!Array.isArray(raw)) throw new FileLibError(400, "invalid_request", "grants must be an array");
  return raw.map((item) => {
    if (typeof item !== "object" || item === null) {
      throw new FileLibError(400, "invalid_request", "grant entries must be objects");
    }
    const grant = item as Record<string, unknown>;
    const principalType = grant["principalType"];
    if (principalType !== "USER" && principalType !== "GROUP") {
      throw new FileLibError(400, "invalid_request", "grant.principalType must be USER or GROUP");
    }
    const role = grant["role"];
    if (role !== "VIEW" && role !== "EDIT" && role !== "MANAGE") {
      throw new FileLibError(400, "invalid_request", "grant.role must be VIEW, EDIT or MANAGE");
    }
    if (typeof grant["principalId"] !== "string" || grant["principalId"] === "") {
      throw new FileLibError(400, "invalid_request", "grant.principalId must be a non-empty string");
    }
    return { principalType, principalId: grant["principalId"], role };
  });
}
