import { internalQuery, mutation, query } from "./_generated/server";
import { api } from "./_generated/api";
import { v } from "convex/values";
import { requireUserId } from "./_utils/auth";
import { nowIso } from "./_utils/time";
import { collectSubtree } from "./_utils/documentTree";
import { enqueueIngestDocumentJob } from "./_utils/ingestJobs";
import { extractTextFromDocumentContent } from "./_utils/text";
import { getCanonicalDocumentByBusinessId, getCanonicalParentDocumentId } from "./_utils/documentRecord";

const accessScope = v.union(v.literal("private"), v.literal("shared"), v.literal("public"));

type SharePermission = "read" | "edit";

type SharePolicy = { permission: SharePermission; disableDownload: boolean; disableCopy: boolean };

type CopyTreeDocument = {
  _id: unknown;
  id: string;
  title: string | null;
  parent_id: string | null;
  workspace_id: string;
  access_scope: "private" | "shared" | "public" | null;
  sort_order: number | null;
  created_at: string | null;
  content: unknown;
  user_id: string;
  deleted_at: string | null;
};

function normalizeTitle(title: string | null | undefined): string {
  const safe = String(title ?? "").trim();
  return safe.length > 0 ? safe : "无标题";
}

function makeUniqueTitle(baseTitle: string, existing: Set<string>): string {
  const base = normalizeTitle(baseTitle);
  if (!existing.has(base)) {
    existing.add(base);
    return base;
  }

  const first = `${base} 副本`;
  if (!existing.has(first)) {
    existing.add(first);
    return first;
  }

  for (let i = 2; i < 1000; i += 1) {
    const candidate = `${base} 副本 ${i}`;
    if (!existing.has(candidate)) {
      existing.add(candidate);
      return candidate;
    }
  }

  const fallback = `${base} 副本 ${Date.now()}`;
  existing.add(fallback);
  return fallback;
}

function extractBlocksFromContent(content: unknown): unknown[] {
  if (Array.isArray(content)) {
    return content as unknown[];
  }
  if (content && typeof content === "object" && Array.isArray((content as { blocks?: unknown[] }).blocks)) {
    return ((content as { blocks?: unknown[] }).blocks ?? []) as unknown[];
  }
  return [];
}

function composeContentWithBlocks(content: unknown, blocks: unknown[]): unknown {
  if (Array.isArray(content)) {
    return blocks as unknown[];
  }
  if (content && typeof content === "object") {
    return {
      ...(content as Record<string, unknown>),
      blocks,
    };
  }
  return { blocks };
}

