export const flattenText = (value) => {
  if (typeof value === 'string') return value;
  if (Array.isArray(value)) return value.map(flattenText).join('');
  if (value && typeof value === 'object') {
    return `${flattenText(value.text)}${flattenText(value.content)}`;
  }
  return '';
};

export const firstNonEmptyText = (...values) => {
  for (const value of values) {
    const text = flattenText(value).trim();
    if (text) return text;
  }
  return '';
};

// 过渡适配（TODO step-4）：legacy→Tiptap inline marks 转换函数组。
// AST/block 迁移 complete 后，前端应直接消费 block document 中的
// tiptap 格式 marks（已由 Rust 侧 local_markdown_parser 输出），
// 不再需要此 JS 侧样式→marks 适配层。
export const legacyStylesToTiptapMarks = (styles) => {
  if (!styles || typeof styles !== 'object') return [];
  const marks = [];
  if (styles.bold) marks.push({ type: 'bold' });
  if (styles.italic) marks.push({ type: 'italic' });
  if (styles.underline) marks.push({ type: 'underline' });
  if (styles.strike || styles.strikethrough) marks.push({ type: 'strike' });
  if (styles.code || styles.inlineCode) marks.push({ type: 'code' });
  const href = typeof styles.link === 'string' && styles.link.trim()
    ? styles.link.trim()
    : typeof styles.href === 'string' && styles.href.trim()
      ? styles.href.trim()
      : '';
  if (href) marks.push({ type: 'link', attrs: { href } });
  return marks;
};

export const legacyMarkArrayToTiptapMarks = (inlineMarks) => {
  if (!Array.isArray(inlineMarks)) return [];
  return inlineMarks.flatMap((mark) => {
    if (!mark || typeof mark !== 'object') return [];
    if (mark.type === 'bold' || mark.type === 'italic' || mark.type === 'underline' || mark.type === 'strike' || mark.type === 'code') {
      return [{ type: mark.type }];
    }
    if (mark.type === 'link') {
      const href = typeof mark?.attrs?.href === 'string' ? mark.attrs.href.trim() : '';
      return href ? [{ type: 'link', attrs: { href } }] : [];
    }
    return [];
  });
};

