import { NextResponse } from "next/server";
import { randomUUID } from "crypto";
import path from "path";
import { JSDOM } from "jsdom";
import { BlockNoteEditor, BlockNoteSchema, markdownToBlocks } from "@blocknote/core";
import type { Block } from "@blocknote/core";
import type { Json } from "@/types/supabase";
import type { MediaAsset } from "@/types/media";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
import { api } from "@/lib/convex/api";
import { getDocumentsBaseDir } from "@/lib/server/local-paths";
import { promises as fs } from "fs";
import os from "os";
import { spawn } from "child_process";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

let domShimReady = false;
function ensureDomShim() {
  if (domShimReady) return;
  const dom = new JSDOM("<!doctype html><html><body></body></html>");
  const g = globalThis as any;

  // 说明：Node.js（以及 Next.js 运行时）可能已经提供了只读的 globalThis.navigator（getter-only）。
  // 这里仅在必要时注入 window/document 等基础对象，避免覆盖只读属性导致 500。
  if (!g.window) g.window = dom.window;
  if (!g.document) g.document = dom.window.document;
  if (!g.DOMParser) g.DOMParser = dom.window.DOMParser;
  if (!g.HTMLElement) g.HTMLElement = dom.window.HTMLElement;
  if (!g.Node) g.Node = dom.window.Node;
  domShimReady = true;
}

const documentsBaseDir = getDocumentsBaseDir();

async function ensureDocumentScaffold(id: string, title: string) {
  const folder = path.join(documentsBaseDir, id);
  const indexFile = path.join(folder, "index.md");
  await fs.mkdir(folder, { recursive: true });
  try {
    await fs.access(indexFile);
  } catch {
    const safeTitle = title && title.trim() ? title.trim() : "无标题";
    await fs.writeFile(indexFile, `# ${safeTitle}\n`, "utf8");
  }
}

type InputStore = {
  kind: "dir";
  baseDir: string;
  listFiles: () => Promise<string[]>;
  readText: (relPath: string) => Promise<string>;
  readBytes: (relPath: string) => Promise<Uint8Array>;
  exists: (relPath: string) => Promise<boolean>;
};

type ImportNode =
  | { kind: "doc"; key: string; title: string; mdPath: string; parentKey: string | null }
  | { kind: "folder"; key: string; title: string; dirPath: string; parentKey: string | null };

type TokenRecord =
  | { kind: "image"; token: string; url: string; alt: string }
  | { kind: "file"; token: string; url: string; label: string }
  | { kind: "page"; token: string; targetMdRel: string; label: string };

function normalizeZipPath(value: string): string {
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
}

function isRemoteUrl(url: string): boolean {
  // 说明：Wolai 导出里可能包含 mailto/tel/#锚点等“非文件路径”链接，这里统一视为远端（不当作本地文件处理）。
  return /^(https?:\/\/|data:|mailto:|tel:)/i.test(url) || url.trim().startsWith("#");
}

function stripAngleBrackets(url: string): string {
  const trimmed = url.trim();
  if (trimmed.startsWith("<") && trimmed.endsWith(">")) {
    return trimmed.slice(1, -1).trim();
  }
  return trimmed;
}

function safeBasename(filePath: string): string {
  const base = path.posix.basename(filePath);
  return base || "未命名资源";
}

function parseTopHeadingTitle(markdown: string): string | null {
  const lines = markdown.split(/\r?\n/);
  for (const line of lines) {
    const t = line.trim();
    if (!t) continue;
    const m = /^#\s+(.+)$/.exec(t);
    if (!m) return null;
    const title = m[1]?.trim() ?? "";
    return title || null;
  }
  return null;
}

function stripTopHeadingIfMatches(markdown: string, title: string): string {
  const lines = markdown.split(/\r?\n/);
  let idx = 0;
  while (idx < lines.length && !lines[idx].trim()) idx += 1;
  const first = lines[idx] ?? "";
  const m = /^#\s+(.+)$/.exec(first.trim());
  if (!m) return markdown;
  const head = (m[1] ?? "").trim();
  if (!head || head !== title.trim()) return markdown;
  const rest = [...lines.slice(0, idx), ...lines.slice(idx + 1)];
  while (rest.length > 0 && !rest[0].trim()) rest.shift();
  return rest.join("\n");
}

function resolveRelativePosix(baseDirRel: string, rel: string): string {
  const cleaned = normalizeZipPath(stripAngleBrackets(rel));
  if (!cleaned) return cleaned;
  if (cleaned.startsWith("/")) return cleaned.replace(/^\/+/, "");
  return path.posix.normalize(path.posix.join(baseDirRel, cleaned));
}

