/**
 * 授权管理(契约 8.1 矩阵的服务端强制)。
 *
 * 矩阵:
 *  - 创建者(creatorId 不可变):可授/改/收 MANAGE、EDIT、VIEW;自身 creator grant 不可动
 *  - MANAGE 持有者:可授/改/收 EDIT、VIEW;不可碰 MANAGE;不可动创建者
 *  - EDIT/VIEW:无授权能力(requireAccess MANAGE 已挡)
 *  - 网站管理员:走 forceAdjustGrants(不查节点权限,D19),全部留 admin.force_adjust 审计
 *
 * D8:目标节点不可见/无权限 → 404;有权限但矩阵禁止 → 403。
 */

import { Prisma } from "@prisma/client";
import type { FileLibGrant, FileLibNode, PrismaClient } from "@prisma/client";
import { FileLibError, type FileLibRole } from "./model.js";
import { FILE_LIB_AUDIT_ACTIONS, writeFileLibAudit } from "./audit.js";
import {
  requireAccessInTx,
  type AccessDeps,
  type FileLibActor,
  type InitialGrant,
} from "./treeService.js";

export interface GrantDto {
  readonly id: string;
  readonly principalType: "USER" | "GROUP";
  readonly principalId: string;
  readonly role: FileLibRole;
  readonly isCreatorGrant: boolean;
  readonly createdAt: Date;
}

function toDto(grant: FileLibGrant): GrantDto {
  return {
    id: grant.id,
    principalType: grant.principalType,
    principalId: grant.principalId,
    role: grant.role,
    isCreatorGrant: grant.isCreatorGrant,
    createdAt: grant.createdAt,
  };
}

type Tx = Prisma.TransactionClient;
type Deps = AccessDeps & { readonly prisma: PrismaClient };

/** MANAGE 门禁:带 tx 时用调用方事务(与后续写同绳),不带时自开一个。 */
async function requireManage(
  deps: Deps,
  actor: FileLibActor,
  nodeId: string,
  tx?: Tx,
): Promise<{ readonly node: FileLibNode; readonly role: FileLibRole }> {
  if (tx !== undefined) return requireAccessInTx(tx, deps, actor, nodeId, "MANAGE");
  return deps.prisma.$transaction(async (inner) => requireAccessInTx(inner, deps, actor, nodeId, "MANAGE"));
}

/** 列出节点活跃授权(需 MANAGE)。 */
export async function listGrants(
  deps: Deps,
  actor: FileLibActor,
  nodeId: string,
): Promise<readonly GrantDto[]> {
  await requireManage(deps, actor, nodeId);
  const grants = await deps.prisma.fileLibGrant.findMany({
    where: { organizationId: deps.organizationId, nodeId, revokedAt: null },
    orderBy: [{ isCreatorGrant: "desc" }, { createdAt: "asc" }],
  });
  return grants.map(toDto);
}

export interface PutGrantsResult {
  readonly granted: number;
  readonly updated: number;
  readonly grants: readonly GrantDto[];
}

/**
 * 批量授予/修改(upsert 语义):同 principal 已有活跃授权 → 改级别(permission.update);
 * 没有 → 新建(permission.grant)。8.1 矩阵在写之前整体校验。
 */