function appendPageReferenceBlock(content: unknown, pageId: string, title: string): unknown {
  const blocks = extractBlocksFromContent(content);
  const pageReferenceBlock = {
    id: typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now()}_${Math.random().toString(16).slice(2)}`,
    type: "pageReference",
    props: {
      pageId,
      title: normalizeTitle(title),
    },
  };
  return composeContentWithBlocks(content, [...blocks, pageReferenceBlock]);
}

function getChildrenSorted(childrenByParent: Map<string | null, CopyTreeDocument[]>, parentId: string | null): CopyTreeDocument[] {
  const list = childrenByParent.get(parentId) ?? [];
  return [...list].sort((a, b) => {
    const orderA = a.sort_order ?? Number.MAX_SAFE_INTEGER;
    const orderB = b.sort_order ?? Number.MAX_SAFE_INTEGER;
    if (orderA !== orderB) return orderA - orderB;
    const timeA = new Date(a.created_at ?? 0).getTime();
    const timeB = new Date(b.created_at ?? 0).getTime();
    return timeA - timeB;
  });
}

async function copyMindmapsForDocument(ctx: any, sourceDocId: string, targetDocId: string) {
  return await ctx.runMutation(api.mindmaps.copyByDocument, {
    sourceDocId,
    targetDocId,
  });
}

async function assertDocumentVisibleToUser(ctx: any, doc: any, userId: string, errorMessage = "无权限") {
  await requireWorkspaceMember(ctx, doc.workspace_id, userId);

  if (doc.user_id === userId) {
    return;
  }

  if (doc.access_scope === "public") {
    return;
  }

  const policy = (await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
  if (!policy) {
    throw new Error(errorMessage);
  }
}

async function createDocumentRecord(
  ctx: any,
  args: {
    id: string;
    workspaceId: string;
    parentId: string | null;
    title?: string | null;
    accessScope: "private" | "shared" | "public";
    content?: unknown;
  },
) {
  const userId = await requireUserId(ctx);
  const existing = await getCanonicalDocumentByBusinessId<any>(ctx, args.id);
  if (existing && existing.deleted_at == null) {
    throw new Error("页面已存在");
  }

  const siblings = await ctx.db
    .query("documents")
    .withIndex("by_workspace_parent", (q: any) => q.eq("workspace_id", args.workspaceId).eq("parent_id", args.parentId))
    .collect();

  const sortOrder = siblings.filter((d: any) => d.deleted_at == null).length;
  const ts = nowIso();
  const title = (args.title ?? "无标题") || "无标题";
  const content = typeof args.content === "undefined" ? [] : args.content;
  const rawText = extractTextFromDocumentContent(content);

  await ctx.db.insert("documents", {
    id: args.id,
    user_id: userId,
    workspace_id: args.workspaceId,
    parent_id: args.parentId,
    title,
    content,
    content_revision: 0,
    content_conflict_key: `${args.id}:0`,
    raw_text: rawText,
    access_scope: args.accessScope,
    sort_order: sortOrder,
    is_starred: false,
    is_template: false,

    wide_layout: false,
    use_small_text: false,
    show_heading_numbers: true,
    show_toc: false,
    show_structure: false,
    protect_editing: false,
    show_word_count: true,
    collapse_backlinks: false,
    page_font: "default",
    layout_density: "normal",
    hide_child_pages: false,
    show_block_ref_count: false,
    embed_default_block_id: null,
    word_count: 0,
    character_count: 0,
    block_count: 0,
    todo_total_count: 0,
    todo_done_count: 0,
    created_at: ts,
    updated_at: ts,
    deleted_at: null,
    deleted_by: null,
  });

  return {
    id: args.id,
    title,
    parent_id: args.parentId,
    sort_order: sortOrder,
    is_starred: false,
    created_at: ts,
    updated_at: ts,
    workspace_id: args.workspaceId,
    access_scope: args.accessScope,
    is_template: false,
  };
}

async function updateDocumentContentRecord(
  ctx: any,
  args: {
    id: string;
    content: unknown;
    expectedRevision?: number | null;
    conflictDetectionKey?: string | null;
  },
) {
  const userId = await requireUserId(ctx);

  const doc = await getCanonicalDocumentByBusinessId<any>(ctx, args.id);
  if (!doc) throw new Error("页面不存在");
  if (doc.deleted_at != null) throw new Error("页面不存在");
  await requireWorkspaceMember(ctx, doc.workspace_id, userId);

  if (doc.user_id !== userId) {
    if (doc.access_scope === "public") throw new Error("无权限");
    const perm = (await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
    if (perm !== "edit") throw new Error("无权限");
  }

  const currentRevision = doc.content_revision ?? 0;
  const currentConflictDetectionKey =
    doc.content_conflict_key ??
    `${args.id}:${currentRevision}`;

  if (
    typeof args.expectedRevision === "number" &&
    Number.isInteger(args.expectedRevision) &&
    args.expectedRevision >= 0 &&
    args.expectedRevision !== currentRevision
  ) {
    throw new Error("正文内容已变更，请刷新后重试");
  }

  if (
    typeof args.conflictDetectionKey === "string" &&
    args.conflictDetectionKey.trim() &&
    args.conflictDetectionKey.trim() !== currentConflictDetectionKey
  ) {
    throw new Error("正文冲突检测失败，请刷新后重试");
  }
  const ts = nowIso();
  const rawText = extractTextFromDocumentContent(args.content);
  const nextRevision = currentRevision + 1;
  const nextConflictDetectionKey = `${args.id}:${nextRevision}`;
  await ctx.db.patch(doc._id, {
    content: args.content,
    content_revision: nextRevision,
    content_conflict_key: nextConflictDetectionKey,
    raw_text: rawText,
    updated_at: ts,
  });

  await enqueueIngestDocumentJob(ctx, { userId: doc.user_id, documentId: args.id, debounceMs: 1500 });
  return {
    ok: true,
    updated_at: ts,
    revision: nextRevision,
    conflict_detection_key: nextConflictDetectionKey,
  };
}

async function duplicateDocumentRecord(
  ctx: any,
  args: {
    sourceId: string;
    newId: string;
    title?: string | null;
  },
) {
  const userId = await requireUserId(ctx);

  const source = await getCanonicalDocumentByBusinessId<any>(ctx, args.sourceId);
  if (!source) throw new Error("页面不存在或无权限访问");
  if (source.user_id !== userId) throw new Error("页面不存在或无权限访问");

  const existingTarget = await getCanonicalDocumentByBusinessId(ctx, args.newId);
  if (existingTarget && existingTarget.deleted_at == null) {
    throw new Error("目标页面已存在");
  }

  const siblings = await ctx.db
    .query("documents")
    .withIndex("by_workspace_parent", (q: any) =>
      q.eq("workspace_id", source.workspace_id).eq("parent_id", source.parent_id),
    )
    .collect();

  const sortOrder = siblings.filter((d: any) => d.deleted_at == null).length;
  const ts = nowIso();
  const baseTitle = (source.title ?? "无标题").trim() ? (source.title ?? "无标题").trim() : "无标题";
  const title = (args.title ?? `${baseTitle} 副本`) || `${baseTitle} 副本`;
  const rawText = extractTextFromDocumentContent(source.content ?? []);

  await ctx.db.insert("documents", {
    id: args.newId,
    user_id: userId,
    workspace_id: source.workspace_id,
    parent_id: source.parent_id,
    title,
    content: source.content ?? [],
    content_revision: 0,
    content_conflict_key: `${args.newId}:0`,
    raw_text: rawText,
    access_scope: source.access_scope,
    sort_order: sortOrder,
    is_starred: false,
    is_template: false,

    wide_layout: source.wide_layout ?? false,
    use_small_text: source.use_small_text ?? false,
    show_heading_numbers: source.show_heading_numbers ?? true,
    show_toc: source.show_toc ?? false,
    show_structure: source.show_structure ?? false,
    protect_editing: source.protect_editing ?? false,
    show_word_count: source.show_word_count ?? true,
    collapse_backlinks: (source as any).collapse_backlinks ?? false,
    page_font: (source as any).page_font ?? "default",
    layout_density: (source as any).layout_density ?? "normal",
    hide_child_pages: (source as any).hide_child_pages ?? false,
    show_block_ref_count: (source as any).show_block_ref_count ?? false,
    embed_default_block_id: (source as any).embed_default_block_id ?? null,
    word_count: source.word_count ?? 0,
    character_count: source.character_count ?? 0,
    block_count: source.block_count ?? 0,
    todo_total_count: (source as any).todo_total_count ?? 0,
    todo_done_count: (source as any).todo_done_count ?? 0,

    created_at: ts,
    updated_at: ts,
    deleted_at: null,
    deleted_by: null,
  });

  return {
    id: args.newId,
    title,
    parent_id: source.parent_id ?? null,
    sort_order: sortOrder,
    workspace_id: source.workspace_id,
    access_scope: source.access_scope,
    created_at: ts,
    updated_at: ts,
  };
}

async function requireWorkspaceMember(ctx: any, workspaceId: string, userId: string) {
  const member = await ctx.db
    .query("workspace_members")
    .withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", workspaceId).eq("user_id", userId))
    .first();
  if (!member) {
    throw new Error("无权限");
  }
  return member;
}

async function resolveSharePolicy(ctx: any, doc: any, userId: string): Promise<SharePolicy | null> {
  const direct = await ctx.db
    .query("document_shares")
    .withIndex("by_doc_user", (q: any) => q.eq("document_id", doc.id).eq("shared_with_user_id", userId))
    .first();
  if (direct) {
    return {
      permission: direct.permission as SharePermission,
      disableDownload: Boolean((direct as any).disable_download),
      disableCopy: Boolean((direct as any).disable_copy),
    };
  }

  let parentId: string | null = doc.parent_id ?? null;
  for (let depth = 0; depth < 60 && parentId; depth += 1) {
    const parentShare = await ctx.db
      .query("document_shares")
      .withIndex("by_doc_user", (q: any) => q.eq("document_id", parentId).eq("shared_with_user_id", userId))
      .first();
    if (parentShare && parentShare.include_descendants) {
      return {
        permission: parentShare.permission as SharePermission,
        disableDownload: Boolean((parentShare as any).disable_download),
        disableCopy: Boolean((parentShare as any).disable_copy),
      };
    }

    parentId = await getCanonicalParentDocumentId(ctx, parentId);
  }

  return null;
}

async function resolveSharePermission(ctx: any, doc: any, userId: string): Promise<SharePermission | null> {
  const policy = await resolveSharePolicy(ctx, doc, userId);
  return policy?.permission ?? null;
}

async function resolveGroupSharePolicy(ctx: any, doc: any, userId: string): Promise<SharePolicy | null> {
  const memberships = await ctx.db
    .query("group_members")
    .withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", doc.workspace_id).eq("user_id", userId))
    .collect();

  const groupIds = new Set<string>(memberships.map((m: any) => String(m.group_id)));
  if (groupIds.size === 0) return null;

  const permissionFromGroup = async (documentId: string, groupId: string): Promise<SharePermission> => {
    const override = await ctx.db
      .query("document_group_user_permissions")
      .withIndex("by_doc_group_user", (q: any) =>
        q.eq("document_id", documentId).eq("group_id", groupId).eq("user_id", userId),
      )
      .first();
    return override?.permission === "edit" ? "edit" : "read";
  };

  let best: SharePermission | null = null;
  let disableDownload = false;
  let disableCopy = false;

  const checkDocId = async (documentId: string, requireIncludeDesc: boolean) => {
    const shares = await ctx.db
      .query("document_group_shares")
      .withIndex("by_document", (q: any) => q.eq("document_id", documentId))
      .collect();

    for (const s of shares) {
      const gid = String(s.group_id);
      if (!groupIds.has(gid)) continue;
      if (requireIncludeDesc && !s.include_descendants) continue;

      disableDownload = disableDownload || Boolean((s as any).disable_download);
      disableCopy = disableCopy || Boolean((s as any).disable_copy);

      const perm = await permissionFromGroup(documentId, gid);
      if (perm === "edit") {
        best = "edit";
      } else {
        best = best ?? "read";
      }
    }
  };

  // 当前页面是否被群组公开
  await checkDocId(doc.id, false);

  // 沿父链查找“包含子页面”的群组公开
  let parentId: string | null = doc.parent_id ?? null;
  for (let depth = 0; depth < 60 && parentId; depth += 1) {
    await checkDocId(parentId, true);

    parentId = await getCanonicalParentDocumentId(ctx, parentId);
  }

  if (!best) return null;
  return { permission: best, disableDownload, disableCopy };
}

async function resolveGroupSharePermission(ctx: any, doc: any, userId: string): Promise<SharePermission | null> {
  const policy = await resolveGroupSharePolicy(ctx, doc, userId);
  return policy?.permission ?? null;
}

async function purgeDocumentShareRelations(ctx: any, workspaceId: string, documentId: string) {
  // 说明：页面被“永久删除/清空回收站”后，需要级联清理共享关系；
  // 否则被分享者可能在“共享页面/公共页面”里看到幽灵条目（点开 404 / 无标题）。
  const directShares = await ctx.db
    .query("document_shares")
    .withIndex("by_document", (q: any) => q.eq("document_id", documentId))
    .collect();
  for (const row of directShares) {
    await ctx.db.delete(row._id);
  }

  const groupShares = await ctx.db
    .query("document_group_shares")
    .withIndex("by_document", (q: any) => q.eq("document_id", documentId))
    .collect();

  const touchedGroupIds = new Set<string>();
  for (const row of groupShares) {
    touchedGroupIds.add(String(row.group_id));
    await ctx.db.delete(row._id);
  }

  // 删除该文档在相关群组下的“用户可编辑覆盖权限”
  for (const groupId of touchedGroupIds) {
    const perms = await ctx.db
      .query("document_group_user_permissions")
      .withIndex("by_workspace_group", (q: any) => q.eq("workspace_id", workspaceId).eq("group_id", groupId))
      .collect();
    for (const p of perms) {
      if (String(p.document_id) !== documentId) continue;
      await ctx.db.delete(p._id);
    }
  }
}

async function purgeDocumentRelatedData(ctx: any, workspaceId: string, documentIds: string[]) {
  const uniqueDocIds = Array.from(new Set(documentIds.filter(Boolean)));
  if (uniqueDocIds.length === 0) return;

  // 1) 思维导图
  for (const docId of uniqueDocIds) {
    const rows = await ctx.db
      .query("mindmaps")
      .withIndex("by_doc_mindmap", (q: any) => q.eq("document_id", docId))
      .collect();
    for (const r of rows) {
      await ctx.db.delete(r._id);
    }
  }

  // 2) 附件（含底层 storage 文件）
  const assets: any[] = [];
  for (const docId of uniqueDocIds) {
    const rows = await ctx.db
      .query("media_assets")
      .withIndex("by_document", (q: any) => q.eq("document_id", docId))
      .collect();
    assets.push(...rows);
  }

  if (assets.length) {
    const assetIdsToDelete = new Set<string>(assets.map((a) => String(a.id)));
    const byStorage = new Map<string, string[]>();
    for (const a of assets) {
      const sid = (a.storage_id as any) ?? null;
      if (!sid) continue;
      const list = byStorage.get(String(sid)) ?? [];
      list.push(String(a.id));
      byStorage.set(String(sid), list);
    }

    for (const [sid] of byStorage.entries()) {
      const refs = await ctx.db
        .query("media_assets")
        .withIndex("by_storage_id", (q: any) => q.eq("storage_id", sid as any))
        .collect();
      // 说明：只要仍有“未清理”的引用（包括仍在垃圾桶但可恢复的记录），就不要删底层文件。
      const otherAlive = refs.some((r: any) => !assetIdsToDelete.has(String(r.id)) && !r.purged_at);
      if (!otherAlive) {
        try {
          await ctx.storage.delete(sid as any);
        } catch {
          // ignore
        }
      }
    }

    for (const a of assets) {
      await ctx.db.delete(a._id);
    }
  }

  // 3) 在线表格（含行数据）
  for (const docId of uniqueDocIds) {
    const tables = await ctx.db
      .query("document_tables")
      .withIndex("by_document", (q: any) => q.eq("document_id", docId))
      .collect();
    for (const table of tables) {
      const rows = await ctx.db
        .query("document_table_rows")
        .withIndex("by_table", (q: any) => q.eq("table_id", table.id))
        .collect();
      for (const r of rows) {
        await ctx.db.delete(r._id);
      }
      await ctx.db.delete(table._id);
    }
  }

  // 4) 评论
  for (const docId of uniqueDocIds) {
    const msgs = await ctx.db
      .query("comment_messages")
      .withIndex("by_document", (q: any) => q.eq("document_id", docId))
      .collect();
    for (const m of msgs) {
      await ctx.db.delete(m._id);
    }

    const threads = await ctx.db
      .query("comment_threads")
      .withIndex("by_document", (q: any) => q.eq("document_id", docId))
      .collect();
    for (const t of threads) {
      await ctx.db.delete(t._id);
    }
  }

  // 5) 页面引用/反链（source/target 任一命中即删除）
  const deletedRefIds = new Set<string>();
  for (const docId of uniqueDocIds) {
    const bySource = await ctx.db
      .query("page_references")
      .withIndex("by_workspace_source", (q: any) => q.eq("workspace_id", workspaceId).eq("source_page_id", docId))
      .collect();
    for (const r of bySource) {
      const id = String(r._id);
      if (deletedRefIds.has(id)) continue;
      deletedRefIds.add(id);
      await ctx.db.delete(r._id);
    }

    const byTarget = await ctx.db
      .query("page_references")
      .withIndex("by_workspace_target", (q: any) => q.eq("workspace_id", workspaceId).eq("target_page_id", docId))
      .collect();
    for (const r of byTarget) {
      const id = String(r._id);
      if (deletedRefIds.has(id)) continue;
      deletedRefIds.add(id);
      await ctx.db.delete(r._id);
    }
  }

  // 6) 收藏（避免残留用户数据）
  for (const docId of uniqueDocIds) {
    const stars = await ctx.db
      .query("document_stars")
      .withIndex("by_workspace_document_user", (q: any) => q.eq("workspace_id", workspaceId).eq("document_id", docId))
      .collect();
    for (const s of stars) {
      await ctx.db.delete(s._id);
    }
  }

  // 7) 最近访问（避免“最近/历史”里出现幽灵页面）
  // 说明：user_recent_pages 当前缺少按 document_id 的索引，这里先全表扫描再过滤。
  // 若未来数据量变大，再考虑加 index("by_document", ["document_id"]) 或按 workspace/user 拆分索引。
  const recents = await ctx.db.query("user_recent_pages").collect();
  if (recents.length) {
    const docIdSet = new Set(uniqueDocIds);
    for (const r of recents) {
      if (!docIdSet.has(String((r as any).document_id ?? ""))) continue;
      await ctx.db.delete(r._id);
    }
  }

  // 兜底：如果未来新增了其它 “document_id 外键表”，这里可以继续补充；
  // 当前先把最容易产生垃圾、且已出现历史堆积的表（media_assets/mindmaps/表格/评论/引用/收藏）清理掉。
  // 同时，避免误删“非 document_id 维度”的用户行为表（如 jobs 等），后续如确认需要可再补齐。
}

export const getMeta = query({
  args: { id: v.string() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) return null;
    if (doc.deleted_at != null) return null;
    try {
      await requireWorkspaceMember(ctx, doc.workspace_id, userId);
    } catch {
      return null;
    }

    let canEdit = false;
    let disableDownload = false;
    let disableCopy = false;
    if (doc.user_id === userId) {
      canEdit = true;
    } else if (doc.access_scope === "public") {
      canEdit = false;
    } else {
      const policy = (await resolveSharePolicy(ctx, doc, userId)) ?? (await resolveGroupSharePolicy(ctx, doc, userId));
      if (!policy) return null;
      canEdit = policy.permission === "edit";
      disableDownload = policy.disableDownload;
      disableCopy = policy.disableCopy;
    }
    return {
      id: doc.id,
      user_id: doc.user_id,
      workspace_id: doc.workspace_id,
      access_scope: doc.access_scope,
      can_edit: canEdit,
      disable_download: disableDownload,
      disable_copy: disableCopy,
      title: doc.title ?? null,
      parent_id: doc.parent_id ?? null,
      created_at: doc.created_at,
      updated_at: doc.updated_at ?? null,
      wide_layout: doc.wide_layout ?? null,
      use_small_text: doc.use_small_text ?? null,
      show_heading_numbers: doc.show_heading_numbers ?? null,
      show_toc: doc.show_toc ?? null,
      show_structure: doc.show_structure ?? null,
      protect_editing: doc.protect_editing ?? null,
      show_word_count: doc.show_word_count ?? null,
      collapse_backlinks: (doc as any).collapse_backlinks ?? null,
      page_font: (doc as any).page_font ?? null,
      layout_density: (doc as any).layout_density ?? null,
      hide_child_pages: (doc as any).hide_child_pages ?? null,
      show_block_ref_count: (doc as any).show_block_ref_count ?? null,
      embed_default_block_id: (doc as any).embed_default_block_id ?? null,
      word_count: doc.word_count ?? null,
      character_count: doc.character_count ?? null,
      block_count: doc.block_count ?? null,
      todo_total_count: (doc as any).todo_total_count ?? null,
      todo_done_count: (doc as any).todo_done_count ?? null,
    };
  },
});

export const getPermissionForUser = query({
  args: { userId: v.string(), id: v.string() },
  handler: async (ctx, args) => {
    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) return null;
    if (doc.deleted_at != null) return null;

    // workspace 权限
    await requireWorkspaceMember(ctx, doc.workspace_id, args.userId);

    if (doc.user_id === args.userId) {
      return { permission: "edit" as const, disableDownload: false, disableCopy: false };
    }
    if (doc.access_scope === "public") {
      return { permission: "read" as const, disableDownload: false, disableCopy: false };
    }

    const policy = (await resolveSharePolicy(ctx, doc, args.userId)) ?? (await resolveGroupSharePolicy(ctx, doc, args.userId));
    if (!policy) return null;

    return {
      permission: policy.permission === "edit" ? ("edit" as const) : ("read" as const),
      disableDownload: policy.disableDownload,
      disableCopy: policy.disableCopy,
    };
  },
});

export const getMetaForIngest = internalQuery({
  args: { userId: v.string(), id: v.string() },
  handler: async (ctx, args) => {
    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) return null;
    if (doc.user_id !== args.userId) return null;
    return {
      id: doc.id,
      user_id: doc.user_id,
      workspace_id: doc.workspace_id,
      access_scope: doc.access_scope,
      title: doc.title ?? null,
      parent_id: doc.parent_id ?? null,
      created_at: doc.created_at,
      updated_at: doc.updated_at ?? null,
      wide_layout: doc.wide_layout ?? null,
      use_small_text: doc.use_small_text ?? null,
      show_heading_numbers: doc.show_heading_numbers ?? null,
      show_toc: doc.show_toc ?? null,
      show_structure: doc.show_structure ?? null,
      protect_editing: doc.protect_editing ?? null,
      show_word_count: doc.show_word_count ?? null,
      word_count: doc.word_count ?? null,
      character_count: doc.character_count ?? null,
      block_count: doc.block_count ?? null,
    };
  },
});

export const getContent = query({
  args: { id: v.string() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) return null;
    if (doc.deleted_at != null) return null;
    try {
      await requireWorkspaceMember(ctx, doc.workspace_id, userId);
    } catch {
      return null;
    }

    if (doc.user_id === userId) {
      return {
        content: doc.content ?? null,
        revision: doc.content_revision ?? 0,
        conflict_detection_key:
          doc.content_conflict_key ??
          `${args.id}:${doc.content_revision ?? 0}`,
      };
    }
    if (doc.access_scope === "public") {
      return {
        content: doc.content ?? null,
        revision: doc.content_revision ?? 0,
        conflict_detection_key:
          doc.content_conflict_key ??
          `${args.id}:${doc.content_revision ?? 0}`,
      };
    }
    const perm = (await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
    if (!perm) return null;
    return {
      content: doc.content ?? null,
      revision: doc.content_revision ?? 0,
      conflict_detection_key:
        doc.content_conflict_key ??
        `${args.id}:${doc.content_revision ?? 0}`,
    };
  },
});

export const getContentForIngest = internalQuery({
  args: { userId: v.string(), id: v.string() },
  handler: async (ctx, args) => {
    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) return null;
    if (doc.user_id !== args.userId) return null;
    return {
      content: doc.content ?? null,
      revision: doc.content_revision ?? 0,
      conflict_detection_key:
        doc.content_conflict_key ??
        `${args.id}:${doc.content_revision ?? 0}`,
    };
  },
});

export const listByWorkspace = query({
  args: { workspaceId: v.string() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    // workspace 权限
    try {
      await requireWorkspaceMember(ctx, args.workspaceId, userId);
    } catch {
      return [];
    }

    const docs = await ctx.db
      .query("documents")
      .withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
      .collect();

    // 说明：这里仅用于侧边栏数据（不返回垃圾桶 deleted_at != null）。
    const alive = docs.filter((d) => d.deleted_at == null);

    const shares = await ctx.db
      .query("document_shares")
      .withIndex("by_workspace_shared_with", (q: any) =>
        q.eq("workspace_id", args.workspaceId).eq("shared_with_user_id", userId),
      )
      .collect();

    const directShares = new Map<
      string,
      { permission: SharePermission; includeDescendants: boolean }
    >();
    for (const s of shares) {
      directShares.set(s.document_id, {
        permission: s.permission as SharePermission,
        includeDescendants: Boolean(s.include_descendants),
      });
    }

    const groupMemberships = await ctx.db
      .query("group_members")
      .withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
      .collect();
    const groupIds = new Set<string>(groupMemberships.map((m: any) => String(m.group_id)));

    const directGroupShares = new Map<string, { includeDescendants: boolean }>();
    if (groupIds.size > 0) {
      for (const gid of groupIds) {
        const rows = await ctx.db
          .query("document_group_shares")
          .withIndex("by_workspace_group", (q: any) => q.eq("workspace_id", args.workspaceId).eq("group_id", gid))
          .collect();
        for (const r of rows) {
          const existing = directGroupShares.get(r.document_id);
          if (!existing) {
            directGroupShares.set(r.document_id, { includeDescendants: Boolean(r.include_descendants) });
          } else {
            existing.includeDescendants = existing.includeDescendants || Boolean(r.include_descendants);
          }
        }
      }
    }

    const parentById = new Map<string, string | null>();
    for (const d of alive) {
      parentById.set(d.id, d.parent_id ?? null);
    }

    const shareAccessCache = new Map<string, boolean>();
    const canAccessByShare = (docId: string): boolean => {
      const cached = shareAccessCache.get(docId);
      if (typeof cached === "boolean") return cached;
      if (directShares.has(docId)) {
        shareAccessCache.set(docId, true);
        return true;
      }
      let parentId = parentById.get(docId) ?? null;
      for (let depth = 0; depth < 60 && parentId; depth += 1) {
        const parentShare = directShares.get(parentId);
        if (parentShare && parentShare.includeDescendants) {
          shareAccessCache.set(docId, true);
          return true;
        }
        parentId = parentById.get(parentId) ?? null;
      }
      shareAccessCache.set(docId, false);
      return false;
    };

    const groupAccessCache = new Map<string, boolean>();
    const canAccessByGroupShare = (docId: string): boolean => {
      const cached = groupAccessCache.get(docId);
      if (typeof cached === "boolean") return cached;
      if (directGroupShares.has(docId)) {
        groupAccessCache.set(docId, true);
        return true;
      }
      let parentId = parentById.get(docId) ?? null;
      for (let depth = 0; depth < 60 && parentId; depth += 1) {
        const parentShare = directGroupShares.get(parentId);
        if (parentShare && parentShare.includeDescendants) {
          groupAccessCache.set(docId, true);
          return true;
        }
        parentId = parentById.get(parentId) ?? null;
      }
      groupAccessCache.set(docId, false);
      return false;
    };

    const stars = await ctx.db
      .query("document_stars")
      .withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
      .collect();
    const starredDocIds = new Set<string>(stars.map((s: any) => String(s.document_id)));

    return alive
      .filter((d) => {
        if (d.user_id === userId) return true;
        if (d.access_scope === "public") return true;
        return canAccessByShare(d.id) || canAccessByGroupShare(d.id);
      })
      .map((d) => ({
        access_scope:
          d.user_id === userId ? d.access_scope : d.access_scope === "public" ? "public" : "shared",
        id: d.id,
        workspace_id: d.workspace_id,
        title: d.title ?? "无标题",
        parent_id: d.parent_id ?? null,
        sort_order: d.sort_order ?? null,
        is_starred: Boolean(starredDocIds.has(d.id) || d.is_starred),
        is_template: d.is_template ?? false,
        created_at: d.created_at,
        updated_at: d.updated_at ?? null,
      }));
  },
});

export const listSearchDataByWorkspace = query({
  args: { workspaceId: v.string() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    // workspace 权限
    try {
      await requireWorkspaceMember(ctx, args.workspaceId, userId);
    } catch {
      return [];
    }

    const docs = await ctx.db
      .query("documents")
      .withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
      .collect();

    const alive = docs.filter((d) => d.deleted_at == null);

    const shares = await ctx.db
      .query("document_shares")
      .withIndex("by_workspace_shared_with", (q: any) =>
        q.eq("workspace_id", args.workspaceId).eq("shared_with_user_id", userId),
      )
      .collect();

    const directShares = new Map<
      string,
      { permission: SharePermission; includeDescendants: boolean }
    >();
    for (const s of shares) {
      directShares.set(s.document_id, {
        permission: s.permission as SharePermission,
        includeDescendants: Boolean(s.include_descendants),
      });
    }

    const groupMemberships = await ctx.db
      .query("group_members")
      .withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
      .collect();
    const groupIds = new Set<string>(groupMemberships.map((m: any) => String(m.group_id)));

    const directGroupShares = new Map<string, { includeDescendants: boolean }>();
    if (groupIds.size > 0) {
      for (const gid of groupIds) {
        const rows = await ctx.db
          .query("document_group_shares")
          .withIndex("by_workspace_group", (q: any) => q.eq("workspace_id", args.workspaceId).eq("group_id", gid))
          .collect();
        for (const r of rows) {
          const existing = directGroupShares.get(r.document_id);
          if (!existing) {
            directGroupShares.set(r.document_id, { includeDescendants: Boolean(r.include_descendants) });
          } else {
            existing.includeDescendants = existing.includeDescendants || Boolean(r.include_descendants);
          }
        }
      }
    }

    const parentById = new Map<string, string | null>();
    for (const d of alive) {
      parentById.set(d.id, d.parent_id ?? null);
    }

    const shareAccessCache = new Map<string, boolean>();
    const canAccessByShare = (docId: string): boolean => {
      const cached = shareAccessCache.get(docId);
      if (typeof cached === "boolean") return cached;
      if (directShares.has(docId)) {
        shareAccessCache.set(docId, true);
        return true;
      }
      let parentId = parentById.get(docId) ?? null;
      for (let depth = 0; depth < 60 && parentId; depth += 1) {
        const parentShare = directShares.get(parentId);
        if (parentShare && parentShare.includeDescendants) {
          shareAccessCache.set(docId, true);
          return true;
        }
        parentId = parentById.get(parentId) ?? null;
      }
      shareAccessCache.set(docId, false);
      return false;
    };

    const groupAccessCache = new Map<string, boolean>();
    const canAccessByGroupShare = (docId: string): boolean => {
      const cached = groupAccessCache.get(docId);
      if (typeof cached === "boolean") return cached;
      if (directGroupShares.has(docId)) {
        groupAccessCache.set(docId, true);
        return true;
      }
      let parentId = parentById.get(docId) ?? null;
      for (let depth = 0; depth < 60 && parentId; depth += 1) {
        const parentShare = directGroupShares.get(parentId);
        if (parentShare && parentShare.includeDescendants) {
          groupAccessCache.set(docId, true);
          return true;
        }
        parentId = parentById.get(parentId) ?? null;
      }
      groupAccessCache.set(docId, false);
      return false;
    };

    return alive
      .filter((d) => {
        if (d.user_id === userId) return true;
        if (d.access_scope === "public") return true;
        return canAccessByShare(d.id) || canAccessByGroupShare(d.id);
      })
      .map((d) => ({
        id: d.id,
        workspace_id: d.workspace_id,
        title: d.title ?? null,
        created_at: d.created_at ?? null,
        updated_at: d.updated_at ?? null,
        raw_text:
          typeof d.raw_text === "string" && d.raw_text.trim()
            ? d.raw_text
            : extractTextFromDocumentContent(d.content ?? null),
      }));
  },
});

export const listTrashedByWorkspace = query({
  args: { workspaceId: v.string() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const docs = await ctx.db
      .query("documents")
      .withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
      .collect();

    return docs
      .filter((d) => d.user_id === userId)
      .filter((d) => d.deleted_at != null)
      .sort((a, b) => (b.deleted_at ?? "").localeCompare(a.deleted_at ?? ""))
      .slice(0, 100)
      .map((d) => ({
        id: d.id,
        title: d.title ?? null,
        parent_id: d.parent_id ?? null,
        deleted_at: d.deleted_at!,
        access_scope: d.access_scope,
      }));
  },
});

export const create = mutation({
  args: {
    id: v.string(),
    workspaceId: v.string(),
    parentId: v.union(v.string(), v.null()),
    title: v.optional(v.union(v.string(), v.null())),
    accessScope,
    content: v.optional(v.any()),
  },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);
    const existing = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (existing && existing.deleted_at == null) {
      throw new Error("页面已存在");
    }

    const siblings = await ctx.db
      .query("documents")
      .withIndex("by_workspace_parent", (q) => q.eq("workspace_id", args.workspaceId).eq("parent_id", args.parentId))
      .collect();

    const sortOrder = siblings.filter((d) => d.deleted_at == null).length;
    const ts = nowIso();

    const title = (args.title ?? "无标题") || "无标题";
    const content = typeof args.content === "undefined" ? [] : args.content;
    const rawText = extractTextFromDocumentContent(content);

    await ctx.db.insert("documents", {
      id: args.id,
      user_id: userId,
      workspace_id: args.workspaceId,
      parent_id: args.parentId,
      title,
      content,
      content_revision: 0,
      content_conflict_key: `${args.id}:0`,
      raw_text: rawText,
      access_scope: args.accessScope,
      sort_order: sortOrder,
      is_starred: false,
      is_template: false,

      wide_layout: false,
      use_small_text: false,
      show_heading_numbers: true,
      show_toc: false,
      show_structure: false,
      protect_editing: false,
      show_word_count: true,
      collapse_backlinks: false,
      page_font: "default",
      layout_density: "normal",
      hide_child_pages: false,
      show_block_ref_count: false,
      embed_default_block_id: null,
      word_count: 0,
      character_count: 0,
      block_count: 0,
      todo_total_count: 0,
      todo_done_count: 0,
      created_at: ts,
      updated_at: ts,
      deleted_at: null,
      deleted_by: null,
    });

    return {
      id: args.id,
      title,
      parent_id: args.parentId,
      sort_order: sortOrder,
      is_starred: false,
      created_at: ts,
      updated_at: ts,
      workspace_id: args.workspaceId,
      access_scope: args.accessScope,
      is_template: false,
    };
  },
});

export const createWithParentReference = mutation({
  args: {
    id: v.string(),
    workspaceId: v.string(),
    parentId: v.union(v.string(), v.null()),
    title: v.optional(v.union(v.string(), v.null())),
    accessScope,
    content: v.optional(v.any()),
  },
  handler: async (ctx, args) => {
    if (!args.parentId) {
      return await createDocumentRecord(ctx, {
        id: args.id,
        workspaceId: args.workspaceId,
        parentId: args.parentId,
        title: args.title ?? null,
        accessScope: args.accessScope,
        content: args.content,
      });
    }

    const userId = await requireUserId(ctx);
    const parentDoc = await getCanonicalDocumentByBusinessId<any>(ctx, args.parentId);
    if (!parentDoc) {
      throw new Error("父页面不存在或无权限");
    }
    if (parentDoc.deleted_at != null) {
      throw new Error("父页面不存在或无权限");
    }
    await assertDocumentVisibleToUser(ctx, parentDoc, userId, "父页面不存在或无权限");

    const created = await createDocumentRecord(ctx, {
      ...args,
      workspaceId: parentDoc.workspace_id,
      parentId: args.parentId,
      accessScope: (parentDoc.access_scope ?? "private") as "private" | "shared" | "public",
      content: args.content ?? [],
    });

    const updatedContent = appendPageReferenceBlock(parentDoc.content ?? null, created.id, created.title ?? "无标题");
    await updateDocumentContentRecord(ctx, {
      id: args.parentId,
      content: updatedContent,
    });

    return created;
  },
});

export const updateContent = mutation({
  args: {
    id: v.string(),
    content: v.any(),
    expectedRevision: v.optional(v.union(v.number(), v.null())),
    conflictDetectionKey: v.optional(v.union(v.string(), v.null())),
  },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) throw new Error("页面不存在");
    if (doc.deleted_at != null) throw new Error("页面不存在");
    await requireWorkspaceMember(ctx, doc.workspace_id, userId);

    if (doc.user_id !== userId) {
      if (doc.access_scope === "public") throw new Error("无权限");
      const perm = (await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
      if (perm !== "edit") throw new Error("无权限");
    }
    const currentRevision = doc.content_revision ?? 0;
    const currentConflictDetectionKey =
      doc.content_conflict_key ??
      `${args.id}:${currentRevision}`;

    if (
      typeof args.expectedRevision === "number" &&
      Number.isInteger(args.expectedRevision) &&
      args.expectedRevision >= 0 &&
      args.expectedRevision !== currentRevision
    ) {
      throw new Error("正文内容已变更，请刷新后重试");
    }

    if (
      typeof args.conflictDetectionKey === "string" &&
      args.conflictDetectionKey.trim() &&
      args.conflictDetectionKey.trim() !== currentConflictDetectionKey
    ) {
      throw new Error("正文冲突检测失败，请刷新后重试");
    }
    const ts = nowIso();
    const rawText = extractTextFromDocumentContent(args.content);
    const nextRevision = currentRevision + 1;
    const nextConflictDetectionKey = `${args.id}:${nextRevision}`;
    await ctx.db.patch(doc._id, {
      content: args.content,
      content_revision: nextRevision,
      content_conflict_key: nextConflictDetectionKey,
      raw_text: rawText,
      updated_at: ts,
    });

    // 说明：在 Convex 模式下，把"自动入库/LightRAG 触发"迁到 Convex jobs/actions。
    // 采用 debounce，避免频繁保存时触发过多任务。
    await enqueueIngestDocumentJob(ctx, { userId: doc.user_id, documentId: args.id, debounceMs: 1500 });
    return {
      ok: true,
      updated_at: ts,
      revision: nextRevision,
      conflict_detection_key: nextConflictDetectionKey,
    };
  },
});

export const updateTitle = mutation({
  args: { id: v.string(), title: v.union(v.string(), v.null()) },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) throw new Error("页面不存在");
    if (doc.deleted_at != null) throw new Error("页面不存在");
    await requireWorkspaceMember(ctx, doc.workspace_id, userId);

    if (doc.user_id !== userId) {
      if (doc.access_scope === "public") throw new Error("无权限");
      const perm = (await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
      if (perm !== "edit") throw new Error("无权限");
    }
    const ts = nowIso();
    await ctx.db.patch(doc._id, { title: args.title, updated_at: ts });
    return { ok: true, updated_at: ts };
  },
});

export const setTemplate = mutation({
  args: { id: v.string(), isTemplate: v.boolean() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) throw new Error("页面不存在");
    if (doc.deleted_at != null) throw new Error("页面不存在");
    await requireWorkspaceMember(ctx, doc.workspace_id, userId);

    if (doc.user_id !== userId) {
      if (doc.access_scope === "public") throw new Error("无权限");
      const perm =
        (await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
      if (perm !== "edit") throw new Error("无权限");
    }

    const ts = nowIso();
    await ctx.db.patch(doc._id, { is_template: args.isTemplate, updated_at: ts });
    return { ok: true, updated_at: ts };
  },
});

export const move = mutation({
  args: {
    id: v.string(),
    parentId: v.union(v.string(), v.null()),
    sortOrder: v.number(),
  },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) throw new Error("页面不存在");
    if (doc.user_id !== userId) throw new Error("无权限");

    // 说明：仅更新当前节点的 sort_order 会导致兄弟节点出现重复 sort_order，
    // 前端会按 sort_order + created_at 排序，结果常常表现为“拖拽无效/拖完又回弹”。
    // 这里统一对目标父节点（以及跨父移动时的原父节点）做重新编号，保证 sort_order 唯一且连续。
    const ts = nowIso();

    const compareDocOrder = (a: any, b: any) => {
      const orderA = typeof a.sort_order === "number" ? a.sort_order : Number.MAX_SAFE_INTEGER;
      const orderB = typeof b.sort_order === "number" ? b.sort_order : Number.MAX_SAFE_INTEGER;
      if (orderA !== orderB) return orderA - orderB;
      // created_at 为 ISO 字符串，按字典序比较即可。
      return String(a.created_at ?? "").localeCompare(String(b.created_at ?? ""));
    };

    const clampIndex = (raw: number, max: number) => {
      const n = Number.isFinite(raw) ? Math.floor(raw) : 0;
      if (n < 0) return 0;
      if (n > max) return max;
      return n;
    };

    const fetchSiblings = async (parentId: string | null) => {
      const siblings = await ctx.db
        .query("documents")
        .withIndex("by_workspace_parent", (q) => q.eq("workspace_id", doc.workspace_id).eq("parent_id", parentId))
        .collect();

      return siblings
        .filter((d) => d.user_id === userId)
        .filter((d) => d.deleted_at == null)
        .sort(compareDocOrder);
    };

    const applyOrder = async (ordered: any[], parentId: string | null, movedId?: any) => {
      for (let i = 0; i < ordered.length; i += 1) {
        const item = ordered[i];
        const nextSortOrder = i;
        const nextParentId = parentId;
        const patch: Record<string, unknown> = {};

        if ((item.parent_id ?? null) !== nextParentId) patch.parent_id = nextParentId;
        if ((item.sort_order ?? null) !== nextSortOrder) patch.sort_order = nextSortOrder;

        // 说明：只强制更新被移动节点的 updated_at，避免拖拽一次导致大量节点更新时间变化。
        if (movedId && item._id === movedId) patch.updated_at = ts;

        if (Object.keys(patch).length > 0) {
           
          await ctx.db.patch(item._id, patch);
        }
      }
    };

    const fromParentId = (doc.parent_id ?? null) as string | null;
    const toParentId = args.parentId;

    if (fromParentId === toParentId) {
      const siblings = await fetchSiblings(toParentId);
      const list = siblings.filter((d) => d.id !== doc.id);
      const position = clampIndex(args.sortOrder, list.length);
      list.splice(position, 0, doc);
      await applyOrder(list, toParentId, doc._id);
      return { ok: true, updated_at: ts };
    }

    // 先重排原父节点，确保移动后原列表连续。
    const oldSiblings = (await fetchSiblings(fromParentId)).filter((d) => d.id !== doc.id);
    await applyOrder(oldSiblings, fromParentId);

    // 再重排目标父节点，把节点插入到目标位置。
    const newSiblings = (await fetchSiblings(toParentId)).filter((d) => d.id !== doc.id);
    const position = clampIndex(args.sortOrder, newSiblings.length);
    newSiblings.splice(position, 0, doc);
    await applyOrder(newSiblings, toParentId, doc._id);

    return { ok: true, updated_at: ts };
  },
});

export const softDelete = mutation({
  args: { id: v.string() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) throw new Error("页面不存在");
    if (doc.user_id !== userId) throw new Error("无权限");
    const ts = nowIso();

    // 级联软删除：删除父节点时，必须同步删除子节点，否则子节点会因为父节点缺失而“跑到根目录”。
    const allInWorkspace = await ctx.db
      .query("documents")
      .withIndex("by_workspace", (q) => q.eq("workspace_id", doc.workspace_id))
      .collect();
    const owned = allInWorkspace.filter((d) => d.user_id === userId);
    const subtree = collectSubtree(owned, doc.id);

    let moved = 0;
    for (const item of subtree) {
      if (item.deleted_at != null) continue;
      await ctx.db.patch(item._id, { deleted_at: ts, deleted_by: userId, updated_at: ts });
      // 说明：为了避免被分享者仍看到已删除页面（点开 404），软删除时也同步移除共享关系。
      // 如需保留共享关系用于恢复后自动生效，可改为仅在 purge/emptyTrash 时清理。
      await purgeDocumentShareRelations(ctx, doc.workspace_id, item.id);
      moved += 1;
    }

    return { ok: true, moved, deleted_at: ts };
  },
});

export const restore = mutation({
  args: { id: v.string() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) throw new Error("页面不存在");
    if (doc.user_id !== userId) throw new Error("无权限");
    const ts = nowIso();
    const restoreDeletedAt = doc.deleted_at;
    await ctx.db.patch(doc._id, {
      deleted_at: null,
      deleted_by: null,
      parent_id: null,
      access_scope: "private",
      updated_at: ts,
    });

    // 级联恢复：仅恢复“随本次父节点删除而进入垃圾桶”的子节点，避免把之前单独删除的子页面一并恢复。
    if (restoreDeletedAt != null) {
      const allInWorkspace = await ctx.db
        .query("documents")
        .withIndex("by_workspace", (q) => q.eq("workspace_id", doc.workspace_id))
        .collect();
      const owned = allInWorkspace.filter((d) => d.user_id === userId);
      const subtree = collectSubtree(owned, doc.id);

      for (const item of subtree) {
        if (item.id === doc.id) continue;
        if (item.deleted_at !== restoreDeletedAt) continue;
        await ctx.db.patch(item._id, { deleted_at: null, deleted_by: null, updated_at: ts });
      }
    }
    return { ok: true };
  },
});

export const purge = mutation({
  args: { id: v.string() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) throw new Error("页面不存在");
    if (doc.user_id !== userId) throw new Error("无权限");

    // 级联彻底删除：避免父节点被删后，子节点变成孤儿数据。
    const allInWorkspace = await ctx.db
      .query("documents")
      .withIndex("by_workspace", (q) => q.eq("workspace_id", doc.workspace_id))
      .collect();
    const owned = allInWorkspace.filter((d) => d.user_id === userId);
    const subtree = collectSubtree(owned, doc.id);

    // 删除顺序对当前数据模型无强制要求，这里简单逐个删除即可。
    await purgeDocumentRelatedData(ctx, doc.workspace_id, subtree.map((d) => d.id));
    for (const item of subtree) {
      await purgeDocumentShareRelations(ctx, doc.workspace_id, item.id);
      await ctx.db.delete(item._id);
    }

    return { ok: true, deletedCount: subtree.length };
  },
});

export const emptyTrashByWorkspace = mutation({
  args: { workspaceId: v.string() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    // 说明：阶段 4/5 先用"membership 存在即可"的规则，避免引入复杂权限模型。
    const membership = await ctx.db
      .query("workspace_members")
      .withIndex("by_workspace_user", (q) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
      .first();

    if (!membership) {
      throw new Error("无权操作该工作空间");
    }

    const docs = await ctx.db
      .query("documents")
      .withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
      .collect();

    const toDelete = docs.filter((d) => d.user_id === userId && d.deleted_at != null);
    await purgeDocumentRelatedData(ctx, args.workspaceId, toDelete.map((d) => d.id));
    for (const d of toDelete) {
      await purgeDocumentShareRelations(ctx, args.workspaceId, d.id);
      await ctx.db.delete(d._id);
    }

    return { ok: true, deletedCount: toDelete.length };
  },
});

export const updateOptions = mutation({
  args: {
    id: v.string(),
    options: v.object({
      wideLayout: v.optional(v.boolean()),
      smallText: v.optional(v.boolean()),
      showHeadingNumbers: v.optional(v.boolean()),
      showToc: v.optional(v.boolean()),
      showStructure: v.optional(v.boolean()),
      protectEditing: v.optional(v.boolean()),
      showWordCount: v.optional(v.boolean()),
      collapseBacklinks: v.optional(v.boolean()),
      pageFont: v.optional(v.union(v.literal("default"), v.literal("song"), v.literal("kai"))),
      layoutDensity: v.optional(v.union(v.literal("compact"), v.literal("normal"), v.literal("spacious"))),
      hideChildPages: v.optional(v.boolean()),
      showBlockRefCount: v.optional(v.boolean()),
      embedDefaultBlockId: v.optional(v.union(v.string(), v.null())),
    }),
  },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) throw new Error("页面不存在");
    if (doc.deleted_at != null) throw new Error("页面不存在");
    await requireWorkspaceMember(ctx, doc.workspace_id, userId);

    if (doc.user_id !== userId) {
      if (doc.access_scope === "public") throw new Error("无权限");
      const perm = (await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
      if (perm !== "edit") throw new Error("无权限");
    }

    const patch: Record<string, unknown> = {};
    if (typeof args.options.wideLayout === "boolean") patch.wide_layout = args.options.wideLayout;
    if (typeof args.options.smallText === "boolean") patch.use_small_text = args.options.smallText;
    if (typeof args.options.showHeadingNumbers === "boolean")
      patch.show_heading_numbers = args.options.showHeadingNumbers;
    if (typeof args.options.showToc === "boolean") patch.show_toc = args.options.showToc;
    if (typeof args.options.showStructure === "boolean") patch.show_structure = args.options.showStructure;
    if (typeof args.options.protectEditing === "boolean") patch.protect_editing = args.options.protectEditing;
    if (typeof args.options.showWordCount === "boolean") patch.show_word_count = args.options.showWordCount;
    if (typeof args.options.collapseBacklinks === "boolean") patch.collapse_backlinks = args.options.collapseBacklinks;
    if (typeof (args.options as any).pageFont === "string") patch.page_font = (args.options as any).pageFont;
    if (typeof (args.options as any).layoutDensity === "string") patch.layout_density = (args.options as any).layoutDensity;
    if (typeof (args.options as any).hideChildPages === "boolean") patch.hide_child_pages = (args.options as any).hideChildPages;
    if (typeof (args.options as any).showBlockRefCount === "boolean")
      patch.show_block_ref_count = (args.options as any).showBlockRefCount;
    if (typeof (args.options as any).embedDefaultBlockId === "string" || (args.options as any).embedDefaultBlockId === null) {
      patch.embed_default_block_id = (args.options as any).embedDefaultBlockId;
    }

    if (Object.keys(patch).length === 0) {
      throw new Error("缺少可更新的选项");
    }

    const ts = nowIso();
    await ctx.db.patch(doc._id, { ...patch, updated_at: ts });
    return { ok: true, updated_at: ts };
  },
});

export const updateStats = mutation({
  args: {
    id: v.string(),
    wordCount: v.number(),
    characterCount: v.number(),
    blockCount: v.number(),
    todoTotal: v.optional(v.number()),
    todoDone: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
    if (!doc) throw new Error("页面不存在");
    if (doc.deleted_at != null) throw new Error("页面不存在");
    await requireWorkspaceMember(ctx, doc.workspace_id, userId);

    if (doc.user_id !== userId) {
      if (doc.access_scope === "public") throw new Error("无权限");
      const perm = (await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
      if (perm !== "edit") throw new Error("无权限");
    }
    const ts = nowIso();
    await ctx.db.patch(doc._id, {
      word_count: args.wordCount,
      character_count: args.characterCount,
      block_count: args.blockCount,
      todo_total_count: typeof args.todoTotal === "number" ? args.todoTotal : (doc as any).todo_total_count ?? 0,
      todo_done_count: typeof args.todoDone === "number" ? args.todoDone : (doc as any).todo_done_count ?? 0,
      updated_at: ts,
    });
    return { ok: true, updated_at: ts };
  },
});

export const duplicate = mutation({
  args: {
    sourceId: v.string(),
    newId: v.string(),
    title: v.optional(v.union(v.string(), v.null())),
  },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const source = await getCanonicalDocumentByBusinessId(ctx, args.sourceId);
    if (!source) throw new Error("页面不存在或无权限访问");
    if (source.user_id !== userId) throw new Error("页面不存在或无权限访问");

    const existingTarget = await getCanonicalDocumentByBusinessId(ctx, args.newId);
    if (existingTarget && existingTarget.deleted_at == null) {
      throw new Error("目标页面已存在");
    }

    const siblings = await ctx.db
      .query("documents")
      .withIndex("by_workspace_parent", (q) =>
        q.eq("workspace_id", source.workspace_id).eq("parent_id", source.parent_id),
      )
      .collect();

    const sortOrder = siblings.filter((d) => d.deleted_at == null).length;
    const ts = nowIso();
    const baseTitle = (source.title ?? "无标题").trim() ? (source.title ?? "无标题").trim() : "无标题";
    const title = (args.title ?? `${baseTitle} 副本`) || `${baseTitle} 副本`;
    const rawText = extractTextFromDocumentContent(source.content ?? []);

    await ctx.db.insert("documents", {
      id: args.newId,
      user_id: userId,
      workspace_id: source.workspace_id,
      parent_id: source.parent_id,
      title,
      content: source.content ?? [],
      content_revision: 0,
      content_conflict_key: `${args.newId}:0`,
      raw_text: rawText,
      access_scope: source.access_scope,
      sort_order: sortOrder,
      is_starred: false,
      is_template: false,

      wide_layout: source.wide_layout ?? false,
      use_small_text: source.use_small_text ?? false,
      show_heading_numbers: source.show_heading_numbers ?? true,
      show_toc: source.show_toc ?? false,
      show_structure: source.show_structure ?? false,
      protect_editing: source.protect_editing ?? false,
      show_word_count: source.show_word_count ?? true,
      collapse_backlinks: (source as any).collapse_backlinks ?? false,
      page_font: (source as any).page_font ?? "default",
      layout_density: (source as any).layout_density ?? "normal",
      hide_child_pages: (source as any).hide_child_pages ?? false,
      show_block_ref_count: (source as any).show_block_ref_count ?? false,
      embed_default_block_id: (source as any).embed_default_block_id ?? null,
      word_count: source.word_count ?? 0,
      character_count: source.character_count ?? 0,
      block_count: source.block_count ?? 0,
      todo_total_count: (source as any).todo_total_count ?? 0,
      todo_done_count: (source as any).todo_done_count ?? 0,

      created_at: ts,
      updated_at: ts,
      deleted_at: null,
      deleted_by: null,
    });

    return {
      id: args.newId,
      title,
      parent_id: source.parent_id ?? null,
      sort_order: sortOrder,
      workspace_id: source.workspace_id,
      access_scope: source.access_scope,
      created_at: ts,
      updated_at: ts,
    };
  },
});

export const duplicateWithMindmaps = mutation({
  args: {
    sourceId: v.string(),
    newId: v.string(),
    title: v.optional(v.union(v.string(), v.null())),
  },
  handler: async (ctx, args) => {
    const duplicated = await duplicateDocumentRecord(ctx, args);
    await copyMindmapsForDocument(ctx, args.sourceId, duplicated.id);
    return duplicated;
  },
});

export const copyTree = mutation({
  args: {
    items: v.array(
      v.object({
        documentId: v.string(),
        recursive: v.boolean(),
      }),
    ),
    targetParentId: v.union(v.string(), v.null()),
  },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);
    const normalizedItems = args.items.filter((item) => Boolean(item?.documentId));
    if (normalizedItems.length === 0) {
      throw new Error("items 为空");
    }

    let workspaceId: string | null = null;
    if (args.targetParentId) {
      const targetDoc = await getCanonicalDocumentByBusinessId<any>(ctx, args.targetParentId);
      if (!targetDoc) {
        throw new Error("目标页面不存在或无权限");
      }
      if (targetDoc.deleted_at != null) {
        throw new Error("目标页面不存在或无权限");
      }
      await assertDocumentVisibleToUser(ctx, targetDoc, userId, "目标页面不存在或无权限");
      workspaceId = targetDoc.workspace_id;
    }

    const sourceIds = Array.from(new Set(normalizedItems.map((it) => it.documentId)));
    const allDocs: CopyTreeDocument[] = [];
    for (const sourceId of sourceIds) {
      const doc = await getCanonicalDocumentByBusinessId<any>(ctx, sourceId) as CopyTreeDocument | null;
      if (!doc || doc.deleted_at != null || doc.user_id !== userId) {
        throw new Error("源页面不存在或无权限");
      }
      allDocs.push(doc);
      if (!workspaceId) {
        workspaceId = doc.workspace_id;
      } else if (workspaceId !== doc.workspace_id) {
        throw new Error("源页面不在同一工作空间");
      }
    }

    if (!workspaceId) {
      throw new Error("缺少目标工作空间");
    }

    const workspaceDocs = await ctx.db
      .query("documents")
      .withIndex("by_workspace", (q) => q.eq("workspace_id", workspaceId))
      .collect();
    const ownedDocs = workspaceDocs.filter((d) => d.user_id === userId && d.deleted_at == null);
    const childrenByParent = new Map<string | null, CopyTreeDocument[]>();
    for (const doc of ownedDocs as CopyTreeDocument[]) {
      const list = childrenByParent.get(doc.parent_id ?? null) ?? [];
      list.push(doc);
      childrenByParent.set(doc.parent_id ?? null, list);
    }

    const existingTitleSetByParent = new Map<string | null, Set<string>>();
    const seedTitleSet = (parent: string | null) => {
      if (existingTitleSetByParent.has(parent)) return;
      const titles = new Set<string>();
      (childrenByParent.get(parent) ?? []).forEach((d) => titles.add(normalizeTitle(d.title)));
      existingTitleSetByParent.set(parent, titles);
    };
    seedTitleSet(args.targetParentId);

    const newIdByOldId = new Map<string, string>();
    const copyQueue: Array<{ old: CopyTreeDocument; newParentId: string | null }> = [];

    const enqueueTree = (root: CopyTreeDocument, newParent: string | null, recursive: boolean) => {
      const visit = (node: CopyTreeDocument, parentNewId: string | null) => {
        const newId =
          typeof crypto.randomUUID === "function"
            ? crypto.randomUUID()
            : `${Date.now()}_${Math.random().toString(16).slice(2)}`;
        newIdByOldId.set(node.id, newId);
        copyQueue.push({ old: node, newParentId: parentNewId });
        if (!recursive) return;
        const children = getChildrenSorted(childrenByParent, node.id);
        children.forEach((child) => visit(child, newId));
      };
      visit(root, newParent);
    };

    for (const item of normalizedItems) {
      const doc = allDocs.find((d) => d.id === item.documentId) ?? null;
      if (doc) {
        enqueueTree(doc, args.targetParentId, Boolean(item.recursive));
      }
    }

    if (copyQueue.length === 0) {
      throw new Error("没有可复制的页面");
    }

    const insertedDocs: Array<{ oldId: string; newId: string; title: string }> = [];

    for (const item of copyQueue) {
      const newId = newIdByOldId.get(item.old.id)!;
      const parentId = item.newParentId;

      if (!existingTitleSetByParent.has(parentId)) {
        seedTitleSet(parentId);
      }
      const titleSet = existingTitleSetByParent.get(parentId) ?? new Set<string>();
      existingTitleSetByParent.set(parentId, titleSet);

      const newTitle = makeUniqueTitle(normalizeTitle(item.old.title), titleSet);

      await createDocumentRecord(ctx, {
        id: newId,
        workspaceId,
        parentId,
        title: newTitle,
        accessScope: (item.old.access_scope ?? "private") as "private" | "shared" | "public",
        content: item.old.content ?? [],
      });

      await copyMindmapsForDocument(ctx, item.old.id, newId);
      insertedDocs.push({ oldId: item.old.id, newId, title: newTitle });
    }

    return {
      items: insertedDocs,
    };
  },
});

export const listAllForCopy = query({
  args: { workspaceId: v.string() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    const docs = await ctx.db
      .query("documents")
      .withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
      .collect();

    return docs
      .filter((d) => d.user_id === userId)
      .filter((d) => d.deleted_at == null)
      .map((d) => ({
        id: d.id,
        title: d.title ?? null,
        parent_id: d.parent_id ?? null,
        workspace_id: d.workspace_id,
        access_scope: d.access_scope,
        sort_order: d.sort_order ?? null,
        created_at: d.created_at ?? null,
        content: d.content ?? null,
      }));
  },
});
