type MindMapData = {
  data: Record<string, unknown>;
  children?: MindMapData[];
};

const readBlobAsDataUrl = (blob: Blob): Promise<string> =>
  new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result || ""));
    reader.onerror = (err) => reject(err);
    reader.readAsDataURL(blob);
  });

const getImageSizeFromBlob = async (blob: Blob): Promise<{ width: number; height: number } | null> => {
  try {
    if (typeof createImageBitmap === "function") {
      const bmp = await createImageBitmap(blob);
      const res = { width: bmp.width, height: bmp.height };
      try {
        bmp.close();
      } catch {
        // ignore
      }
      return res;
    }
  } catch {
    // ignore
  }

  return new Promise((resolve) => {
    const url = URL.createObjectURL(blob);
    const img = new Image();
    img.onload = () => {
      const res = { width: img.naturalWidth, height: img.naturalHeight };
      URL.revokeObjectURL(url);
      resolve(res);
    };
    img.onerror = () => {
      URL.revokeObjectURL(url);
      resolve(null);
    };
    img.src = url;
  });
};

const walkElements = (root: Element): Element[] => {
  const stack: Element[] = [root];
  const out: Element[] = [];
  while (stack.length) {
    const el = stack.pop()!;
    out.push(el);
    for (let i = el.children.length - 1; i >= 0; i--) {
      stack.push(el.children[i] as Element);
    }
  }
  return out;
};

const findFirstByLocalName = (root: Element, name: string): Element | null => {
  for (const el of walkElements(root)) {
    if (el.localName === name) return el;
  }
  return null;
};

const findFirstChildByLocalName = (root: Element, name: string): Element | null => {
  for (const el of Array.from(root.children)) {
    if ((el as Element).localName === name) return el as Element;
  }
  return null;
};

const findChildrenByLocalName = (root: Element, name: string): Element[] =>
  Array.from(root.children).filter((el) => (el as Element).localName === name) as Element[];

const getTopicPlainText = (topicEl: Element): string => {
  const textEl = findFirstChildByLocalName(topicEl, "Text");
  const raw = textEl?.getAttribute("PlainText") ?? "";
  return raw;
};

const getTopicHyperlink = (topicEl: Element): { url: string; title: string } | null => {
  const linkEl = findFirstChildByLocalName(topicEl, "Hyperlink");
  if (!linkEl) return null;
  const url = linkEl.getAttribute("Url") ?? "";
  const title = linkEl.getAttribute("Title") ?? "";
  if (!url) return null;
  return { url, title };
};

const guessMimeFromImageType = (imageType: string): string => {
  const t = (imageType || "").toLowerCase();
  if (t.includes("png")) return "image/png";
  if (t.includes("jpeg") || t.includes("jpg")) return "image/jpeg";
  if (t.includes("gif")) return "image/gif";
  if (t.includes("bmp")) return "image/bmp";
  if (t.includes("svg")) return "image/svg+xml";
  return "application/octet-stream";
};

const parseMmArchiveUriToZipPath = (uri: string): string | null => {
  // 常见格式：mmarch://bin/<uuid>.bin
  const raw = String(uri || "");
  const lower = raw.toLowerCase();
  const prefix = "mmarch://";
  if (!lower.startsWith(prefix)) return null;
  let rest = raw.slice(prefix.length);
  while (rest.startsWith("/")) rest = rest.slice(1);
  return rest || null;
};

const getTopicImageInfo = (topicEl: Element): { uri: string; mime: string } | null => {
  const oneImage = findFirstChildByLocalName(topicEl, "OneImage");
  if (!oneImage) return null;
  const imageEl = findFirstByLocalName(oneImage, "Image");
  if (!imageEl) return null;
  const imageDataEl = findFirstByLocalName(imageEl, "ImageData");
  const uriEl = imageEl ? findFirstByLocalName(imageEl, "Uri") : null;
  const uri = (uriEl?.textContent ?? "").trim();
  if (!uri) return null;
  const mime = guessMimeFromImageType(imageDataEl?.getAttribute("ImageType") ?? "");
  return { uri, mime };
};