function stripMarkdownLinkTitle(raw: string): string {
  // 说明：处理 Wolai 导出的 title 属性：path "title"
  return raw.split(/\s+\"/)[0]?.trim() ?? raw.trim();
}

function stripLocalQueryAndHash(url: string): string {
  // 说明：本地文件路径不应包含 ?query/#hash；否则会导致资源无法匹配。
  const noHash = url.split("#")[0] ?? url;
  const noQuery = (noHash ?? url).split("?")[0] ?? noHash ?? url;
  return noQuery.trim();
}

function guessMimeType(fileName: string): string {
  const ext = path.extname(fileName).toLowerCase();
  switch (ext) {
    case ".png":
      return "image/png";
    case ".jpg":
    case ".jpeg":
      return "image/jpeg";
    case ".gif":
      return "image/gif";
    case ".webp":
      return "image/webp";
    case ".svg":
      return "image/svg+xml";
    case ".pdf":
      return "application/pdf";
    case ".doc":
      return "application/msword";
    case ".docx":
      return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
    case ".xls":
      return "application/vnd.ms-excel";
    case ".xlsx":
      return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    case ".ppt":
      return "application/vnd.ms-powerpoint";
    case ".pptx":
      return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
    case ".mp3":
      return "audio/mpeg";
    case ".wav":
      return "audio/wav";
    case ".m4a":
      return "audio/mp4";
    case ".mp4":
      return "video/mp4";
    case ".mov":
      return "video/quicktime";
    default:
      return "application/octet-stream";
  }
}

function resolveAssetTypeByMime(mime: string): "image" | "video" | "audio" | "file" {
  if (mime.startsWith("image/")) return "image";
  if (mime.startsWith("video/")) return "video";
  if (mime.startsWith("audio/")) return "audio";
  return "file";
}

function toFsPath(baseDir: string, relPath: string): string {
  const normalized = normalizeZipPath(relPath);
  const parts = normalized.split("/").filter(Boolean);
  return path.join(baseDir, ...parts);
}

async function listFilesRecursive(rootDir: string): Promise<string[]> {
  const out: string[] = [];
  const stack: Array<{ abs: string; rel: string }> = [{ abs: rootDir, rel: "" }];

  while (stack.length > 0) {
    const current = stack.pop()!;
    const entries = await fs.readdir(current.abs, { withFileTypes: true });
    for (const entry of entries) {
      const abs = path.join(current.abs, entry.name);
      const rel = current.rel ? `${current.rel}/${entry.name}` : entry.name;
      if (entry.isDirectory()) {
        stack.push({ abs, rel });
      } else if (entry.isFile()) {
        out.push(normalizeZipPath(rel));
      }
    }
  }

  return out;
}

function escapePwshSingleQuoted(value: string): string {
  return value.replace(/'/g, "''");
}

async function extractZipToTemp(zipPath: string): Promise<string> {
  const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "wolai-import-"));
  const literalZip = escapePwshSingleQuoted(zipPath);
  const literalOut = escapePwshSingleQuoted(tmp);
  const command = `Expand-Archive -LiteralPath '${literalZip}' -DestinationPath '${literalOut}' -Force`;

  await new Promise<void>((resolve, reject) => {
    const child = spawn("powershell", ["-NoProfile", "-Command", command], {
      stdio: ["ignore", "pipe", "pipe"],
    });
    let stderr = "";
    child.stderr.on("data", (chunk) => {
      stderr += chunk.toString();
    });
    child.on("error", (err) => reject(err));
    child.on("close", (code) => {
      if (code === 0) resolve();
      else reject(new Error(`解压失败（code=${code}）：${stderr || "unknown"}`));
    });
  });

  return tmp;
}

async function createDirStore(baseDir: string): Promise<InputStore> {
  const abs = path.resolve(baseDir);
  return {
    kind: "dir",
    baseDir: abs,
    listFiles: async () => await listFilesRecursive(abs),
    readText: async (relPath) => await fs.readFile(toFsPath(abs, relPath), "utf8"),
    readBytes: async (relPath) => new Uint8Array(await fs.readFile(toFsPath(abs, relPath))),
    exists: async (relPath) => {
      try {
        await fs.access(toFsPath(abs, relPath));
        return true;
      } catch {
        return false;
      }
    },
  };
}

const MAX_ASSET_BYTES = 100 * 1024 * 1024;

