/**
 * TipTap 与 EditorBlockDocument 的适配层。
 * 约定：EditorBlockDocument 是正式语义边界，legacy blocks 仅用于兼容存储与旧链路。
 */
export type TiptapMark = {
  type: string;
  attrs?: Record<string, unknown>;
};

export type TiptapNode = {
  type: string;
  attrs?: Record<string, unknown>;
  content?: TiptapNode[];
  text?: string;
  marks?: TiptapMark[];
};

export type TiptapDoc = {
  type: "doc";
  content: TiptapNode[];
};

export type EditorTextMark = "bold" | "italic" | "underline" | "strike" | "code";

export type EditorInlineMark = EditorTextMark | TiptapMark;

export type EditorBlockType =
  | "paragraph"
  | "heading"
  | "bullet_list_item"
  | "numbered_list_item"
  | "todo"
  | "quote"
  | "code_block"
  | "mindmap";

export type EditorContentNode = {
  type: "text";
  text: string;
  marks?: EditorInlineMark[];
};

export type EditorBlock = {
  blockId: string;
  blockType: EditorBlockType;
  props?: {
    headingLevel?: number | null;
    checked?: boolean | null;
    language?: string | null;
    mindmapId?: string | null;
    rootNodeId?: string | null;
    projectionVersion?: number | null;
  };
  contentNodes?: EditorContentNode[];
  childBlockIds?: string[];
};

export type EditorBlockDocument = {
  documentId: string;
  rootBlockIds: string[];
  blocks: EditorBlock[];
};

type Json = unknown;

type LegacyBlockLike = {
  id?: string;
  blockId?: string;
  type?: string;
  blockType?: string;
  content?: unknown;
  contentNodes?: unknown;
  props?: Record<string, unknown>;
  children?: unknown;
};

const EMPTY_DOC: TiptapDoc = {
  type: "doc",
  content: [{ type: "paragraph", attrs: {}, content: [] }],
};

const MARK_NAME_MAP: Record<string, EditorTextMark> = {
  bold: "bold",
  italic: "italic",
  underline: "underline",
  strike: "strike",
  code: "code",
};

const BLOCK_ID_ATTR = "blockId";

function coerceTextMark(value: unknown): EditorTextMark | null {
  const normalized = String(value ?? "").trim().toLowerCase();
  return MARK_NAME_MAP[normalized] ?? null;
}