const compactTree = (node: MindMapData, isRoot = false): MindMapData => {
  const children = (node.children ?? []).map((c) => compactTree(c, false));
  const data = node.data ?? {};
  const hasContent =
    Boolean(String(data.text ?? "").trim()) ||
    Boolean(String((data as Record<string, unknown>).hyperlink ?? "").trim()) ||
    Boolean(String((data as Record<string, unknown>).note ?? "").trim()) ||
    Boolean(String((data as Record<string, unknown>).image ?? "").trim()) ||
    Boolean(((data as Record<string, unknown>).tag as unknown[] | undefined)?.length) ||
    Boolean(String((data as Record<string, unknown>).attachmentUrl ?? "").trim());
  if (!isRoot && !hasContent && children.length === 1) {
    return children[0];
  }
  return { ...node, children };
};

export const parseMindManagerMmapFile = async (file: File): Promise<MindMapData> => {
  const JSZip = (await import("jszip")).default;
  const zip = await JSZip.loadAsync(file);

  const xmlFile = zip.file("Document.xml");
  if (!xmlFile) {
    throw new Error("Document.xml 不存在");
  }
  const xmlText = await xmlFile.async("string");
  const xmlDoc = new DOMParser().parseFromString(xmlText, "application/xml");
  if (xmlDoc.getElementsByTagName("parsererror").length > 0) {
    throw new Error("Document.xml 解析失败");
  }
  const docEl = xmlDoc.documentElement;
  if (!docEl) throw new Error("Document.xml 内容为空");

  const defaultsGroup = findFirstByLocalName(docEl, "RootTopicDefaultsGroup");
  const defaultTextEl = defaultsGroup ? findFirstByLocalName(defaultsGroup, "DefaultText") : null;
  const defaultRootText = defaultTextEl?.getAttribute("PlainText") ?? "中心主题";

  const oneTopicEl = findFirstByLocalName(docEl, "OneTopic");
  const rootTopicEl = oneTopicEl ? findFirstChildByLocalName(oneTopicEl, "Topic") : null;
  if (!rootTopicEl) {
    throw new Error("未找到根主题");
  }

  const binCache = new Map<string, { dataUrl: string; width: number; height: number }>();

  const resolveBinToImageDataUrl = async (
    uri: string,
    mime: string,
  ): Promise<{ dataUrl: string; width: number; height: number } | null> => {
    const zipPath = parseMmArchiveUriToZipPath(uri);
    if (!zipPath) return null;
    if (binCache.has(zipPath)) return binCache.get(zipPath)!;

    const entry = zip.file(zipPath) ?? zip.file(`/${zipPath}`);
    if (!entry) return null;
    const bytes = await entry.async("uint8array");
    const rawBuffer = bytes.buffer as ArrayBuffer;
    const sliced = rawBuffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
    const blob = new Blob([sliced], { type: mime });
    const dataUrl = await readBlobAsDataUrl(blob);
    const size = await getImageSizeFromBlob(blob);
    const width = size?.width ?? 0;
    const height = size?.height ?? 0;
    const res = { dataUrl, width, height };
    binCache.set(zipPath, res);
    return res;
  };

  const walkTopic = async (topicEl: Element, isRoot: boolean): Promise<MindMapData> => {
    const text = getTopicPlainText(topicEl);
    const hyperlink = getTopicHyperlink(topicEl);
    const imageInfo = getTopicImageInfo(topicEl);
    const nodeData: Record<string, unknown> = {
      text: text ?? "",
    };

    if (hyperlink?.url) {
      nodeData.hyperlink = hyperlink.url;
      if (hyperlink.title) nodeData.hyperlinkTitle = hyperlink.title;
      // MindManager 常见：文本为空但带链接标题，作为节点可见文本更符合导入预期
      if (!String(nodeData.text || "").trim() && hyperlink.title) {
        nodeData.text = hyperlink.title;
      }
    }

    if (imageInfo?.uri) {
      const resolved = await resolveBinToImageDataUrl(imageInfo.uri, imageInfo.mime);
      if (resolved?.dataUrl) {
        nodeData.image = resolved.dataUrl;
        nodeData.imageSize = {
          width: resolved.width || 0,
          height: resolved.height || 0,
          custom: true,
        };
        nodeData.imgPlacement = "top";
      }
    }

    if (isRoot && !String(nodeData.text || "").trim()) {
      nodeData.text = defaultRootText || "中心主题";
    }

    const subTopicsEl = findFirstChildByLocalName(topicEl, "SubTopics");
    const childTopicEls = subTopicsEl ? findChildrenByLocalName(subTopicsEl, "Topic") : [];
    const children = await Promise.all(childTopicEls.map((c) => walkTopic(c, false)));
    return { data: nodeData, children };
  };

  const tree = await walkTopic(rootTopicEl, true);
  return compactTree(tree, true);
};