function detectRootMdPath(mdPaths: string[]): { rootMdPath: string | null; candidates: string[] } {
  const normalized = mdPaths.map(normalizeZipPath);
  const topDirs = Array.from(new Set(normalized.map((p) => p.split("/")[0]).filter(Boolean)));
  if (topDirs.length === 1) {
    const top = topDirs[0]!;
    const candidates = normalized
      .filter((p) => p.startsWith(`${top}/`))
      .filter((p) => p.split("/").length === 2)
      .sort((a, b) => a.localeCompare(b, "zh-Hans-CN"));
    if (candidates.length === 1) return { rootMdPath: candidates[0]!, candidates };
    if (candidates.length > 1) return { rootMdPath: null, candidates };
  }

  const sorted = normalized
    .slice()
    .sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b, "zh-Hans-CN"));
  if (sorted.length === 0) return { rootMdPath: null, candidates: [] };
  return { rootMdPath: sorted[0]!, candidates: sorted.slice(0, 8) };
}

function buildImportNodes(opts: {
  scopePrefix: string;
  rootMdPath: string;
  mdPaths: string[];
}): { nodes: ImportNode[]; mdRelToKey: Map<string, string> } {
  const scope = opts.scopePrefix.endsWith("/") ? opts.scopePrefix : `${opts.scopePrefix}/`;
  const withinScope = opts.mdPaths.map(normalizeZipPath).filter((p) => p.startsWith(scope));
  const root = normalizeZipPath(opts.rootMdPath);
  const mdSet = new Set(withinScope);

  const allDirs: Set<string> = new Set();
  for (const md of withinScope) {
    const dir = path.posix.dirname(md);
    const segments = dir.split("/").filter(Boolean);
    let accum = "";
    for (const seg of segments) {
      accum = accum ? `${accum}/${seg}` : seg;
      allDirs.add(`${accum}/`);
    }
  }

  const representing: Map<string, string> = new Map();
  for (const dir of allDirs) {
    const dirName = dir.split("/").filter(Boolean).pop() ?? "";
    if (!dirName) continue;
    const prefer = `${dir}${dirName}.md`;
    const index = `${dir}index.md`;
    if (mdSet.has(prefer)) representing.set(dir, prefer);
    else if (mdSet.has(index)) representing.set(dir, index);
  }
  representing.set(scope, root);

  // 说明：仅为“包含 markdown 的目录”创建合成文件夹页（避免 image/ 这类纯资源目录）。
  const folderNodes: Array<Extract<ImportNode, { kind: "folder" }>> = [];
  for (const dir of allDirs) {
    if (dir === scope) continue;
    if (representing.has(dir)) continue;
    folderNodes.push({
      kind: "folder",
      key: `__dir__${dir}`,
      title: (dir.split("/").filter(Boolean).pop() ?? "目录").trim() || "目录",
      dirPath: dir,
      parentKey: null,
    });
  }

  const docNodes: Array<Extract<ImportNode, { kind: "doc" }>> = withinScope.map((md) => ({
    kind: "doc",
    key: md,
    title: path.posix.basename(md, ".md") || "无标题",
    mdPath: md,
    parentKey: null,
  }));

  const nodesByKey = new Map<string, ImportNode>();
  for (const n of [...folderNodes, ...docNodes]) nodesByKey.set(n.key, n);

  const getContainerKeyForDir = (dir: string): string => {
    const normalizedDir = dir.endsWith("/") ? dir : `${dir}/`;
    const rep = representing.get(normalizedDir);
    if (rep) return rep;
    return `__dir__${normalizedDir}`;
  };

  for (const node of folderNodes) {
    const parentDir = path.posix.dirname(node.dirPath.endsWith("/") ? node.dirPath.slice(0, -1) : node.dirPath);
    const parentDirNorm = parentDir === "." ? "" : `${parentDir}/`;
    node.parentKey = parentDirNorm ? getContainerKeyForDir(parentDirNorm) : root;
  }

  for (const node of docNodes) {
    if (node.mdPath === root) {
      node.parentKey = null;
      continue;
    }
    const dir = `${path.posix.dirname(node.mdPath)}/`;
    const rep = representing.get(dir);
    if (rep && rep !== node.mdPath) {
      node.parentKey = rep;
      continue;
    }
    if (rep && rep === node.mdPath) {
      const parentDir = path.posix.dirname(dir.endsWith("/") ? dir.slice(0, -1) : dir);
      const parentDirNorm = parentDir === "." ? "" : `${parentDir}/`;
      node.parentKey = parentDirNorm ? getContainerKeyForDir(parentDirNorm) : root;
      continue;
    }
    node.parentKey = getContainerKeyForDir(dir);
  }

  for (const node of [...folderNodes, ...docNodes]) {
    if (node.key === root) continue;
    if (!node.parentKey) continue;
    if (!nodesByKey.has(node.parentKey)) node.parentKey = root;
  }

  // 说明：构建 md 相对路径 -> key 的映射，用于内部链接替换
  const mdRelToKey = new Map<string, string>();
  for (const md of withinScope) {
    const rel = md.startsWith(scope) ? md.slice(scope.length) : md;
    mdRelToKey.set(rel, md);
  }

  // 说明：输出需要按“父先子后”的顺序创建
  const all = [...folderNodes, ...docNodes];
  const depth = (key: string): number => {
    let d = 0;
    let cur = nodesByKey.get(key) ?? null;
    const seen = new Set<string>();
    while (cur && cur.parentKey) {
      if (seen.has(cur.parentKey)) break;
      seen.add(cur.parentKey);
      d += 1;
      cur = nodesByKey.get(cur.parentKey) ?? null;
    }
    return d;
  };

  all.sort((a, b) => depth(a.key) - depth(b.key) || a.title.localeCompare(b.title, "zh-Hans-CN"));
  return { nodes: all, mdRelToKey };
}

