/**
 * @fileoverview Utilities for handling paste operations in a block-based UI system.
 * This module provides functions for processing clipboard data and converting it
 * into appropriate block structures for a Tiptap-based editor.
 */

import type { Slice } from '@tiptap/pm/model'

import type { EditorProps, EditorView } from '@tiptap/pm/view'
import type { ApplogForInsertOptionalAgent, EntityID } from '@wovin/core/applog'
import { tiptapToPlaintext } from '@note3/utils'
import { generateJSON } from '@tiptap/core'
import { Logger } from 'besonders-logger'
import { XMLParser } from 'fast-xml-parser'
import rehypeFormat from 'rehype-format'
import rehypeStringify from 'rehype-stringify'
import remarkGfm from 'remark-gfm'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import { useContext } from 'solid-js'
import { unified } from 'unified'
import { is as isLikelyMarkdown } from 'very-small-parser/lib/markdown/is'
import { reporter } from 'vfile-reporter'
import { BlockContext } from '../components/BlockTree'
import { baseExtensions, htmlToTiptap, markdownExtensions } from '../components/TipTapExtentions'
import { useFocus, useThreadFromContext } from '../ui/reactive'
import { focusViewOnBlock, tryParseNote3URL, useLocationNavigate } from '../ui/utils-ui'
import { getSubOrShare } from './agent/utils-agent'
import { insertApplogs } from './ApplogDB'
import { deleteAndReplaceBlock } from './block-utils'
import { serializeTiptapToVl } from './block-utils-nowin'
import { BlockVM } from './VMs/BlockVM'
import { RelationVM } from './VMs/RelationVM'

const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO)

export function useBlockContext() {
	return useContext(BlockContext)
}

// tests
/**
 * Tests if a string is a valid URL
 * @param {string} url - The string to test
 * @returns {string|boolean} The URL as a string if valid, false otherwise
 */
export function isValidURL(url) {
	try {
		const _urlObj = new URL(url)
		return _urlObj.toString()
	} catch (e) {
		return false
	}
}

/**
 * Determines if text appears to be markdown (specifically checking for bullet lists)
 * @param {string} maybeMarkdownText - Text to check
 * @returns {boolean} True if text appears to be markdown
 */
