/**
 * 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 EditorBlockType =
  | "paragraph"
  | "heading"
  | "bullet_list_item"
  | "numbered_list_item"
  | "todo"
  | "quote"
  | "code_block";

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

export type EditorBlock = {
  blockId: string;
  blockType: EditorBlockType;
  props?: {
    headingLevel?: number | null;
    checked?: boolean | null;
    language?: string | 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 readMarks(marks: unknown): EditorTextMark[] {
  if (!Array.isArray(marks)) return [];
  const normalized = marks
    .map((mark) => {
      if (typeof mark === "string") return coerceTextMark(mark);
      if (mark && typeof mark === "object") return coerceTextMark((mark as { type?: unknown }).type);
      return null;
    })
    .filter((mark): mark is EditorTextMark => Boolean(mark));
  return Array.from(new Set(normalized));
}

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 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 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; payload?: unknown };
      if (record.type === "text") {
        const text = toText(record.text);
        return text ? [{ type: "text" as const, text, marks: readMarks(record.marks) }] : [];
      }
      if (record.payload && typeof record.payload === "object") {
        const payload = record.payload as { type?: unknown; text?: unknown; marks?: unknown };
        if (payload.type === "text") {
          const text = toText(payload.text);
          return text ? [{ type: "text" as const, text, marks: readMarks(payload.marks) }] : [];
        }
      }
      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 "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((mark) => ({ type: mark }))
      .filter((mark) => Boolean(mark.type));
    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 "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);
  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;
}

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 }
            : undefined,
    content: (block.contentNodes ?? []).map((node) => node.text).join(""),
  }));
}

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;