function extractStandaloneMarkdownLink(line: string): { label: string; href: string } | null {
  const trimmed = line.trim();
  if (!trimmed.startsWith("[") || !trimmed.includes("](") || !trimmed.endsWith(")")) return null;
  const m = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(trimmed);
  if (!m) return null;
  const label = (m[1] ?? "").trim();
  const raw = (m[2] ?? "").trim();
  const href = stripAngleBrackets(stripMarkdownLinkTitle(raw));
  return { label, href };
}

function replaceImagesAndLinks(opts: {
  markdown: string;
  mdDirRel: string;
  mdRelToDocId: Map<string, string>;
  uploadedUrlByRelAssetPath: Map<
    string,
    {
      url: string;
      assetId: string;
      fileName: string;
      mimeType: string;
      fileSize: number;
      assetType: "image" | "video" | "audio" | "file";
    }
  >;
}): { markdown: string; tokens: TokenRecord[]; warnings: string[] } {
  const warnings: string[] = [];
  const tokens: TokenRecord[] = [];

  const lines = opts.markdown.split(/\r?\n/);
  const out: string[] = [];

  const imageRe = /!\[([^\]]*)\]\(([^)]+)\)/g;
  const linkRe = /\[([^\]]+)\]\(([^)]+)\)/g;

  for (const rawLine of lines) {
    let line = rawLine;

    // 1) 先处理“整行的页面链接/附件链接”，便于转成块
    const standalone = extractStandaloneMarkdownLink(line);
    if (standalone) {
      const href0 = standalone.href;
      const href = isRemoteUrl(href0) ? href0 : stripLocalQueryAndHash(href0);
      if (!isRemoteUrl(href)) {
        const target = resolveRelativePosix(opts.mdDirRel, href);
        if (target.toLowerCase().endsWith(".md")) {
          const docId = opts.mdRelToDocId.get(target);
          if (docId) {
            const token = `[[[MNOTE_PAGE_REF:${randomUUID()}]]]`;
            tokens.push({ kind: "page", token, targetMdRel: target, label: standalone.label });
            out.push(token);
            continue;
          }
        }

        const asset = opts.uploadedUrlByRelAssetPath.get(target);
        if (asset) {
          const token = `[[[MNOTE_FILE:${randomUUID()}]]]`;
          tokens.push({ kind: "file", token, url: asset.url, label: standalone.label });
          out.push(token);
          continue;
        }
      }
    }

    // 2) 图片语法替换为 token（避免 BlockNote 的图片解析在 Node 环境报错）
    line = line.replace(imageRe, (_full, altRaw, urlRaw) => {
      const alt = String(altRaw ?? "").trim();
      const raw0 = stripAngleBrackets(stripMarkdownLinkTitle(String(urlRaw ?? "").trim()));
      const url = isRemoteUrl(raw0) ? raw0 : stripLocalQueryAndHash(raw0);
      const token = `[[[MNOTE_IMAGE:${randomUUID()}]]]`;
      tokens.push({ kind: "image", token, url, alt });
      // 说明：用空行包起来，尽量让 markdownToBlocks 生成“单独段落”，便于后续替换为 media block。
      return `\n\n${token}\n\n`;
    });

    // 3) 重写内联链接（页面链接 -> /documents/<id>，附件链接 -> Convex url）
    line = line.replace(linkRe, (full, labelRaw, hrefRaw) => {
      const label = String(labelRaw ?? "");
      const href0 = stripAngleBrackets(stripMarkdownLinkTitle(String(hrefRaw ?? "").trim()));
      const href = isRemoteUrl(href0) ? href0 : stripLocalQueryAndHash(href0);
      if (isRemoteUrl(href)) return full;

      const target = resolveRelativePosix(opts.mdDirRel, href);
      if (target.toLowerCase().endsWith(".md")) {
        const docId = opts.mdRelToDocId.get(target);
        if (docId) return `[${label}](/documents/${encodeURIComponent(docId)})`;
        return full;
      }

      const asset = opts.uploadedUrlByRelAssetPath.get(target);
      if (asset) return `[${label}](${asset.url})`;
      return full;
    });

    out.push(line);
  }

  return { markdown: out.join("\n"), tokens, warnings };
}