const isMarkdown = (maybeMarkdownText: string) => {
	// Check if it looks like markdown bullet list
	const lines = maybeMarkdownText.trim().split('\n')
	const hasMarkdownBullets = lines.some((line: string) => /^\s?[-*+#]\s/.test(line)) // HACKish
	VERBOSE({ lines, hasMarkdownBullets })
	return hasMarkdownBullets
}

/**
 * Checks if the application has permission to read from the clipboard
 * @returns {Promise<void>} Resolves if permission is granted, throws an error if denied
 */
export async function getClipboardPerms() {
	const permission = await navigator.permissions.query({
		// @ts-ignore - clipboard-read is a valid permission name
		name: 'clipboard-read',
	} as PermissionDescriptor)
	if (permission.state === 'denied') {
		throw new Error('Not allowed to read clipboard.')
	}
}
/**
 * Retrieves the contents of the clipboard
 * @returns {Promise<ClipboardItems>} The clipboard contents
 */
export async function getClipboardContents() {
	await getClipboardPerms()
	const clipboardContents = await navigator.clipboard.read()
	return clipboardContents
}
/**
 * Extracts and validates a URL from the clipboard
 * @returns {Promise<string|null>} A valid URL string or null if none found
 */
export async function getValidURLFromClipboard() {
	const clipboardContents = await getClipboardContents()
	const maybeValidURL = await (await clipboardContents[0]?.getType('text/plain'))?.text()
	return isValidURL(maybeValidURL) ? maybeValidURL : null
}
/**
 * Retrieves plain text content from the clipboard
 * @returns {Promise<string|undefined>} The plain text content or undefined
 */
export async function getPlainTextFromClipboard() {
	const clipboardContents = await getClipboardContents()
	const text = await (await clipboardContents[0]?.getType('text/plain'))?.text()
	if (text) DEBUG('plain text found', text)
	return text
}
/**
 * Retrieves HTML content from the clipboard
 * @returns {Promise<string|null>} The HTML content or null if not available
 */
export async function getHTMLFromClipboard() {
	const clipboardContents = await getClipboardContents()
	if (!clipboardContents[0].types.includes('text/html')) {
		return null
	}
	const htmlText = await (await clipboardContents[0]?.getType('text/html'))?.text()
	DEBUG('html found', htmlText)
	return htmlText
}

/**
 * Extracts a URI from clipboard data
 * @param {DataTransfer} clipboard - The clipboard data transfer object
 * @returns {string|null} A valid URI string or null
 */
export function getURIfromClipboard(clipboard: DataTransfer) {
	const plainText = clipboard.getData('text')
	const uriString = isValidURL(plainText) || null
	return uriString
}
/**
 * Extracts image data from clipboard
 * @param {DataTransfer} clipboard - The clipboard data transfer object
 * @returns {object | null} An object containing image data (blob, URL, type) or null
 */
export function getImagefromClipboard(clipboard: DataTransfer) {
	const firstItem = clipboard.items[0]
	const isImage = firstItem.kind === 'file' && firstItem.type.includes('image/')
	if (!isImage) return null
	const imgBlob = firstItem.getAsFile()

	const URLObj = window.URL || window.webkitURL
	const imgURL = URLObj.createObjectURL(imgBlob)
	const imgType = firstItem.type
	DEBUG('[getImageFromClipboard]', { imgType, imgURL })
	return { imgBlob, imgURL, imgType }
}

/**
 * Interface representing a Tiptap node structure
 */
interface TiptapNode {
	type: string
	content?: TiptapNode[]
	text?: string
	attrs?: {
		level?: number
	}
	children?: TiptapNode[]
	txt?: string
}
/**
 * Extracts content from a Tiptap node
 * @param {TiptapNode} node - The Tiptap node to extract content from
 * @returns {string} The serialized content
 */
const getBlockContent = (node: TiptapNode): string => {
	if (['heading', 'paragraph'].includes(node.type)) {
		const tempDoc: TiptapNode = { type: 'doc', content: [node] }
		return serializeTiptapToVl(tempDoc)
	} else if (node.type === 'listItem') {
		const firstPara = node.content?.find(n => n.type === 'paragraph')
		if (firstPara) {
			const tempDoc: TiptapNode = { type: 'doc', content: [firstPara] }
			return serializeTiptapToVl(tempDoc)
		}
		return ''
	}
	return ''
}
/**
 * Processes a bullet list node and creates corresponding blocks
 * @param {TiptapNode} bulletList - The bullet list node to process
 * @param {EntityID} parentID - The ID of the parent block
 * @param {ApplogForInsertOptionalAgent[]} logsArray - Array to push new applogs to
 * @param {EntityID | null} afterStart - ID of the block to insert after
 * @returns {EntityID | null} The ID of the last created block or null
 */
const processBulletList = (
	bulletList: TiptapNode,
	parentID: EntityID,
	logsArray: ApplogForInsertOptionalAgent[],
	afterStart: EntityID | null = null,
): EntityID | null => {
	if (bulletList.type !== 'bulletList' || !bulletList.content) return null

	let after = afterStart
	let lastCreated = after
	for (const listItem of bulletList.content) {
		if (listItem.type !== 'listItem') continue

		const content = getBlockContent(listItem)
		if (!content) continue

		// Create block for this list item
		const blockBuilder = BlockVM.buildNew({ content })
		logsArray.push(...blockBuilder.build())
		logsArray.push(...(RelationVM.buildNew({ childOf: parentID, block: blockBuilder.en, after })).build())
		after = blockBuilder.en
		lastCreated = after

		// Check for nested bulletList
		const nestedList = listItem.content?.find(node => node.type === 'bulletList')
		if (nestedList) {
			processBulletList(nestedList, blockBuilder.en, logsArray, null)
		}
	}
	return lastCreated
}
/**
 * Organizes headings by their level to create a nested structure
 * @param {Array} arrayOfTipTapHeadingsWithChildren - Array of Tiptap heading nodes with children
 * @returns {Array} The nested heading structure
 */
const nestHeadingsByLevel = (arrayOfTipTapHeadingsWithChildren: { children: TiptapNode['content'] } & TiptapNode['content']) => {
	const nestedByLevel = [...arrayOfTipTapHeadingsWithChildren]
	if (nestedByLevel.length > 1) {
		// Create a mapping of heading level to its index for quick lookup
		const headingIndices: number[] = []
		for (let i = 0; i < nestedByLevel.length; i++) {
			if (nestedByLevel[i].type === 'heading') {
				headingIndices.push(i)
			}
		}

		// Process each heading to find its proper parent
		for (let eachIdx = nestedByLevel.length - 1; eachIdx >= 0; eachIdx--) {
			const thisHeading = nestedByLevel[eachIdx]
			// @ts-ignore - attrs property exists on Tiptap nodes
			if (thisHeading.type === 'heading' && thisHeading.attrs?.level && thisHeading.attrs.level > 1) {
				// Find the nearest preceding heading with a smaller level (closer to root)
				let parentHeading = null
				let parentIndex = -1

				// Look backwards through the heading indices to find the closest larger heading
				for (let hIdx = headingIndices.length - 1; hIdx >= 0; hIdx--) {
					const idx = headingIndices[hIdx]
					if (idx < eachIdx) {
						const candidateHeading = nestedByLevel[idx]
						if (
							candidateHeading.type === 'heading'
							// @ts-ignore - attrs property exists on Tiptap nodes
							&& candidateHeading.attrs?.level
							// @ts-ignore - attrs property exists on Tiptap nodes
							&& candidateHeading.attrs.level < thisHeading.attrs.level
						) {
							parentHeading = candidateHeading
							parentIndex = idx
							break // Found the nearest larger heading
						}
					}
				}

				// Move this heading to be a child of its parent
				if (parentHeading) {
					// Remove from current position
					const [movedHeading] = nestedByLevel.splice(eachIdx, 1)
					// Add to parent's children (at the beginning to preserve order)
					parentHeading.children = parentHeading.children || []
					parentHeading.children.push(movedHeading)

					// Update heading indices array since we removed an element
					headingIndices.splice(headingIndices.indexOf(eachIdx), 1)
				}
			}
		}
	}
	return nestedByLevel
}
/**
 * Reorganizes Tiptap nodes by type, grouping non-headings under headings
 * @param {TiptapNode['content']} markdownChildren - Array of Tiptap nodes
 * @returns {Array} Reorganized nodes
 */
export const reorganizeTiptapNodesByType = (markdownChildren: TiptapNode['content']) => {
	let nestedByHeadings = []
	if (markdownChildren.find(n => n.type === 'heading')) {
		let newkidsforheading = []
		for (let eachIdx = markdownChildren.length - 1; eachIdx >= 0; eachIdx--) {
			const eachNode = markdownChildren[eachIdx]
			if (eachNode.type !== 'heading') {
				// crawl back until a heading is found and
				const eachMaybeNonHeadingNode = markdownChildren[eachIdx]
				if (eachMaybeNonHeadingNode.type !== 'heading') {
					newkidsforheading.push(eachMaybeNonHeadingNode) // markdownChildren.splice(eachIdx, 1)[0])
				}
			} else {
				// @ts-ignore - extending TiptapNode with custom properties
				eachNode.txt = tiptapToPlaintext(eachNode.content?.[0])
				// @ts-ignore - extending TiptapNode with custom properties
				eachNode.children = eachNode.children ?? []
				// @ts-ignore - extending TiptapNode with custom properties
				eachNode.children.push(...newkidsforheading)
				newkidsforheading = []
				nestedByHeadings.unshift(eachNode)
			}
		}
	} else {
		nestedByHeadings = [...markdownChildren]
	}
	return nestedByHeadings
}
/**
 * Reorganizes Tiptap nodes by both type and heading level
 * @param {TiptapNode['content']} markdownChildren - Array of Tiptap nodes
 * @returns {Array} Reorganized nodes nested by heading level
 */
export const reorganizeTiptapNodesByTypeAndLevel = (markdownChildren: TiptapNode['content']) => {
	// reorganize the nested tree so that:
	//  - lower levels are children of higher ones
	//  - all non headings are nested within the headings
	const nestedByHeadings = reorganizeTiptapNodesByType(markdownChildren)
	const nestedWithinHeadingsAndByLevel = nestHeadingsByLevel(nestedByHeadings) // TODO make the nesting by heading optional

	VERBOSE.force({ nestedByHeadings, nestedWithinHeadingsAndByLevel })
	return nestedWithinHeadingsAndByLevel
}
/**
 * Converts markdown to HTML using remark/rehype processors
 * @param {string} markDownWithTodosReplaced - Markdown text with todo items replaced
 * @returns {Promise<string>} The resulting HTML string
 */
const htmlFromMarkdownRemark = async (markDownWithTodosReplaced) => {
	// TODO also catch - [ ] and other variants
	const file = await unified()
		.use(remarkParse)
		.use(remarkGfm) // ← enable GFM: tables, task lists, strikethrough, autolinks
		.use(remarkRehype)
		.use(rehypeFormat)
		.use(rehypeStringify)
		.process(markDownWithTodosReplaced)
	const htmlReporterResult = reporter(file)
	if (htmlReporterResult !== 'no issues found') WARN(htmlReporterResult)
	const htmlFromMarkdown = String(file)
	DEBUG({ htmlFromMarkdown, markDownWithTodosReplaced })
	return htmlFromMarkdown
}
/** Parsed node structure from markdown */
interface ParsedNode {
	content: ReturnType<typeof getBlockContent>
	children: ParsedNode[]
}

/** Result of parsing markdown into a structure (no applogs yet) */
interface ParsedMarkdown {
	roots: ParsedNode[]
}

/**
 * Parses markdown text into a structure without creating applogs.
 * @param {string} markdownText - The markdown text to parse
 * @returns {Promise<ParsedMarkdown | null>} Parsed structure or null if not valid markdown
 */
export const parseMarkdownStructure = async (
	markdownText: string,
): Promise<ParsedMarkdown | null> => {
	if (!isLikelyMarkdown(markdownText)) return null

	VERBOSE('[parseMarkdownStructure] Detected markdown bullets', { markdownText })
	const markDownWithTodosReplaced = markdownText.replaceAll('* [ ]', '+todo').replaceAll('* [x]', '* +todo/done') // HACK
	const htmlFromMarkdown = await htmlFromMarkdownRemark(markDownWithTodosReplaced)

	const tiptapJson = generateJSON(htmlFromMarkdown, markdownExtensions) as TiptapNode
	const maybeNestedUnderHeadings = reorganizeTiptapNodesByTypeAndLevel(tiptapJson.content ?? [])
	DEBUG.force({ htmlFromMarkdown, maybeNestedUnderHeadings, tiptapJson })

	if (!tiptapJson.content?.length) return null

	// Recursively convert tiptap node to ParsedNode
	const convertChildren = (nodes: any[]): ParsedNode[] => {
		const result: ParsedNode[] = []
		for (const node of nodes || []) {
			if (['heading', 'paragraph'].includes(node.type as string)) {
				const content = getBlockContent(node)
				if (!content) continue
				result.push({
					content,
					children: [
						...convertChildren(node.children || []),
						...convertBulletListChildren(node),
					],
				})
			} else if (node.type === 'bulletList') {
				result.push(...convertBulletList(node))
			}
		}
		return result
	}

	// Convert bullet list items, handling nesting
	const convertBulletList = (bulletList: TiptapNode): ParsedNode[] => {
		if (bulletList.type !== 'bulletList' || !bulletList.content) return []
		const result: ParsedNode[] = []
		for (const listItem of bulletList.content) {
			if (listItem.type !== 'listItem') continue
			const content = getBlockContent(listItem)
			if (!content) continue
			const nestedList = listItem.content?.find(n => n.type === 'bulletList')
			result.push({
				content,
				children: nestedList ? convertBulletList(nestedList) : [],
			})
		}
		return result
	}

	// Get nested bullet list from a node (for headings/paragraphs with bullet children)
	const convertBulletListChildren = (node: any): ParsedNode[] => {
		const nestedList = node.content?.find(n => n.type === 'bulletList')
		return nestedList ? convertBulletList(nestedList) : []
	}

	const roots = convertChildren(maybeNestedUnderHeadings)
	if (roots.length === 0) return null

	DEBUG.force('[parseMarkdownStructure] Result', { roots })
	return { roots }
}

/**
 * Generates applogs for pasting parsed markdown into the tree.
 * Handles all placement logic based on target state and focus.
 *
 * A block is considered "empty" only if it has no content AND no children.
 *
 * Logic matrix:
 * | Target    | Focused | Roots  | Action                                    |
 * |-----------|---------|--------|-------------------------------------------|
 * | Empty     | No      | any    | Replace target relation, delete target    |
 * | Empty     | Yes     | single | Reuse target (update content)             |
 * | Empty     | Yes     | multi  | Roots become children of target           |
 * | Non-empty | any     | any    | Roots become children of target           |
 */
export const generatePasteApplogs = ({
	parsed,
	targetBlock,
	targetRelation,
	isFocused,
}: {
	parsed: ParsedMarkdown
	targetBlock: BlockVM
	targetRelation: RelationVM | null
	isFocused: boolean
}): { logs: ApplogForInsertOptionalAgent[]; newFocusID: EntityID | null } => {
	const logs: ApplogForInsertOptionalAgent[] = []
	const isSingleRoot = parsed.roots.length === 1
	const hasChildren = (targetBlock.kidRelations?.length ?? 0) > 0
	// A block is considered empty only if it has no content AND no children
	const isEmpty = targetBlock.isEmpty && !hasChildren
	let newFocusID: EntityID | null = null

	// Helper: create a block and its children structure
	// Creates the block, then recursively creates children with relations to this block
	// The caller is responsible for creating the relation from this block to its parent
	const createBlockWithChildren = (node: ParsedNode): EntityID => {
		const builder = BlockVM.buildNew({ content: node.content })
		logs.push(...builder.build())
		const blockID = builder.en

		// Create children with relations to this block
		let lastChildID: EntityID | null = null
		for (const child of node.children) {
			const childID = createBlockWithChildren(child)
			logs.push(...RelationVM.buildNew({
				childOf: blockID,
				block: childID,
				after: lastChildID,
			}).build())
			lastChildID = childID
		}

		return blockID
	}

	// Helper: create blocks for roots and attach as children of a parent
	const createRootsAsChildren = (parentID: EntityID, afterExisting: EntityID | null) => {
		let afterID = afterExisting
		for (const root of parsed.roots) {
			const rootID = createBlockWithChildren(root)
			logs.push(...RelationVM.buildNew({
				childOf: parentID,
				block: rootID,
				after: afterID,
			}).build())
			afterID = rootID
		}
	}

	// Helper: create roots as siblings
	const createRootsAsSiblings = (
		parentID: EntityID,
		afterID: EntityID | null,
	): EntityID => {
		let lastID = afterID
		let firstRootID: EntityID | null = null
		for (const root of parsed.roots) {
			const rootID = createBlockWithChildren(root)
			logs.push(...RelationVM.buildNew({
				childOf: parentID,
				block: rootID,
				after: lastID,
			}).build())
			if (!firstRootID) firstRootID = rootID
			lastID = rootID
		}
		return firstRootID!
	}

	if (isEmpty && !isFocused) {
		// Empty (no content, no children) and not focused - can replace target
		if (targetRelation) {
			// Create first root separately
			const firstRoot = parsed.roots[0]
			const firstRootID = createBlockWithChildren(firstRoot)
			// Update existing relation to point to first root instead of target
			logs.push(...targetRelation.buildUpdate({ block: firstRootID }).build())
			// Delete the orphaned empty target
			logs.push({ en: targetBlock.en, at: 'isDeleted', vl: true })
			// Create remaining roots as siblings
			let afterID = firstRootID
			for (let i = 1; i < parsed.roots.length; i++) {
				const rootID = createBlockWithChildren(parsed.roots[i])
				logs.push(...RelationVM.buildNew({
					childOf: targetRelation.childOf,
					block: rootID,
					after: afterID,
				}).build())
				afterID = rootID
			}
			newFocusID = firstRootID
		} else {
			// No parent - just create roots as children (target becomes container)
			createRootsAsChildren(targetBlock.en, null)
		}
	} else if (isEmpty && isFocused && isSingleRoot) {
		// Reuse target block - update its content
		const root = parsed.roots[0]
		logs.push(...targetBlock.buildUpdate({ content: root.content }).build())

		// Create children under target
		let lastChildID: EntityID | null = null
		for (const child of root.children) {
			const childID = createBlockWithChildren(child)
			logs.push(...RelationVM.buildNew({
				childOf: targetBlock.en,
				block: childID,
				after: lastChildID,
			}).build())
			lastChildID = childID
		}
		// newFocusID stays null - already focused on target
	} else {
		// Empty + focused + multi OR non-empty: roots become children of target
		// Find last existing child for proper ordering
		const lastExistingChild = targetBlock.kidRelations?.at(-1)?.block ?? null
		createRootsAsChildren(targetBlock.en, lastExistingChild)
	}

	DEBUG.force('[generatePasteApplogs] Result', {
		logs,
		newFocusID,
		isEmpty,
		isFocused,
		isSingleRoot,
		hasChildren,
	})

	return { logs, newFocusID }
}

/**
 * Determines the most relevant data type from clipboard and processes it accordingly
 * @param {DataTransfer} clipboard - The clipboard data transfer object
 * @returns {Promise<object>} Processed data in various formats depending on clipboard content
 */
export const getRelevancefromClipboard = (
	clipboard: DataTransfer,
	thread,
	blockVM,
	relationVM,
	locnav,
	focus: () => EntityID | null,
) => {
	let asParsedXML, uriString, parsedHTML, imageInfo
	const plainText = clipboard.getData('text/plain')
	VERBOSE.force({ plainText })
	try {
		uriString = getURIfromClipboard(clipboard)
		if (uriString) {
			return { uriString }
		}
		// Try parsing as markdown bullet list
		// TODO: Promise false positive issue - if parseMarkdownStructure returns null after
		// we've returned { markdownResultPromise }, TipTap was told not to handle the paste
		// but we didn't actually paste anything. Content is silently dropped.
		if (plainText) {
			if (isLikelyMarkdown(plainText)) {
				const isFocused = focus() === blockVM.en

				const markdownResultPromise = parseMarkdownStructure(plainText).then(
					(parsed) => {
						if (!parsed) return false
						DEBUG.force('[markdown paste]', { parsed })

						const { logs, newFocusID } = generatePasteApplogs({
							parsed,
							targetBlock: blockVM,
							targetRelation: relationVM,
							isFocused,
						})

						insertApplogs(thread, logs)

						// Navigate to new focus if needed
						if (newFocusID && focus() === blockVM.en) {
							focusViewOnBlock({ id: newFocusID, inputFocus: true }, locnav)
						}

						DEBUG('added', logs)
						return true // true indicates we handled the paste so prosemirror/tiptap won't try
					},
				)
				return { markdownResultPromise }
			} else {
				VERBOSE.force('likely not markdown:', plainText)
			}
		}
		imageInfo = getImagefromClipboard(clipboard)
		if (imageInfo) {
			return imageInfo // { imgBlob, imgURL, imgType }
		}

		VERBOSE.force({ plainText, imageInfo, uriString })
		//   if (!item.types.includes("image/png")) {
		// 	throw new Error("Clipboard contains non-image data.");
		//   }
		//   const blob = await item.getType("image/png");
		//   destinationImage.src = URL.createObjectURL(blob);
		for (const type of clipboard.types) {
			const data = clipboard.getData(type)
			DEBUG('clip', type, { data })
			const options = {
				ignoreAttributes: false,
				attributeNamePrefix: '',
				allowBooleanAttributes: true,
			}
			const parser = new XMLParser(options)
			asParsedXML = parser.parse(data)
			DEBUG({ asParsedXML })
		}
	} catch (error) {
		ERROR('Clip parse fail', (error as any).message || error)
	}
	const rootOutlineOrOutlineArray = asParsedXML?.opml?.body?.outline
	/**
	 * Interface representing an OPML outline node
	 */
	interface OutlineNode {
		text: string
		tiptap?: string
		_note?: string
		outline?: OutlineNode
	}
	if (rootOutlineOrOutlineArray) {
		const getContentWithNote = (outlineNode: OutlineNode) => {
			let parsed
			if (outlineNode.tiptap) {
				parsed = JSON.parse(outlineNode.tiptap)
			} else {
				const content = `${outlineNode.text}${outlineNode._note ? `\n${outlineNode._note}` : ''}` // TODO: format
				// generateJSON will translate <b><u>Woven - 0.1</u></b> to tiptap marks for underline and bold #1
				parsed = htmlToTiptap(content)
			}
			DEBUG({ contentWithMaybeHTML: parsed })
			return serializeTiptapToVl(parsed)
		}

		const addApplogsForNode = (logArray: ApplogForInsertOptionalAgent[], outlineNode: OutlineNode | OutlineNode[], parentEn: EntityID) => {
			if (!outlineNode[0]) {
				outlineNode = [outlineNode as OutlineNode]
			}
			let after = null
			for (const eachNode of outlineNode as OutlineNode[]) {
				const eachBuilder = BlockVM.buildNew({ content: getContentWithNote(eachNode) })
				logArray.push(...eachBuilder.build())
				logArray.push(...(RelationVM.buildNew({ childOf: parentEn, block: eachBuilder.en, after })).build())
				after = eachBuilder.en
				if (eachNode.outline) {
					addApplogsForNode(logArray, eachNode.outline, eachBuilder.en)
				}
			}

			return logArray
		}

		const rootBuilder = BlockVM.buildNew({
			content: Array.isArray(rootOutlineOrOutlineArray)
				? serializeTiptapToVl(htmlToTiptap('<em>Pasted blocks</em>')) // HACK: creates a dummy root when pasting array of roots //TODO: use head>title if given
				: getContentWithNote(rootOutlineOrOutlineArray),
		})
		const logsToInsert = rootBuilder.build()
		LOG('beforeLoop', { rootOutline: rootOutlineOrOutlineArray, logsToInsert })
		if (rootOutlineOrOutlineArray.outline) {
			addApplogsForNode(logsToInsert, rootOutlineOrOutlineArray.outline, rootBuilder.en)
		} else if (rootOutlineOrOutlineArray.length) {
			rootOutlineOrOutlineArray.forEach(node => addApplogsForNode(logsToInsert, node.outline, rootBuilder.en))
		}
		LOG('afterLoop', { logsToInsert })
		if (!logsToInsert.length) WARN(`XML parse but no applogs`, { rootOutline: rootOutlineOrOutlineArray })
		return logsToInsert?.length ? { logsToInsert, rootID: rootBuilder.en } : null
	} else {
		parsedHTML = generateJSON(plainText, baseExtensions)
		return { parsedHTML }
	}

	/* else {
		const content = clipboard.getData('text')
		// TODO deal with awkward html via: await getHTMLFromClipboard()
		// eg with escaped html within styled divs (thanks vscode)
		// copy this: <b><u>Woven - 0.1</u></b>
		// paste this bogus html:
		// const _bogusHTML = `<div style="color: #c8d1df;background-color: #0e131b;font-family: 'JetBrainsMono Nerd Font'">
		// <div><span style="color: #b38098;">
		// 	&lt;b&gt;&lt;u&gt;Woven - 0.1&lt;/u&gt;&lt;/b&gt;
		// </span></div></div>`
		parsedHTML = generateJSON(content, baseExtensions)
		return { parsedHTML }
	} */

	return {}
}

/**
 * Creates a paste event handler for tree items in the editor
 * @param {RelationVM | null} relationVM - The relation view model (null for root blocks)
 * @param {BlockVM} blockVM - The block view model (must exist when pasting)
 * @returns {EditorProps['handlePaste']} A function to handle paste events
 */
export function createTreeItemPasteHandler(
	relationVM: RelationVM | null,
	blockVM: BlockVM,
): EditorProps['handlePaste'] {
	const { setPanel } = useBlockContext()
	const thread = useThreadFromContext()
	const focus = useFocus()
	const { locnav } = useLocationNavigate()

	/**
	 * Handles paste events in the editor
	 * @param {EditorView} view - The editor view
	 * @param {ClipboardEvent} event - The clipboard event
	 * @param {Slice} slice - The content slice
	 * @returns {Promise<boolean>} True if the paste was handled, false otherwise
	 */
	return function handlePasteEvent(view: EditorView, event: ClipboardEvent, slice: Slice) {
		const cbData = event.clipboardData
		const asText = cbData.getData('text/plain')
		const cbItems = event.clipboardData.items
		DEBUG.force('[pasteHandler] onPaste', { asText, cbData, cbItems })
		const relevantClipboardInfo = getRelevancefromClipboard(event.clipboardData, thread, blockVM, relationVM, locnav, focus)
		const { logsToInsert, rootID, uriString, markdownResultPromise /* , parsedHTML */ } = relevantClipboardInfo

		const { schema } = view.state
		DEBUG.force('[pasteHandler] with relevantClipboardInfo', {
			relevantClipboardInfo,
			schema,
			view,
			event,
			slice,
			relation: relationVM,
		})
		if (markdownResultPromise) {
			DEBUG.force('markdownDetected should be handled async so we tell tiptap we got this')
			return true
		}
		if (uriString) {
			if (!blockVM.isEmpty) {
				DEBUG(`[handlePaste] block is not empty`, { blockVM })
				return false // handled by tiptap
			}
			const url = tryParseNote3URL(uriString)
			if (!url) {
				VERBOSE('Pasted is not a note3 URL:', uriString)
				return false
			}
			DEBUG(`[pasteHandler] found note3 url:`, url)

			if (!url.focus) {
				console.error('TODO: Not sure what to do without root:', url)
				return false
			}

			if (url.publication && !getSubOrShare(url.publication)) {
				setPanel({ type: 'paste', publication: url.publication, block: url.focus })
			}
			// otherwise, we have everything we need
			void deleteAndReplaceBlock(relationVM.en, url.focus, true)
			// also add a rel/mirror prop
			// relationVM.isMirror = true

			return true
		} else {
			const { imgURL, imgType, imgBlob /* , parsedHTML */ } = relevantClipboardInfo
			if (imgBlob) {
				const currentPos = view.state.selection?.anchor || 0
				const reader = new FileReader()
				let isPasted = false // HACK to avoid infinite editing pasting loop
				reader.onloadend = async (ev) => {
					if (ev.lengthComputable && ev.loaded === ev.total && !isPasted) {
						const imageSrcB64 = reader.result // `<img src="${reader.result}" />`
						// https://github.com/ueberdosis/tiptap/blob/main/packages/extension-image/src/image.ts#L47
						const node = schema.nodes.image.create({
							src: imageSrcB64,
							alt: null,
							title: null,
						}) /* TODO ai to describe the image and set alt tag and title (and/or editable ui) */
						reader.readAsDataURL(imgBlob)
						DEBUG('handlePaste', { ev, node, imgType, imageSrcB64, imgURL })
						isPasted = true
						const transaction = view.state.tr.insert(currentPos, node)
						view.dispatch(transaction)
					}
				}
				reader.readAsDataURL(imgBlob)
				return true
			}
		}
		// } else if (parsedHTML) {
		// 	blockVM.content = parsedHTML
		// 	// HACK this works but throws:
		// 	// Did you intend to update an _observable_ (setting _content directly also does not work)
		// 	// value, instead of the computed property?

		// 	// didn't work:
		// 	// blockVM.markNotEditing()
		// 	// await blockVM.persistContent(parsedHTML)
		// 	// focusBlockAsInput({ id: blockVM.en, end: true })
		// 	return true

		DEBUG.force('[pasteHandler] not handled, passing back to tiptap')
		return false
	}
}

/**
 * Copies text to the clipboard
 * @param {string} text - The text to copy to the clipboard
 * @returns {Promise<void>}
 */
export async function copyToClipboard(text) {
	try {
		await navigator.clipboard.writeText(text)
		/* ✅ Copied successfully */
	} catch (e) {
		DEBUG(`Failed to copy`, e)
		/* ❌ Failed to copy (insufficient permissions) */
	}
}

const workFlowyExampleHTML = `
<ul style="-webkit-tap-highlight-color: transparent; margin: 15px 0px 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent; list-style: disc; color: rgb(51, 51, 51); font-family: &quot;Helvetica Neue&quot;, Arial, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">
<li style="-webkit-tap-highlight-color: transparent; margin: 4px 0px 4px 20px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent;">
<span class="name" data-wfid="f58371f148c7:3fbecf7eeeea" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent; white-space: pre-wrap;">
<span class="innerContentContainer" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent;">Setup your agent <span class="contentTag" title="Filter #UX" data-val="#ux" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background-image: initial; background-position: initial; background-size: initial; background-repeat: initial; background-attachment: initial; background-origin: initial; background-clip: initial; background-color: rgb(119, 59, 59) !important; color: rgb(255, 255, 255) !important;">#<span class="contentTagText" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: none; text-decoration: none;">UX</span>
<span class="contentTagNub" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: none;">
</span>
</span> </span>
</span>
<br style="-webkit-tap-highlight-color: transparent;">
<span class="note" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 12px; vertical-align: baseline; background: transparent; white-space: pre-wrap; color: rgb(102, 102, 102);">
<span class="innerContentContainer" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 12px; vertical-align: baseline; background: transparent;">
<span class="contentTag" title="Filter #NEEDS-DESIGNER" data-val="#needs-designer" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 12px; vertical-align: baseline; background-image: initial; background-position: initial; background-size: initial; background-repeat: initial; background-attachment: initial; background-origin: initial; background-clip: initial; background-color: rgb(64, 128, 107) !important; color: rgb(255, 255, 255) !important;">
<span class="colored bc-green" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 2px 0px; border: 0px; outline: 0px; font-size: 12px; vertical-align: baseline; background: none; color: rgb(255, 255, 255);">#</span>
<span class="contentTagText" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 12px; vertical-align: baseline; background: none; text-decoration: none;">
<span class="colored bc-green" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 2px 0px; border: 0px; outline: 0px; font-size: 12px; vertical-align: baseline; background: none; color: rgb(255, 255, 255);">
</span>NEEDS-DESIGNER</span>
<span class="contentTagNub" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 12px; vertical-align: baseline; background: none;">
</span>
</span>
</span>
</span>
<ul style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent; list-style: disc;">
<li style="-webkit-tap-highlight-color: transparent; margin: 4px 0px 4px 20px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent;">
<span class="name" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent; white-space: pre-wrap;">
<span class="innerContentContainer" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent;">Login to web3</span>
</span>
</li>
<li style="-webkit-tap-highlight-color: transparent; margin: 4px 0px 4px 20px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent;">
<span class="name" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent; white-space: pre-wrap;">
<span class="innerContentContainer" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent;">Create encryption keys <span class="contentTag" title="Filter #UX" data-val="#ux" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background-image: initial; background-position: initial; background-size: initial; background-repeat: initial; background-attachment: initial; background-origin: initial; background-clip: initial; background-color: rgb(119, 59, 59) !important; color: rgb(255, 255, 255) !important;">#<span class="contentTagText" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: none; text-decoration: none;">UX</span>
<span class="contentTagNub" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: none;">import { stringify } from 'safe-stable-stringify';

</span>
</span> </span>
</span>
</li>
<li style="-webkit-tap-highlight-color: transparent; margin: 4px 0px 4px 20px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent;">
<span class="name" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent; white-space: pre-wrap;">
<span class="innerContentContainer" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent;">Push public hello world thread </span>
</span>
</li>
<li style="-webkit-tap-highlight-color: transparent; margin: 4px 0px 4px 20px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent;">
<span class="name" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent; white-space: pre-wrap;">
<span class="innerContentContainer" style="-webkit-tap-highlight-color: transparent; margin: 0px; padding: 0px; border: 0px; outline: 0px; font-size: 13px; vertical-align: baseline; background: transparent;">Send us your link with thread ID </span>
</span>
</li>
</ul>
</li>
</ul>
`