export async function putGrants(
  deps: Deps,
  actor: FileLibActor,
  nodeId: string,
  items: readonly InitialGrant[],
): Promise<PutGrantsResult> {
  validateGrantItems(items);
  return deps.prisma.$transaction(async (tx) => {
    const { node } = await requireManage(deps, actor, nodeId, tx);
    const isCreator = node.creatorId === actor.userId;
    for (const item of items) {
      if (item.role === "MANAGE" && !isCreator) {
        throw new FileLibError(403, "only_creator_can_grant_manage", "only the creator can grant MANAGE");
      }
      if (item.principalType === "USER" && item.principalId === node.creatorId) {
        throw new FileLibError(403, "cannot_touch_creator", "the creator's grant is immutable");
      }
    }

    let granted = 0;
    let updated = 0;
    for (const item of items) {
      const existing = await tx.fileLibGrant.findFirst({
        where: {
          nodeId: node.id,
          principalType: item.principalType,
          principalId: item.principalId,
          revokedAt: null,
        },
      });
      if (existing !== null) {
        if (existing.isCreatorGrant) {
          throw new FileLibError(403, "cannot_touch_creator", "the creator's grant is immutable");
        }
        if (existing.role !== item.role) {
          await tx.fileLibGrant.update({ where: { id: existing.id }, data: { role: item.role } });
          updated += 1;
          await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.permissionUpdate, node.id, node.pathIds, { ...item });
        }
      } else {
        await tx.fileLibGrant.create({
          data: {
            organizationId: deps.organizationId,
            nodeId: node.id,
            principalType: item.principalType,
            principalId: item.principalId,
            role: item.role,
            createdByUserId: actor.userId,
          },
        });
        granted += 1;
        await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.permissionGrant, node.id, node.pathIds, { ...item });
      }
    }
    const grants = await tx.fileLibGrant.findMany({
      where: { organizationId: deps.organizationId, nodeId: node.id, revokedAt: null },
      orderBy: [{ isCreatorGrant: "desc" }, { createdAt: "asc" }],
    });
    return { granted, updated, grants: grants.map(toDto) };
  });
}

/** 收回授权(需 MANAGE;creator grant 与 MANAGE grant 有额外限制,见 8.1)。 */
export async function revokeGrant(
  deps: Deps,
  actor: FileLibActor,
  nodeId: string,
  grantId: string,
): Promise<void> {
  await deps.prisma.$transaction(async (tx) => {
    const { node } = await requireManage(deps, actor, nodeId, tx);
    const grant = await tx.fileLibGrant.findFirst({
      where: { id: grantId, nodeId: node.id, revokedAt: null },
    });
    if (grant === null) throw new FileLibError(404, "grant_not_found", "grant not found");
    if (grant.isCreatorGrant) {
      throw new FileLibError(403, "cannot_touch_creator", "the creator's grant is immutable");
    }
    if (grant.role === "MANAGE" && node.creatorId !== actor.userId) {
      throw new FileLibError(403, "only_creator_can_revoke_manage", "only the creator can revoke MANAGE");
    }
    await tx.fileLibGrant.update({ where: { id: grant.id }, data: { revokedAt: new Date() } });
    await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.permissionRevoke, node.id, node.pathIds, {
      principalType: grant.principalType,
      principalId: grant.principalId,
      role: grant.role,
    });
  });
}

/**
 * 网站管理员强制调整(D19):凭 node id 操作,不查操作者节点权限;矩阵豁免;
 * 每一条变更都落 admin.force_adjust 审计(高危留痕)。
 */
export async function forceAdjustGrants(
  deps: Deps,
  actor: FileLibActor,
  nodeId: string,
  items: readonly InitialGrant[],
): Promise<PutGrantsResult> {
  if (!actor.isWebsiteAdmin) {
    throw new FileLibError(403, "forbidden", "force adjust requires website administrator");
  }
  validateGrantItems(items);
  return deps.prisma.$transaction(async (tx) => {
    const node = await tx.fileLibNode.findFirst({
      where: { id: nodeId, organizationId: deps.organizationId, deletedAt: null },
    });
    if (node === null) throw new FileLibError(404, "node_not_found", "node not found");

    let granted = 0;
    let updated = 0;
    for (const item of items) {
      const existing = await tx.fileLibGrant.findFirst({
        where: {
          nodeId: node.id,
          principalType: item.principalType,
          principalId: item.principalId,
          revokedAt: null,
        },
      });
      if (existing !== null) {
        if (existing.role !== item.role) {
          await tx.fileLibGrant.update({ where: { id: existing.id }, data: { role: item.role } });
          updated += 1;
          await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.adminForceAdjust, node.id, node.pathIds, {
            change: "update",
            principalType: item.principalType,
            principalId: item.principalId,
            from: existing.role,
            to: item.role,
          });
        }
      } else {
        await tx.fileLibGrant.create({
          data: {
            organizationId: deps.organizationId,
            nodeId: node.id,
            principalType: item.principalType,
            principalId: item.principalId,
            role: item.role,
            createdByUserId: actor.userId,
          },
        });
        granted += 1;
        await audit(tx, deps, actor, FILE_LIB_AUDIT_ACTIONS.adminForceAdjust, node.id, node.pathIds, {
          change: "grant",
          principalType: item.principalType,
          principalId: item.principalId,
          role: item.role,
        });
      }
    }
    const grants = await tx.fileLibGrant.findMany({
      where: { organizationId: deps.organizationId, nodeId: node.id, revokedAt: null },
      orderBy: [{ isCreatorGrant: "desc" }, { createdAt: "asc" }],
    });
    return { granted, updated, grants: grants.map(toDto) };
  });
}