function isSingleTokenParagraph(block: any, token: string): boolean {
  if (!block || typeof block !== "object") return false;
  if (!Array.isArray(block.content)) return false;
  if (block.content.length !== 1) return false;
  const node = block.content[0];
  if (!node || typeof node !== "object") return false;
  if (node.type !== "text") return false;
  return String(node.text ?? "").trim() === token;
}

function replaceTokensInBlocks(opts: {
  blocks: any[];
  tokens: TokenRecord[];
  pageRefResolver: (rel: string) => { pageId: string; title: string } | null;
  mediaResolver: (url: string) => { props: Record<string, unknown> } | null;
}): { blocks: any[]; unresolved: TokenRecord[] } {
  const tokenMap = new Map<string, TokenRecord>(opts.tokens.map((t) => [t.token, t]));
  const unresolved = new Set(opts.tokens.map((t) => t.token));

  const walk = (items: any[]): any[] => {
    const out: any[] = [];
    for (const block of items) {
      const maybe =
        block && Array.isArray(block.content) && block.content.length === 1 && block.content[0]?.type === "text"
          ? String(block.content[0].text ?? "").trim()
          : null;

      if (maybe && tokenMap.has(maybe) && isSingleTokenParagraph(block, maybe)) {
        const token = tokenMap.get(maybe)!;
        unresolved.delete(maybe);

        if (token.kind === "page") {
          const resolved = opts.pageRefResolver(token.targetMdRel);
          if (!resolved) {
            out.push(block);
            continue;
          }
          out.push({
            id: randomUUID(),
            type: "pageReference",
            props: { pageId: resolved.pageId, title: token.label || resolved.title || "无标题" },
            content: [],
            children: [],
          });
          continue;
        }

        if (token.kind === "image" || token.kind === "file") {
          const resolved = opts.mediaResolver(token.url);
          if (!resolved) {
            out.push(block);
            continue;
          }
          out.push({ id: randomUUID(), type: "media", props: resolved.props, content: [], children: [] });
          continue;
        }
      }

      const next = { ...block };
      if (Array.isArray(next.children) && next.children.length > 0) {
        next.children = walk(next.children);
      }
      out.push(next);
    }
    return out;
  };

  return { blocks: walk(opts.blocks), unresolved: opts.tokens.filter((t) => unresolved.has(t.token)) };
}

async function uploadToConvexMedia(opts: {
  client: any;
  userId: string;
  workspaceId: string;
  documentId: string;
  fileName: string;
  bytes: Uint8Array;
  mimeType: string;
}): Promise<MediaAsset> {
  const assetId = randomUUID();
  const assetType = resolveAssetTypeByMime(opts.mimeType);

  const uploadUrl = await opts.client.mutation(api.mediaAssets.generateUploadUrl, { userId: opts.userId });
  if (!uploadUrl || typeof uploadUrl !== "string") throw new Error("获取上传地址失败");

  const uploadRes = await fetch(uploadUrl, {
    method: "POST",
    headers: { "Content-Type": opts.mimeType || "application/octet-stream" },
    body: Buffer.from(opts.bytes),
  });
  if (!uploadRes.ok) {
    const text = await uploadRes.text().catch(() => "");
    throw new Error(`上传到 Convex 失败：${uploadRes.status} ${text}`);
  }
  const uploadJson = (await uploadRes.json().catch(() => null)) as { storageId?: string } | null;
  const storageId = uploadJson?.storageId ?? "";
  if (!storageId) throw new Error("上传到 Convex 失败：缺少 storageId");

  const created = await opts.client.mutation(api.mediaAssets.createWithStorage, {
    userId: opts.userId,
    storageId: storageId as any,
    asset: {
      id: assetId,
      workspace_id: opts.workspaceId,
      document_id: opts.documentId,
      asset_type: assetType,
      file_name: opts.fileName || null,
      file_size: opts.bytes.length,
      mime_type: opts.mimeType || null,
    },
  });

  return created as unknown as MediaAsset;
}