export const mergeTiptapMarks = (...groups) => {
  const seen = new Set();
  return groups.flat().filter((mark) => {
    const key = `${mark.type}:${JSON.stringify(mark.attrs || {})}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
};

export const legacyInlineContentToTiptap = (value) => {
  if (typeof value === 'string') return value ? [{ type: 'text', text: value }] : [];
  if (Array.isArray(value)) return value.flatMap(legacyInlineContentToTiptap);
  if (value && typeof value === 'object') {
    const text = typeof value.text === 'string' ? value.text : '';
    if (text) {
      const marks = mergeTiptapMarks(
        legacyStylesToTiptapMarks(value.styles),
        legacyMarkArrayToTiptapMarks(value.marks)
      );
      return [{ type: 'text', text, ...(marks.length ? { marks } : {}) }];
    }
    return legacyInlineContentToTiptap(value.content || value.contentNodes);
  }
  return [];
};

export const legacyBlockToTiptap = (block, index = 0) => {
  const type = typeof block?.type === 'string' ? block.type : 'paragraph';
  const blockId = typeof block?.id === 'string' && block.id.trim()
    ? block.id.trim()
    : typeof block?.blockId === 'string' && block.blockId.trim()
      ? block.blockId.trim()
      : `block-${index + 1}`;
  const content = legacyInlineContentToTiptap(block?.content ?? block?.contentNodes);
  const textAlign = typeof block?.props?.textAlign === 'string'
    ? block.props.textAlign
    : typeof block?.props?.text_align === 'string'
      ? block.props.text_align
      : undefined;
  const withTextAlign = (attrs = {}) => textAlign ? { ...attrs, textAlign } : attrs;
  const nestedChildren = Array.isArray(block?.children)
    ? block.children.map((child, childIndex) => legacyBlockToTiptap(child, childIndex)).filter(Boolean)
    : [];
  const withListChildren = (itemType, listType, attrs = {}) => ({
    type: listType,
    attrs: { blockId },
    content: [{
      type: itemType,
      attrs: { blockId, ...attrs },
      content: [
        { type: 'paragraph', attrs: { blockId }, content },
        ...nestedChildren,
      ],
    }],
  });
  if (type === 'heading') {
    const level = Number(block?.props?.level || block?.level || 1) || 1;
    const collapsed = typeof block?.props?.collapsed === 'boolean' ? { collapsed: block.props.collapsed } : {};
    return { type: 'heading', attrs: withTextAlign({ blockId, level: Math.max(1, Math.min(6, level)), ...collapsed }), content };
  }
  if (type === 'bulletListItem' || type === 'bullet_list_item') return withListChildren('listItem', 'bulletList');
  if (type === 'numberedListItem' || type === 'numbered_list_item') return withListChildren('listItem', 'orderedList');
  if (type === 'checkListItem' || type === 'advancedTodo' || type === 'todo') {
    return withListChildren('taskItem', 'taskList', { checked: Boolean(block?.props?.checked) });
  }
  if (type === 'quote' || type === 'blockquote') {
    return { type: 'blockquote', attrs: withTextAlign({ blockId }), content: [{ type: 'paragraph', attrs: withTextAlign({ blockId }), content }] };
  }
  if (type === 'codeBlock' || type === 'code_block') {
    return { type: 'codeBlock', attrs: withTextAlign({ blockId, language: block?.props?.language || null }), content };
  }
  if (type === 'divider' || type === 'horizontalRule' || type === 'horizontal_rule') return { type: 'horizontalRule', attrs: { blockId } };
  if (type === 'mindmap') {
    const data = block?.props?.data && typeof block.props.data === 'object' ? block.props.data : null;
    const mindmapId = firstNonEmptyText(
      block?.props?.mindmapId,
      block?.props?.mindmap_id,
      block?.props?.sourcePath,
      block?.props?.source_path,
      block?.mindmapId,
      block?.mindmap_id,
      data?.mindmapId,
      data?.mindmap_id,
      data?.id
    );
    const rootNodeId = firstNonEmptyText(block?.props?.rootNodeId, block?.props?.root_node_id, data?.rootNodeId, data?.root_node_id) || 'root';
    return {
      type: 'paragraph',
      attrs: withTextAlign({
        blockId,
        mnoteBlockType: 'mindmap',
        mindmapId,
        rootNodeId,
      }),
    };
  }
  if (type === 'media') {
    const sourcePath = firstNonEmptyText(block?.props?.sourcePath, block?.props?.url, block?.props?.src);
    const name = firstNonEmptyText(block?.props?.name, block?.props?.fileName, block?.props?.title, sourcePath);
    const mediaContent = name
      ? [{
          type: 'text',
          text: name,
          ...(sourcePath ? { marks: [{ type: 'link', attrs: { href: sourcePath } }] } : {}),
        }]
      : content;
    return { type: 'paragraph', attrs: withTextAlign({ blockId }), content: mediaContent };
  }
  if (type === 'table') {
    const tableSnapshot = block?.props?.tiptapTable;
    if (tableSnapshot && typeof tableSnapshot === 'object' && tableSnapshot.type === 'table') return tableSnapshot;
    return {
      type: 'table',
      content: [{
        type: 'tableRow',
        content: [{
          type: 'tableCell',
          attrs: { colspan: 1, rowspan: 1, colwidth: null },
          content: [{ type: 'paragraph', content }],
        }],
      }],
    };
  }
  if (type === 'toc' || type === 'tocNode' || type === 'toc_node') {
    const tocSnapshot = block?.props?.tiptapTocNode || block?.props?.tiptapToc;
    if (tocSnapshot && typeof tocSnapshot === 'object' && tocSnapshot.type === 'tocNode') return tocSnapshot;
    return {
      type: 'tocNode',
      attrs: {
        topOffset: Number(block?.props?.topOffset || block?.props?.top_offset || 0) || 0,
        maxShowCount: Number(block?.props?.maxShowCount || block?.props?.max_show_count || 20) || 20,
        showTitle: block?.props?.showTitle !== false,
      },
    };
  }
  if (type === 'image') {
    const imageSnapshot = block?.props?.tiptapImage;
    if (imageSnapshot && typeof imageSnapshot === 'object' && imageSnapshot.type === 'image') return imageSnapshot;
    const attrs = {
      src: String(block?.props?.src || block?.src || ''),
      alt: block?.props?.alt || block?.alt || null,
      title: block?.props?.title || block?.title || null,
    };
    return attrs.src ? { type: 'image', attrs: { blockId, ...attrs } } : null;
  }
  return { type: 'paragraph', attrs: withTextAlign({ blockId }), content };
};

export const textToTiptapDocument = (text) => ({
  type: 'doc',
  content: [{
    type: 'paragraph',
    content: text ? [{ type: 'text', text }] : [],
  }],
});

export const isTiptapDocument = (content) => (
  content &&
  typeof content === 'object' &&
  !Array.isArray(content) &&
  content.type === 'doc'
);

export const mindmapDomDescriptors = (root) => {
  if (!(root instanceof HTMLElement)) return [];
  return Array.from(root.querySelectorAll('[data-testid="mnote-mindmap-editor-root"]'))
    .flatMap((node) => {
      if (!(node instanceof HTMLElement)) return [];
      const mindmapId = typeof node.dataset.mnoteMindmapId === 'string' ? node.dataset.mnoteMindmapId.trim() : '';
      if (!mindmapId) return [];
      const rootNodeId = typeof node.dataset.mnoteRootNodeId === 'string' && node.dataset.mnoteRootNodeId.trim()
        ? node.dataset.mnoteRootNodeId.trim()
        : 'root';
      return [{ mindmapId, rootNodeId }];
    });
};

export const hydrateMindmapAttrsFromDom = (tiptapDocument, root) => {
  if (!isTiptapDocument(tiptapDocument) || !Array.isArray(tiptapDocument.content)) return tiptapDocument;
  const descriptors = mindmapDomDescriptors(root);
  if (!descriptors.length) return tiptapDocument;
  let index = 0;
  for (const node of tiptapDocument.content) {
    if (node?.type !== 'paragraph' || node?.attrs?.mnoteBlockType !== 'mindmap') continue;
    const descriptor = descriptors[index];
    index += 1;
    if (!descriptor) continue;
    node.attrs = node.attrs && typeof node.attrs === 'object' ? node.attrs : {};
    if (typeof node.attrs.mindmapId !== 'string' || !node.attrs.mindmapId.trim()) {
      node.attrs.mindmapId = descriptor.mindmapId;
    }
    if (typeof node.attrs.rootNodeId !== 'string' || !node.attrs.rootNodeId.trim()) {
      node.attrs.rootNodeId = descriptor.rootNodeId;
    }
  }
  return tiptapDocument;
};

export const toTiptapDocument = (content, fallbackText = '') => {
  if (isTiptapDocument(content)) return content;
  const blocks = Array.isArray(content) ? content : Array.isArray(content?.blocks) ? content.blocks : [];
  const nodes = blocks.map(legacyBlockToTiptap).filter(Boolean);
  if (nodes.length) return { type: 'doc', content: nodes };
  return textToTiptapDocument(fallbackText);
};

export const decodeLocalIdSegment = (segment) => {
  const encoded = String(segment || '').replace(/~([0-9a-fA-F]{2})/g, '%$1');
  try {
    return decodeURIComponent(encoded);
  } catch (_) {
    return String(segment || '').replace(/~2F/g, '/').replace(/~20/g, ' ');
  }
};

export const localMarkdownRelativePathFromDocumentId = (documentId) => {
  const raw = String(documentId || '').trim();
  const segment = raw.startsWith('local-md:') ? raw.slice('local-md:'.length) : raw;
  return decodeLocalIdSegment(segment).replace(/^\/+/, '');
};

export const localMarkdownDocumentIdFromRelativePath = (relativePath) => {
  const normalized = String(relativePath || '').trim().replace(/^\/+/, '');
  if (!normalized) return '';
  return 'local-md:' + normalized
    .split('/')
    .map((part) => part.replace(/ /g, '~20'))
    .join('~2F');
};

export const localMarkdownDirectoryFromDocumentId = (documentId) => {
  const relativePath = localMarkdownRelativePathFromDocumentId(documentId);
  const slash = relativePath.lastIndexOf('/');
  return slash >= 0 ? relativePath.slice(0, slash) : '';
};

export const isExternalOrSpecialUrl = (value) => {
  const text = String(value || '').trim();
  return !text
    || text.startsWith('#')
    || text.startsWith('data:')
    || text.startsWith('blob:')
    || text.startsWith('mailto:')
    || text.startsWith('http://')
    || text.startsWith('https://')
    || text.startsWith('/api/');
};

export const normalizeLocalAssetRelativePath = (value, context) => {
  const text = String(value || '').trim();
  if (!text || isExternalOrSpecialUrl(text)) return text;
  if (text.startsWith('/')) return text.replace(/^\/+/, '');
  const baseDir = localMarkdownDirectoryFromDocumentId(context?.documentId);
  return (baseDir ? `${baseDir}/${text}` : text)
    .split('/')
    .filter((part) => part && part !== '.')
    .join('/');
};

export const localFileOpenUrlForTiptap = (value, context) => {
  if (!context || context.sourceKind !== 'local_folder' || !context.rootUri) return value;
  const relativePath = normalizeLocalAssetRelativePath(value, context);
  if (!relativePath || isExternalOrSpecialUrl(relativePath)) return value;
  const url = new URL('/api/local-folder/files/open', window.location.origin);
  url.searchParams.set('rootUri', context.rootUri);
  url.searchParams.set('path', relativePath);
  return url.toString();
};

export const localFileOpenPathFromTiptapHref = (href) => {
  try {
    const url = new URL(String(href || ''), window.location.origin);
    if (url.pathname !== '/api/local-folder/files/open') return '';
    return String(url.searchParams.get('path') || '').trim();
  } catch (_) {
    return '';
  }
};

export const fileNameFromPath = (path) => {
  const value = String(path || '').trim();
  return value.includes('/') ? value.split('/').pop() : value;
};

export const attachmentClassForFileName = (fileName) => {
  const name = String(fileName || '').trim().toLowerCase();
  const ext = name.includes('.') ? name.split('.').pop() : '';
  if (['doc', 'docx', 'odt', 'rtf'].includes(ext)) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-word';
  if (['ppt', 'pptx', 'odp'].includes(ext)) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-ppt';
  if (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-sheet';
  if (ext === 'pdf') return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-pdf';
  if (['md', 'markdown', 'txt', 'json', 'js', 'ts', 'tsx', 'jsx', 'rs', 'py', 'go', 'java', 'kt', 'swift', 'c', 'cpp', 'h', 'hpp', 'css', 'scss', 'html', 'xml', 'yaml', 'yml', 'toml', 'ini', 'sh', 'bash', 'zsh', 'sql', 'vue', 'svelte'].includes(ext)) {
    return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-code';
  }
  return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-file';
};

export const mergeClassNames = (...values) => {
  const seen = new Set();
  return values
    .flatMap((value) => String(value || '').split(/\s+/))
    .filter((name) => {
      if (!name || seen.has(name)) return false;
      seen.add(name);
      return true;
    })
    .join(' ');
};

export const localAttachmentClassForTiptapHref = (href) => {
  const localFilePath = localFileOpenPathFromTiptapHref(href);
  return localFilePath ? attachmentClassForFileName(fileNameFromPath(localFilePath)) : '';
};

export const localizeTiptapAssetUrls = (node, context) => {
  if (!node || typeof node !== 'object') return node;
  if (node.type === 'image' && node.attrs && typeof node.attrs.src === 'string') {
    node.attrs = { ...node.attrs, src: localFileOpenUrlForTiptap(node.attrs.src, context) };
  }
  if (Array.isArray(node.marks)) {
    node.marks = node.marks.map((mark) => {
      if (!mark || mark.type !== 'link' || !mark.attrs || typeof mark.attrs.href !== 'string') return mark;
      const href = localFileOpenUrlForTiptap(mark.attrs.href, context);
      const attachmentClass = localAttachmentClassForTiptapHref(href);
      const attrs = attachmentClass
        ? {
            ...mark.attrs,
            href,
            class: mergeClassNames(mark.attrs.class, attachmentClass),
            target: mark.attrs.target || '_blank',
            rel: mark.attrs.rel || 'noopener noreferrer nofollow',
          }
        : { ...mark.attrs, href };
      return { ...mark, attrs };
    });
  }
  if (Array.isArray(node.content)) {
    node.content = node.content.map((child) => localizeTiptapAssetUrls(child, context));
  }
  return node;
};

export const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => {
  if (String(body?.projectionSource || body?.projection_source || '') === 'local_markdown.content' && body?.content) {
    return localizeTiptapAssetUrls(toTiptapDocument(body.content, fallbackText), context);
  }
  const blockDocument = body?.blockDocument || body?.block_document || body?.editorDocument || body?.editor_document || null;
  if (blockDocument) return localizeTiptapAssetUrls(toTiptapDocument(blockDocument, fallbackText), context);
  return localizeTiptapAssetUrls(toTiptapDocument(body?.content, fallbackText), context);
};

export const inlineTextNodes = (node) => {
  if (!node || typeof node !== 'object') return [];
  if (Array.isArray(node.content)) {
    return node.content.flatMap((child) => {
      if (child?.type === 'text') {
        const text = typeof child.text === 'string' ? child.text : '';
        if (!text) return [];
        const styles = {};
        const marks = [];
        for (const mark of Array.isArray(child.marks) ? child.marks : []) {
          if (mark?.type === 'bold') {
            styles.bold = true;
            marks.push('bold');
          }
          if (mark?.type === 'italic') {
            styles.italic = true;
            marks.push('italic');
          }
          if (mark?.type === 'underline') {
            styles.underline = true;
            marks.push('underline');
          }
          if (mark?.type === 'strike') {
            styles.strike = true;
            marks.push('strike');
          }
          if (mark?.type === 'code') {
            styles.code = true;
            marks.push('code');
          }
          if (mark?.type === 'link') {
            const href = typeof mark?.attrs?.href === 'string' ? mark.attrs.href.trim() : '';
            if (href) styles.link = href;
          }
        }
        return [{
          payload: { type: 'text', text, ...(marks.length ? { marks } : {}) },
          attrs: Object.keys(styles).length ? { styles } : {},
          type: 'text',
          text,
          ...(Object.keys(styles).length ? { styles } : {}),
        }];
      }
      if (child?.type === 'hardBreak') return [{ payload: { type: 'hard_break' }, attrs: {}, type: 'text', text: '\n' }];
      return inlineTextNodes(child);
    });
  }
  return [];
};

export const firstChild = (node) => Array.isArray(node?.content) ? node.content[0] : null;
export const blockIdOf = (node, index) => {
  const raw = typeof node?.attrs?.blockId === 'string' ? node.attrs.blockId.trim() : '';
  return raw || `block-${index + 1}`;
};
export const mindmapPropsFromAttrs = (attrs, fallbackMindmapId) => {
  const data = attrs?.data && typeof attrs.data === 'object' ? attrs.data : {};
  const mindmapId = firstNonEmptyText(
    attrs?.mindmapId,
    attrs?.mindmap_id,
    attrs?.sourcePath,
    attrs?.source_path,
    data?.mindmapId,
    data?.mindmap_id,
    data?.id,
    fallbackMindmapId
  );
  const rootNodeId = firstNonEmptyText(attrs?.rootNodeId, attrs?.root_node_id, data?.rootNodeId, data?.root_node_id) || 'root';
  const projectionVersion = Number(attrs?.projectionVersion ?? attrs?.projection_version);
  return {
    mindmapId,
    rootNodeId,
    ...(Number.isFinite(projectionVersion) ? { projectionVersion } : {}),
  };
};

export const tiptapNodeToEditorBlock = (node, index) => {
  const blockId = blockIdOf(node, index);
  if (node?.type === 'paragraph' && node?.attrs?.mnoteBlockType === 'mindmap') {
    const mindmapId = firstNonEmptyText(
      node?.attrs?.mindmapId,
      node?.attrs?.mindmap_id,
      node?.attrs?.sourcePath,
      node?.attrs?.source_path,
      blockId
    );
    return {
      blockId,
      blockType: 'mindmap',
      props: {
        ...mindmapPropsFromAttrs(node?.attrs, blockId),
        sourcePath: mindmapId,
      },
      contentNodes: [],
      childBlockIds: [],
    };
  }
  if (node?.type === 'paragraph') return { blockId, blockType: 'paragraph', props: {}, contentNodes: inlineTextNodes(node), childBlockIds: [] };
  if (node?.type === 'heading') {
    const level = Math.max(1, Math.min(6, Number(node?.attrs?.level || 1) || 1));
    return { blockId, blockType: 'heading', props: { headingLevel: level }, contentNodes: inlineTextNodes(node), childBlockIds: [] };
  }
  if (node?.type === 'bulletList') return { blockId, blockType: 'bullet_list_item', props: {}, contentNodes: inlineTextNodes(firstChild(firstChild(node))), childBlockIds: [] };
  if (node?.type === 'orderedList') return { blockId, blockType: 'numbered_list_item', props: {}, contentNodes: inlineTextNodes(firstChild(firstChild(node))), childBlockIds: [] };
  if (node?.type === 'taskList') {
    const taskItem = firstChild(node);
    return { blockId, blockType: 'todo', props: { checked: Boolean(taskItem?.attrs?.checked) }, contentNodes: inlineTextNodes(firstChild(taskItem)), childBlockIds: [] };
  }
  if (node?.type === 'blockquote') return { blockId, blockType: 'quote', props: {}, contentNodes: inlineTextNodes(firstChild(node)), childBlockIds: [] };
  if (node?.type === 'codeBlock') return { blockId, blockType: 'code_block', props: { language: typeof node?.attrs?.language === 'string' ? node.attrs.language : null }, contentNodes: inlineTextNodes(node), childBlockIds: [] };
  if (node?.type === 'horizontalRule') return { blockId, blockType: 'divider', props: {}, contentNodes: [], childBlockIds: [] };
  if (node?.type === 'image') return { blockId, blockType: 'image', props: { src: node?.attrs?.src || '', alt: node?.attrs?.alt || null, title: node?.attrs?.title || null, tiptapImage: node }, contentNodes: [], childBlockIds: [] };
  if (node?.type === 'tocNode') return { blockId, blockType: 'toc', props: { tiptapTocNode: node }, contentNodes: [], childBlockIds: [] };
  if (node?.type === 'table') return { blockId, blockType: 'table', props: { tiptapTable: node }, contentNodes: inlineTextNodes(node), childBlockIds: [] };
  return null;
};

export const editorDocumentFromTiptapDocument = (bootstrap, tiptapDocument) => {
  const content = Array.isArray(tiptapDocument?.content) ? tiptapDocument.content : [];
  const blocks = content.map(tiptapNodeToEditorBlock).filter(Boolean);
  return {
    documentId: bootstrap.documentId,
    rootBlockIds: blocks.map((block) => block.blockId),
    blocks,
  };
};

export const legacyBlocksFromEditorDocument = (editorDocument) => (
  Array.isArray(editorDocument?.blocks) ? editorDocument.blocks : []
).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'
          ? mindmapPropsFromAttrs(block.props || {}, block.blockId)
        : block.blockType === 'image'
          ? { ...(block.props || {}) }
          : block.blockType === 'toc'
            ? { ...(block.props || {}) }
            : block.blockType === 'table'
              ? { ...(block.props || {}) }
              : undefined,
  content: block.blockType === 'mindmap'
    ? ''
    : Array.isArray(block.contentNodes)
    ? block.contentNodes.map((node) => {
      if (!node || typeof node !== 'object') return null;
      const payload = node.payload && typeof node.payload === 'object' ? node.payload : {};
      const text = typeof payload.text === 'string'
        ? payload.text
        : payload.type === 'hard_break'
          ? '\n'
          : typeof node.text === 'string'
            ? node.text
            : '';
      if (!text) return null;
      const attrs = node.attrs && typeof node.attrs === 'object' ? node.attrs : {};
      const styles = attrs.styles && typeof attrs.styles === 'object'
        ? attrs.styles
        : node.styles && typeof node.styles === 'object'
          ? node.styles
          : null;
      const marks = Array.isArray(payload.marks) ? payload.marks : [];
      return {
        type: 'text',
        text,
        ...(styles ? { styles } : {}),
        ...(marks.length ? { marks } : {}),
      };
    }).filter(Boolean)
    : '',
}));

export const conflictDetectionKeyFromBody = (body) => typeof body?.conflictDetectionKey === 'string'
  ? body.conflictDetectionKey
  : typeof body?.conflict_detection_key === 'string'
    ? body.conflict_detection_key
    : typeof body?.fileVersion === 'string'
      ? body.fileVersion
      : typeof body?.file_version === 'string'
        ? body.file_version
        : null;

export const conflictDetectionKeyBelongsToSession = (session, key) => {
  if (!session || session.sourceKind !== 'local_folder') return true;
  const value = String(key || '').trim();
  if (!value) return false;
  return value.startsWith(`local-md:${session.documentId}:`);
};

export const revisionFromConflictKey = (value) => {
  const match = String(value || '').match(/:(\d+)$/);
  return match ? Number(match[1]) : null;
};