/** 项目独立权限开关(P5/D11):需 MANAGE;状态不变则空操作。 */
export async function setIndependentPermission(
  deps: Deps,
  actor: FileLibActor,
  nodeId: string,
  enabled: boolean,
): Promise<{ readonly enabled: boolean }> {
  return deps.prisma.$transaction(async (tx) => {
    const { node } = await requireManage(deps, actor, nodeId, tx);
    if (node.kind !== "PROJECT") {
      throw new FileLibError(400, "invalid_node_kind", "independent permission applies to projects only");
    }
    const current = await tx.fileLibProjectSettings.findUnique({
      where: { nodeId: node.id },
      select: { independentPermissionsEnabled: true },
    });
    if ((current?.independentPermissionsEnabled ?? false) === enabled) {
      return { enabled }; // 状态未变:空操作,不产生审计
    }
    await tx.fileLibProjectSettings.upsert({
      where: { nodeId: node.id },
      update: { independentPermissionsEnabled: enabled },
      create: { nodeId: node.id, independentPermissionsEnabled: enabled },
    });
    await writeFileLibAudit(tx, {
      action: enabled
        ? FILE_LIB_AUDIT_ACTIONS.independentEnable
        : FILE_LIB_AUDIT_ACTIONS.independentDisable,
      actorUserId: actor.userId,
      organizationId: deps.organizationId,
      objectType: "project",
      objectId: node.id,
      objectPath: node.pathIds,
      detail: { enabled },
    });
    return { enabled };
  });
}

function validateGrantItems(items: readonly InitialGrant[]): void {
  if (items.length === 0) throw new FileLibError(400, "invalid_request", "grants must not be empty");
  const seen = new Set<string>();
  for (const item of items) {
    if (item.principalType !== "USER" && item.principalType !== "GROUP") {
      throw new FileLibError(400, "invalid_request", `bad principalType: ${String(item.principalType)}`);
    }
    if (item.role !== "VIEW" && item.role !== "EDIT" && item.role !== "MANAGE") {
      throw new FileLibError(400, "invalid_request", `bad role: ${String(item.role)}`);
    }
    if (item.principalId.trim() === "") {
      throw new FileLibError(400, "invalid_request", "principalId must not be empty");
    }
    const key = `${item.principalType}:${item.principalId}`;
    if (seen.has(key)) throw new FileLibError(400, "duplicate_principal", `duplicate principal: ${key}`);
    seen.add(key);
  }
}

async function audit(
  tx: Prisma.TransactionClient,
  deps: Deps,
  actor: FileLibActor,
  action: string,
  nodeId: string,
  pathIds: string,
  detail: Record<string, unknown>,
): Promise<void> {
  await writeFileLibAudit(tx, {
    action,
    actorUserId: actor.userId,
    organizationId: deps.organizationId,
    objectType: "grant",
    objectId: nodeId,
    objectPath: pathIds,
    detail,
  });
}