export async function POST(request: Request) {
  if (!isConvexEnabled()) {
    return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
  }

  let auth;
  try {
    auth = await requireAuthContext();
  } catch (err) {
    if (err instanceof HttpError) {
      return NextResponse.json({ error: "未登录" }, { status: err.status });
    }
    return NextResponse.json({ error: "未登录" }, { status: 401 });
  }

  try {
    ensureDomShim();

    const contentType = request.headers.get("content-type") ?? "";
    const isJson = contentType.includes("application/json");

    let parentId: string | null = null;
    let rootMdPathInput: string | null = null;
    let zipPath: string | null = null;
    let extractedDir: string | null = null;
    let store: InputStore | null = null;

    if (isJson) {
      const payload = (await request.json().catch(() => null)) as any;
      parentId = String(payload?.parentId ?? "").trim() || null;
      rootMdPathInput = String(payload?.rootMdPath ?? "").trim() || null;
      zipPath = String(payload?.zipPath ?? "").trim() || null;
      extractedDir = String(payload?.extractedDir ?? "").trim() || null;
    } else {
      const formData = await request.formData();
      parentId = String(formData.get("parentId") ?? "").trim() || null;
      rootMdPathInput = String(formData.get("rootMdPath") ?? "").trim() || null;
      const localZipPath = String(formData.get("zipPath") ?? "").trim();
      zipPath = localZipPath || null;

      const file = formData.get("file");
      if (file instanceof File) {
        // 说明：浏览器上传大 ZIP（比如 1GB）会导致内存/超时问题，这里直接给出提示。
        const size = Number((file as any).size ?? 0);
        if (size > 200 * 1024 * 1024) {
          return NextResponse.json(
            { error: "ZIP 太大，请使用“本地 ZIP 路径”方式导入（不要上传文件）" },
            { status: 413 },
          );
        }
        return NextResponse.json(
          { error: "当前仅支持“本地 ZIP 路径”导入（大文件避免上传）" },
          { status: 400 },
        );
      }
    }

    if (extractedDir) {
      store = await createDirStore(extractedDir);
    } else if (zipPath) {
      const stat = await fs.stat(zipPath);
      if (!stat.isFile()) {
        return NextResponse.json({ error: "zipPath 不是文件" }, { status: 400 });
      }
      const extractDir = await extractZipToTemp(zipPath);
      // 说明：使用解压输出目录作为 store 根目录，使相对路径保持 dQeAax/... 这种形态。
      store = await createDirStore(extractDir);
      extractedDir = extractDir;
    } else {
      return NextResponse.json({ error: "缺少 zipPath（本地 ZIP 路径）" }, { status: 400 });
    }

    const allFilePaths = await store.listFiles();
    const mdPaths = allFilePaths.filter((p) => p.toLowerCase().endsWith(".md"));
    if (mdPaths.length === 0) return NextResponse.json({ error: "未发现 .md 文件" }, { status: 400 });

    let rootMdPath = rootMdPathInput ? normalizeZipPath(rootMdPathInput) : null;
    if (rootMdPath && !(await store.exists(rootMdPath))) {
      return NextResponse.json({ error: "rootMdPath 不存在", rootMdPath }, { status: 400 });
    }
    if (!rootMdPath) {
      const detected = detectRootMdPath(mdPaths);
      if (!detected.rootMdPath) {
        return NextResponse.json(
          { error: "无法自动判断入口页面，请指定 rootMdPath", candidates: detected.candidates },
          { status: 409 },
        );
      }
      rootMdPath = detected.rootMdPath;
    }

    const scopePrefix = `${path.posix.dirname(rootMdPath)}/`;
    const { nodes, mdRelToKey } = buildImportNodes({ scopePrefix, rootMdPath, mdPaths });

    let client;
    try {
      client = await getConvexAuthedHttpClient();
    } catch (err) {
      const msg = err instanceof Error ? err.message : "未登录";
      const status = msg.includes("未登录") ? 401 : 500;
      return NextResponse.json({ error: msg }, { status });
    }

    // workspace / accessScope：有 parentId 则继承，否则走默认 workspace
    let workspaceId = "";
    let accessScope: "private" | "shared" | "public" = "private";
    if (parentId) {
      const parentMeta = await client.query(api.documents.getMeta, { id: parentId });
      if (!parentMeta) return NextResponse.json({ error: "父页面不存在或无权限" }, { status: 404 });
      workspaceId = String((parentMeta as any).workspace_id ?? "");
      accessScope = ((parentMeta as any).access_scope ?? "private") as typeof accessScope;
      if (!workspaceId) return NextResponse.json({ error: "无法读取父页面 workspaceId" }, { status: 400 });
    } else {
      const workspaceBootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, {
        fallbackName: auth.email ?? auth.name ?? "我的空间",
        workspaceIdIfCreate: randomUUID(),
      });
      workspaceId = String((workspaceBootstrap as any)?.activeWorkspaceId ?? "");
      if (!workspaceId) return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 });
    }

    // 读取每个 md 的标题（优先 # 顶部标题），并写回 node.title
    for (const node of nodes) {
      if (node.kind !== "doc") continue;
      const text = await store.readText(node.mdPath).catch(() => "");
      if (!text) continue;
      node.title = parseTopHeadingTitle(text) ?? node.title;
    }

    // 先创建全部节点（documents / 合成目录页）
    const keyToDocId = new Map<string, string>();
    for (const node of nodes) {
      const docId = randomUUID();
      const parentDocId =
        node.key === rootMdPath
          ? parentId
          : node.parentKey
            ? keyToDocId.get(node.parentKey) ?? parentId
            : parentId;

      const created = await client.mutation(api.documents.create, {
        id: docId,
        workspaceId,
        parentId: parentDocId ?? null,
        title: node.title,
        accessScope,
        content: [],
      });

      if (!created || (created as any).id !== docId) {
        return NextResponse.json({ error: "创建页面失败（Convex 返回异常）" }, { status: 500 });
      }
      keyToDocId.set(node.key, docId);
      await ensureDocumentScaffold(docId, node.title);
    }

    // 逐页导入内容（markdown -> blocks；图片/附件 -> media block + media_assets）
    const schema = BlockNoteSchema.create();
    const editor = BlockNoteEditor.create({ schema });
    const warnings: string[] = [];
    let importedCount = 0;

    for (const node of nodes) {
      if (node.kind !== "doc") continue;
      const docId = keyToDocId.get(node.key) ?? "";
      if (!docId) continue;
      let markdown = await store.readText(node.mdPath);
      markdown = stripTopHeadingIfMatches(markdown, node.title);

      const mdDirFull = `${path.posix.dirname(node.mdPath)}/`;
      const mdDirRel = mdDirFull.startsWith(scopePrefix) ? mdDirFull.slice(scopePrefix.length) : mdDirFull;

      // 预扫：把本页引用到的本地资源上传到 Convex，并建立 relPath -> url 映射
      const uploadedUrlByRelAssetPath = new Map<
        string,
        {
          url: string;
          assetId: string;
          fileName: string;
          mimeType: string;
          fileSize: number;
          assetType: "image" | "video" | "audio" | "file";
        }
      >();

      const localAssetPaths = new Set<string>();
      {
        const imgRe = /!\[[^\]]*\]\(([^)]+)\)/g;
        let m: RegExpExecArray | null;
        while ((m = imgRe.exec(markdown))) {
          const raw0 = stripAngleBrackets(stripMarkdownLinkTitle(String(m[1] ?? "").trim()));
          const raw = isRemoteUrl(raw0) ? raw0 : stripLocalQueryAndHash(raw0);
          if (!raw || isRemoteUrl(raw)) continue;
          const resolved = resolveRelativePosix(mdDirRel, raw);
          if (resolved) localAssetPaths.add(resolved);
        }
      }
      {
        const linkRe = /\[[^\]]+\]\(([^)]+)\)/g;
        let m: RegExpExecArray | null;
        while ((m = linkRe.exec(markdown))) {
          const raw0 = stripAngleBrackets(stripMarkdownLinkTitle(String(m[1] ?? "").trim()));
          const raw = isRemoteUrl(raw0) ? raw0 : stripLocalQueryAndHash(raw0);
          if (!raw || isRemoteUrl(raw)) continue;
          const resolved = resolveRelativePosix(mdDirRel, raw);
          if (!resolved) continue;
          if (resolved.toLowerCase().endsWith(".md")) continue;
          localAssetPaths.add(resolved);
        }
      }

      for (const relAssetPath of Array.from(localAssetPaths)) {
        const zipPath = `${scopePrefix}${relAssetPath}`.replace(/\/{2,}/g, "/");
        if (!(await store.exists(zipPath))) {
          warnings.push(`[${node.title}] 未找到资源文件：${relAssetPath}`);
          continue;
        }
        const absAssetPath = toFsPath(store.baseDir, zipPath);
        const stat = await fs.stat(absAssetPath).catch(() => null);
        if (stat && typeof stat.size === "number" && stat.size > MAX_ASSET_BYTES) {
          const mb = (stat.size / 1024 / 1024).toFixed(1);
          warnings.push(`[${node.title}] 资源过大已跳过（${mb}MB）：${relAssetPath}`);
          continue;
        }

        const bytes = await store.readBytes(zipPath);
        const fileName = safeBasename(relAssetPath);
        const mimeType = guessMimeType(fileName);
        const assetType = resolveAssetTypeByMime(mimeType);
        const created = await uploadToConvexMedia({
          client,
          userId: auth.userId,
          workspaceId,
          documentId: docId,
          fileName,
          bytes,
          mimeType,
        });
        uploadedUrlByRelAssetPath.set(relAssetPath, {
          url: String((created as any).file_url ?? ""),
          assetId: String((created as any).id ?? ""),
          fileName: String((created as any).file_name ?? fileName),
          mimeType: String((created as any).mime_type ?? mimeType),
          fileSize: Number((created as any).file_size ?? bytes.length),
          assetType,
        });
      }

      const mdRelToDocId = new Map<string, string>();
      for (const [rel, key] of mdRelToKey.entries()) {
        const id = keyToDocId.get(key);
        if (id) mdRelToDocId.set(rel, id);
      }

      const replaced = replaceImagesAndLinks({
        markdown,
        mdDirRel,
        mdRelToDocId,
        uploadedUrlByRelAssetPath,
      });
      markdown = replaced.markdown;
      warnings.push(...replaced.warnings);

      let blocks: Block<any, any, any>[] = [];
      try {
        blocks = markdownToBlocks(markdown, editor.pmSchema) as any;
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        warnings.push(`[${node.title}] Markdown 解析失败，已降级为纯文本：${msg}`);
        blocks = [
          {
            id: randomUUID(),
            type: "paragraph",
            props: { backgroundColor: "default", textColor: "default", textAlignment: "left" },
            content: [{ type: "text", text: markdown, styles: {} }],
            children: [],
          } as any,
        ];
      }

      const replacedBlocks = replaceTokensInBlocks({
        blocks: blocks as any[],
        tokens: replaced.tokens,
        pageRefResolver: (rel) => {
          const id = mdRelToDocId.get(rel) ?? "";
          if (!id) return null;
          return { pageId: id, title: path.posix.basename(rel, ".md") || "无标题" };
        },
        mediaResolver: (url) => {
          if (isRemoteUrl(url)) {
            const fileName = safeBasename(url);
            const mimeType = guessMimeType(fileName);
            const assetType = resolveAssetTypeByMime(mimeType);
            return {
              props: {
                fileUrl: url,
                thumbnailUrl: url,
                caption: "",
                captionAlign: "left",
                hasBorder: true,
                linkUrl: "",
                assetId: "",
                assetType,
                fileName,
                fileSize: 0,
                mimeType,
                width: 0,
                ocrStatus: "idle",
                documentId: docId,
              },
            };
          }

          const relAssetPath = resolveRelativePosix(mdDirRel, url);
          const asset = uploadedUrlByRelAssetPath.get(relAssetPath);
          if (!asset || !asset.url) return null;
          return {
            props: {
              fileUrl: asset.url,
              thumbnailUrl: asset.url,
              caption: "",
              captionAlign: "left",
              hasBorder: true,
              linkUrl: "",
              assetId: asset.assetId || "",
              assetType: asset.assetType,
              fileName: asset.fileName || safeBasename(relAssetPath),
              fileSize: asset.fileSize || 0,
              mimeType: asset.mimeType || guessMimeType(asset.fileName || safeBasename(relAssetPath)),
              width: 0,
              ocrStatus: "idle",
              documentId: docId,
            },
          };
        },
      });

      for (const token of replacedBlocks.unresolved) {
        warnings.push(`[${node.title}] 未能解析 token：${token.token}`);
      }

      // 说明：Convex 的 Json 不允许出现 undefined（尤其是表格类 block 里可能带有 columnWidths: [undefined]）。
      // 用 JSON 序列化做一次深度净化：对象属性的 undefined 会被移除，数组里的 undefined 会变成 null。
      const sanitizedBlocks = JSON.parse(JSON.stringify(replacedBlocks.blocks)) as Json;
      await client.mutation(api.documents.updateContent, { id: docId, content: sanitizedBlocks });
      importedCount += 1;
    }

    const rootId = keyToDocId.get(rootMdPath) ?? "";
    return NextResponse.json({
      ok: true,
      rootDocumentId: rootId,
      importedCount,
      warnings: warnings.slice(0, 200),
      documentsBaseDir,
      extractedDir,
    });
  } catch (error) {
    console.error("Wolai 导入失败", error);
    const message = error instanceof Error ? error.message : "导入失败";
    const stack = error instanceof Error ? error.stack : undefined;
    return NextResponse.json({ error: message, stack }, { status: 500 });
  }
}