function uniqueInlineMarks(marks: EditorInlineMark[]): EditorInlineMark[] {
  const seen = new Set<string>();
  return marks.filter((mark) => {
    const key = typeof mark === "string" ? mark : `${mark.type}:${JSON.stringify(mark.attrs ?? {})}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function readMarks(marks: unknown): EditorInlineMark[] {
  if (!Array.isArray(marks)) return [];
  return uniqueInlineMarks(marks.flatMap((mark): EditorInlineMark[] => {
    if (typeof mark === "string") {
      const textMark = coerceTextMark(mark);
      return textMark ? [textMark] : [];
    }
    if (!mark || typeof mark !== "object") return [];
    const record = mark as { type?: unknown; attrs?: unknown };
    const textMark = coerceTextMark(record.type);
    if (textMark) return [textMark];
    const type = typeof record.type === "string" ? record.type.trim() : "";
    if (type !== "link") return [];
    const attrs = record.attrs && typeof record.attrs === "object" ? record.attrs as Record<string, unknown> : {};
    const href = typeof attrs.href === "string" ? attrs.href.trim() : "";
    return href ? [{ type: "link", attrs: { href } }] : [];
  }));
}

function readStyles(styles: unknown): EditorInlineMark[] {
  if (!styles || typeof styles !== "object") return [];
  const record = styles as Record<string, unknown>;
  const marks: EditorInlineMark[] = [];
  if (record.bold) marks.push("bold");
  if (record.italic) marks.push("italic");
  if (record.underline) marks.push("underline");
  if (record.strike || record.strikethrough) marks.push("strike");
  if (record.code || record.inlineCode) marks.push("code");
  const href = typeof record.link === "string" && record.link.trim()
    ? record.link.trim()
    : typeof record.href === "string" && record.href.trim()
      ? record.href.trim()
      : "";
  if (href) marks.push({ type: "link", attrs: { href } });
  return marks;
}

function readInlineMarks(marks: unknown, styles: unknown): EditorInlineMark[] {
  return uniqueInlineMarks([...readMarks(marks), ...readStyles(styles)]);
}

function toTiptapMark(mark: EditorInlineMark): TiptapMark | null {
  if (typeof mark === "string") return { type: mark };
  const type = typeof mark.type === "string" ? mark.type.trim() : "";
  if (!type) return null;
  return mark.attrs ? { type, attrs: mark.attrs } : { type };
}

function toText(value: unknown): string {
  if (typeof value === "string") return value;
  if (Array.isArray(value)) return value.map(toText).join("");
  if (value && typeof value === "object") {
    const obj = value as { text?: unknown; content?: unknown; value?: unknown; contentNodes?: unknown };
    return `${toText(obj.text)}${toText(obj.value)}${toText(obj.content)}${toText(obj.contentNodes)}`;
  }
  return "";
}

function firstNonEmptyText(...values: unknown[]): string {
  for (const value of values) {
    const text = toText(value).trim();
    if (text) return text;
  }
  return "";
}

function clampHeadingLevel(value: unknown): number {
  const level = Number(value);
  return Number.isInteger(level) && level >= 1 && level <= 6 ? level : 1;
}

function normalizeBlockId(value: unknown, fallback: string): string {
  const raw = typeof value === "string" ? value.trim() : "";
  return raw || fallback;
}

function optionalText(value: unknown): string | null {
  const raw = typeof value === "string" ? value.trim() : "";
  return raw || null;
}

function mindmapReferenceProps(
  attrsOrProps: Record<string, unknown> | undefined,
  fallbackMindmapId: string,
): NonNullable<EditorBlock["props"]> {
  const data = attrsOrProps?.data && typeof attrsOrProps.data === "object"
    ? attrsOrProps.data as Record<string, unknown>
    : {};
  const mindmapId =
    optionalText(attrsOrProps?.mindmapId) ??
    optionalText(attrsOrProps?.mindmap_id) ??
    optionalText(data.mindmapId) ??
    optionalText(data.mindmap_id) ??
    optionalText(data.id) ??
    fallbackMindmapId;
  const rootNodeId =
    optionalText(attrsOrProps?.rootNodeId) ??
    optionalText(attrsOrProps?.root_node_id) ??
    optionalText(data.rootNodeId) ??
    optionalText(data.root_node_id) ??
    "root";
  const projectionVersion = Number(attrsOrProps?.projectionVersion ?? attrsOrProps?.projection_version);
  return {
    mindmapId,
    rootNodeId,
    ...(Number.isFinite(projectionVersion) ? { projectionVersion } : {}),
  };
}

function normalizeEditorContentNodes(input: unknown): EditorContentNode[] {
  if (Array.isArray(input)) {
    const nodes = input.flatMap((item) => {
      if (!item || typeof item !== "object") return [];
      const record = item as { type?: unknown; text?: unknown; marks?: unknown; styles?: unknown; payload?: unknown };
      if (record.type === "text") {
        const text = toText(record.text);
        return text ? [{ type: "text" as const, text, marks: readInlineMarks(record.marks, record.styles) }] : [];
      }
      if (record.payload && typeof record.payload === "object") {
        const payload = record.payload as { type?: unknown; text?: unknown; marks?: unknown; styles?: unknown };
        if (payload.type === "text") {
          const text = toText(payload.text);
          return text ? [{ type: "text" as const, text, marks: readInlineMarks(payload.marks, payload.styles) }] : [];
        }
      }
      const text = toText(item);
      return text ? [{ type: "text" as const, text, marks: [] }] : [];
    });
    return nodes;
  }
  const text = toText(input);
  return text ? [{ type: "text", text, marks: [] }] : [];
}

function normalizeLegacyBlock(block: LegacyBlockLike, index: number): EditorBlock {
  const blockType = String(block.blockType ?? block.type ?? "paragraph").trim().toLowerCase();
  const blockId = normalizeBlockId(block.blockId ?? block.id, `block-${index + 1}`);
  const props = block.props ?? {};
  const contentNodes = normalizeEditorContentNodes(block.contentNodes ?? block.content ?? props.content ?? props.text ?? props.title);

  switch (blockType) {
    case "heading":
      return {
        blockId,
        blockType: "heading",
        props: { headingLevel: clampHeadingLevel(props.level ?? props.headingLevel) },
        contentNodes,
        childBlockIds: [],
      };
    case "bullet_list_item":
    case "bullet_list":
    case "bullet-list":
      return { blockId, blockType: "bullet_list_item", props: {}, contentNodes, childBlockIds: [] };
    case "numbered_list_item":
    case "ordered_list":
    case "ordered-list":
      return { blockId, blockType: "numbered_list_item", props: {}, contentNodes, childBlockIds: [] };
    case "todo":
    case "task":
      return {
        blockId,
        blockType: "todo",
        props: { checked: Boolean(props.checked) },
        contentNodes,
        childBlockIds: [],
      };
    case "quote":
    case "blockquote":
      return { blockId, blockType: "quote", props: {}, contentNodes, childBlockIds: [] };
    case "code":
    case "code_block":
    case "code-block":
      return {
        blockId,
        blockType: "code_block",
        props: { language: typeof props.language === "string" ? props.language : null },
        contentNodes,
        childBlockIds: [],
      };
    case "mindmap": {
      return {
        blockId,
        blockType: "mindmap",
        props: mindmapReferenceProps(props, blockId),
        contentNodes: [],
        childBlockIds: [],
      };
    }
    case "media": {
      const sourcePath = firstNonEmptyText(props.sourcePath, props.url, props.src);
      const name = firstNonEmptyText(props.name, props.fileName, props.title, sourcePath);
      return {
        blockId,
        blockType: "paragraph",
        props: {},
        contentNodes: name
          ? [
              {
                type: "text",
                text: name,
                marks: sourcePath ? [{ type: "link", attrs: { href: sourcePath } }] : [],
              },
            ]
          : contentNodes,
        childBlockIds: [],
      };
    }
    case "paragraph":
    case "text":
    default:
      return { blockId, blockType: "paragraph", props: {}, contentNodes, childBlockIds: [] };
  }
}

function normalizeInputToEditorDocument(value: Json): EditorBlockDocument {
  if (value && typeof value === "object" && !Array.isArray(value)) {
    const maybe = value as Partial<EditorBlockDocument>;
    if (typeof maybe.documentId === "string" && Array.isArray(maybe.blocks)) {
      const normalizedBlocks = maybe.blocks.map((block, index) => normalizeLegacyBlock(block as LegacyBlockLike, index));
      const rootBlockIds = Array.isArray(maybe.rootBlockIds) && maybe.rootBlockIds.length > 0
        ? maybe.rootBlockIds.map((id, index) => normalizeBlockId(id, normalizedBlocks[index]?.blockId ?? `block-${index + 1}`))
        : normalizedBlocks.map((block) => block.blockId);
      return {
        documentId: maybe.documentId,
        rootBlockIds,
        blocks: normalizedBlocks,
      };
    }
    if (Array.isArray((value as { blocks?: unknown }).blocks)) {
      const blocks = ((value as { blocks: unknown[] }).blocks ?? []).map((block, index) =>
        normalizeLegacyBlock(block as LegacyBlockLike, index),
      );
      return {
        documentId: typeof (value as { documentId?: unknown }).documentId === "string"
          ? String((value as { documentId?: unknown }).documentId)
          : "document",
        rootBlockIds: blocks.map((block) => block.blockId),
        blocks,
      };
    }
  }

  const blocks = Array.isArray(value)
    ? value.map((block, index) => normalizeLegacyBlock(block as LegacyBlockLike, index))
    : [];

  return {
    documentId: "document",
    rootBlockIds: blocks.map((block) => block.blockId),
    blocks,
  };
}

function textNodesToInline(contentNodes: EditorContentNode[] | undefined): TiptapNode[] {
  return (contentNodes ?? []).flatMap((node) => {
    const text = toText(node.text);
    if (!text) return [];
    const marks = (node.marks ?? [])
      .map(toTiptapMark)
      .filter((mark): mark is TiptapMark => Boolean(mark));
    return [{ type: "text", text, marks: marks.length > 0 ? marks : undefined }];
  });
}

function blockToTiptapNode(block: EditorBlock): TiptapNode {
  const commonAttrs: Record<string, unknown> = { [BLOCK_ID_ATTR]: block.blockId };
  switch (block.blockType) {
    case "heading":
      return {
        type: "heading",
        attrs: { ...commonAttrs, level: clampHeadingLevel(block.props?.headingLevel) },
        content: textNodesToInline(block.contentNodes),
      };
    case "bullet_list_item":
      return {
        type: "bulletList",
        attrs: commonAttrs,
        content: [{ type: "listItem", attrs: commonAttrs, content: [{ type: "paragraph", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],
      };
    case "numbered_list_item":
      return {
        type: "orderedList",
        attrs: commonAttrs,
        content: [{ type: "listItem", attrs: commonAttrs, content: [{ type: "paragraph", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],
      };
    case "todo":
      return {
        type: "taskList",
        attrs: commonAttrs,
        content: [{ type: "taskItem", attrs: { ...commonAttrs, checked: Boolean(block.props?.checked) }, content: [{ type: "paragraph", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }] }],
      };
    case "quote":
      return {
        type: "blockquote",
        attrs: commonAttrs,
        content: [{ type: "paragraph", attrs: commonAttrs, content: textNodesToInline(block.contentNodes) }],
      };
    case "code_block":
      return {
        type: "codeBlock",
        attrs: { ...commonAttrs, language: block.props?.language ?? null },
        content: textNodesToInline(block.contentNodes),
      };
    case "mindmap":
      return {
        type: "paragraph",
        attrs: {
          ...commonAttrs,
          mnoteBlockType: "mindmap",
          ...mindmapReferenceProps(block.props, block.blockId),
        },
      };
    case "paragraph":
    default:
      return {
        type: "paragraph",
        attrs: commonAttrs,
        content: textNodesToInline(block.contentNodes),
      };
  }
}

function extractInlineContent(node: TiptapNode | undefined): EditorContentNode[] {
  if (!node) return [];
  if (Array.isArray(node.content)) {
    return node.content.flatMap((child) => {
      if (child.type === "text") {
        const text = toText(child.text);
        if (!text) return [];
        return [{ type: "text" as const, text, marks: readMarks(child.marks) }];
      }
      if (child.type === "hardBreak") {
        return [{ type: "text" as const, text: "\n", marks: [] }];
      }
      return extractInlineContent(child);
    });
  }
  if (node.type === "text") {
    const text = toText(node.text);
    return text ? [{ type: "text", text, marks: readMarks(node.marks) }] : [];
  }
  return [];
}

function firstChild(node: TiptapNode | undefined): TiptapNode | undefined {
  return Array.isArray(node?.content) ? node?.content?.[0] : undefined;
}

function normalizeDocumentId(value: unknown, fallback = "document"): string {
  const raw = typeof value === "string" ? value.trim() : "";
  return raw || fallback;
}

function nodeBlockId(node: TiptapNode, index: number): string {
  return normalizeBlockId(node.attrs?.[BLOCK_ID_ATTR], `block-${index + 1}`);
}

function tiptapNodeToBlock(node: TiptapNode, index: number): EditorBlock | null {
  const blockId = nodeBlockId(node, index);
  if (node.attrs?.mnoteBlockType === "mindmap") {
    return {
      blockId,
      blockType: "mindmap",
      props: mindmapReferenceProps(node.attrs, blockId),
      contentNodes: [],
      childBlockIds: [],
    };
  }
  switch (node.type) {
    case "paragraph":
      return { blockId, blockType: "paragraph", props: {}, contentNodes: extractInlineContent(node), childBlockIds: [] };
    case "heading":
      return {
        blockId,
        blockType: "heading",
        props: { headingLevel: clampHeadingLevel(node.attrs?.level) },
        contentNodes: extractInlineContent(node),
        childBlockIds: [],
      };
    case "bulletList": {
      const paragraph = firstChild(firstChild(node));
      return {
        blockId,
        blockType: "bullet_list_item",
        props: {},
        contentNodes: extractInlineContent(paragraph),
        childBlockIds: [],
      };
    }
    case "orderedList": {
      const paragraph = firstChild(firstChild(node));
      return {
        blockId,
        blockType: "numbered_list_item",
        props: {},
        contentNodes: extractInlineContent(paragraph),
        childBlockIds: [],
      };
    }
    case "taskList": {
      const taskItem = firstChild(node);
      const paragraph = firstChild(taskItem);
      return {
        blockId,
        blockType: "todo",
        props: { checked: Boolean(taskItem?.attrs?.checked) },
        contentNodes: extractInlineContent(paragraph),
        childBlockIds: [],
      };
    }
    case "blockquote":
      return { blockId, blockType: "quote", props: {}, contentNodes: extractInlineContent(firstChild(node)), childBlockIds: [] };
    case "codeBlock":
      return {
        blockId,
        blockType: "code_block",
        props: { language: typeof node.attrs?.language === "string" ? String(node.attrs.language) : null },
        contentNodes: extractInlineContent(node),
        childBlockIds: [],
      };
    default:
      return null;
  }
}

export function editorBlockDocumentFromContent(content: Json): EditorBlockDocument {
  return normalizeInputToEditorDocument(content);
}

export function normalizeEditorBlockDocument(
  value: unknown,
  fallbackDocumentId?: string,
): EditorBlockDocument {
  const normalized = normalizeInputToEditorDocument(value as Json);
  const documentId = normalizeDocumentId(
    fallbackDocumentId ?? normalized.documentId,
    normalized.documentId,
  );
  const blockIdSet = new Set(normalized.blocks.map((block) => block.blockId));
  const rootBlockIds = Array.from(
    new Set(
      normalized.rootBlockIds
        .map((id, index) => normalizeBlockId(id, normalized.blocks[index]?.blockId ?? `block-${index + 1}`))
        .filter((id) => blockIdSet.has(id)),
    ),
  );
  return {
    ...normalized,
    documentId,
    rootBlockIds: rootBlockIds.length > 0 ? rootBlockIds : normalized.blocks.map((block) => block.blockId),
  };
}

export function tiptapDocFromEditorBlockDocument(document: EditorBlockDocument): TiptapDoc {
  const blockById = new Map(document.blocks.map((block) => [block.blockId, block]));
  const nodes = document.rootBlockIds
    .map((blockId) => blockById.get(blockId))
    .filter((block): block is EditorBlock => Boolean(block))
    .map(blockToTiptapNode);
  return {
    type: "doc",
    content: nodes.length > 0 ? nodes : EMPTY_DOC.content,
  };
}

export function editorBlockDocumentFromTiptapDoc(doc: unknown, documentId = "document"): EditorBlockDocument {
  if (!doc || typeof doc !== "object") {
    return { documentId, rootBlockIds: [], blocks: [] };
  }
  const root = doc as { type?: unknown; content?: unknown };
  if (root.type !== "doc" || !Array.isArray(root.content)) {
    return { documentId, rootBlockIds: [], blocks: [] };
  }
  const blocks = root.content.flatMap((node, index) => {
    if (!node || typeof node !== "object") return [];
    const block = tiptapNodeToBlock(node as TiptapNode, index);
    return block ? [block] : [];
  });
  return {
    documentId,
    rootBlockIds: blocks.map((block) => block.blockId),
    blocks,
  };
}

function orderedBlocksForLegacy(document: EditorBlockDocument): EditorBlock[] {
  const blockById = new Map(document.blocks.map((block) => [block.blockId, block]));
  const visited = new Set<string>();
  const ordered: EditorBlock[] = [];

  const visit = (blockId: string) => {
    if (visited.has(blockId)) return;
    visited.add(blockId);
    const block = blockById.get(blockId);
    if (!block) return;
    ordered.push(block);
    for (const childId of block.childBlockIds ?? []) {
      visit(childId);
    }
  };

  for (const rootId of document.rootBlockIds) {
    visit(rootId);
  }
  for (const block of document.blocks) {
    visit(block.blockId);
  }

  return ordered;
}

function legacyStylesFromMarks(marks: EditorInlineMark[] | undefined): Record<string, boolean | string> | undefined {
  const styles: Record<string, boolean | string> = {};
  for (const mark of marks ?? []) {
    if (typeof mark === "string") {
      styles[mark] = true;
      continue;
    }
    if (mark.type === "link") {
      const href = typeof mark.attrs?.href === "string" ? mark.attrs.href.trim() : "";
      if (href) styles.link = href;
    }
  }
  return Object.keys(styles).length ? styles : undefined;
}

function legacyContentFromNodes(contentNodes: EditorContentNode[] | undefined): Json {
  const nodes = contentNodes ?? [];
  if (!nodes.some((node) => (node.marks ?? []).length > 0)) {
    return nodes.map((node) => node.text).join("");
  }
  return nodes.map((node) => {
    const styles = legacyStylesFromMarks(node.marks);
    return {
      type: "text",
      text: node.text,
      ...(styles ? { styles } : {}),
    };
  });
}

export function legacyBlocksFromEditorBlockDocument(document: EditorBlockDocument): Json {
  return orderedBlocksForLegacy(document).map((block) => ({
    id: block.blockId,
    type: block.blockType,
    props:
      block.blockType === "heading"
        ? { level: block.props?.headingLevel ?? 1 }
        : block.blockType === "todo"
          ? { checked: Boolean(block.props?.checked) }
          : block.blockType === "code_block"
            ? { language: block.props?.language ?? null }
            : block.blockType === "mindmap"
              ? mindmapReferenceProps(block.props, block.blockId)
            : undefined,
    content: block.blockType === "mindmap" ? "" : legacyContentFromNodes(block.contentNodes),
  }));
}

export function tiptapDocFromBlocks(blocks: Json): TiptapDoc {
  return tiptapDocFromEditorBlockDocument(editorBlockDocumentFromContent(blocks));
}

export function blocksFromTiptapDoc(doc: unknown): Json {
  return legacyBlocksFromEditorBlockDocument(editorBlockDocumentFromTiptapDoc(doc));
}

export const blocksToTiptapDoc = tiptapDocFromBlocks;
export const tiptapDocToBlocks = blocksFromTiptapDoc;
export const tiptapDocumentToEditorBlockDocumentAdapter = editorBlockDocumentFromTiptapDoc;
export const editorBlockDocumentToLegacyBlocksAdapter = legacyBlocksFromEditorBlockDocument;
