import type { EntityID } from '@wovin/core/applog'
import type { ThreadOnlyCurrentNoDeleted } from '@wovin/core/thread'
import { action } from '@wovin/core/mobx'
import { Logger } from 'besonders-logger'
import { useContext } from 'solid-js'
import { BlockContext } from '../components/BlockTree'
import { htmlToTiptap, markdownExtensions } from '../components/TipTapExtentions'
import { insertBlockInRelChain, removeBlockFromRelChain } 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)
}

export const handleBlockDrag = action(function handleBlockDrag(
	thread: ThreadOnlyCurrentNoDeleted,
	blockID: EntityID,
	sourceRelationID: EntityID | null,
	newParentID: EntityID,
	newAfterID: EntityID | null,
) {
	DEBUG(`[handleBlockDrag]`, { blockID, sourceRelationID, newParentID, newAfterID, thread })

	if (sourceRelationID) {
		removeBlockFromRelChain(thread, blockID, sourceRelationID)
	}
	insertBlockInRelChain(thread, blockID, newParentID, newAfterID, sourceRelationID)
})

export async function getClipboardPerms() {
	const permission = await navigator.permissions.query({
		name: 'clipboard-read',
	})
	if (permission.state === 'denied') {
		throw new Error('Not allowed to read clipboard.')
	}
}
export async function getClipboardContents() {
	await getClipboardPerms()
	const clipboardContents = await navigator.clipboard.read()
	return clipboardContents
}
export async function getValidURLFromClipboard() {
	const clipboardContents = await getClipboardContents()
	const maybeValidURL = await (await clipboardContents[0]?.getType('text/plain'))?.text()
	return isValidURL(maybeValidURL) ? maybeValidURL : null
}
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
}
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
}
export function isValidURL(url) {
	try {
		const _urlObj = new URL(url)
		return _urlObj.toString()
	} catch (e) {
		return false
	}
}

export function getURIfromClipboard(clipboard: DataTransfer) {
	const plainText = clipboard.getData('text')
	const uriString = isValidURL(plainText) || null
	return uriString
}
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 TiptapNode {
	type: string
	content?: TiptapNode[]
	text?: string
}
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 ''
}
// Recursive function to process bulletList nodes
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
}
const exampleTreeChildren = [
	{
		type: 'heading',
		depth: 1,
		children: [
			{
				type: 'text',
				value: 'Chapter 1 – Re‑imagining Attachment for Polysecure Relating',
				position: {
					start: {
						line: 1,
						column: 3,
						offset: 2,
					},
					end: {
						line: 1,
						column: 62,
						offset: 61,
					},
				},
			},
		],
		position: {
			start: {
				line: 1,
				column: 1,
				offset: 0,
			},
			end: {
				line: 1,
				column: 62,
				offset: 61,
			},
		},
	},
	{
		type: 'code',
		lang: null,
		meta: null,
		value: '- Consensual nonmonogamy can be a fertile context for deep security, where each attachment pattern is understood as a wise strategy that once kept love safe and can now be gently updated to allow more ease, trust, and mutual nourishment. [web:21][web:22]\n- This chapter invites the reader to see attachment not as fixed labels that limit love, but as living patterns that can soften and transform in relationships rooted in honesty, consent, and care. [web:21][web:22]\n- In multi‑partner love, people can experience security with more than one person, and all attachment patterns have strengths that can be welcomed and supported rather than shamed. [web:21][web:22]',
		position: {
			start: {
				line: 3,
				column: 1,
				offset: 63,
			},
			end: {
				line: 5,
				column: 202,
				offset: 741,
			},
		},
	},
	{
		type: 'heading',
		depth: 2,
		children: [
			{
				type: 'text',
				value: 'Secure relating as a shared field',
				position: {
					start: {
						line: 7,
						column: 4,
						offset: 746,
					},
					end: {
						line: 7,
						column: 37,
						offset: 779,
					},
				},
			},
		],
		position: {
			start: {
				line: 7,
				column: 1,
				offset: 743,
			},
			end: {
				line: 7,
				column: 37,
				offset: 779,
			},
		},
	},
	{
		type: 'code',
		lang: null,
		meta: null,
		value: '    - Security is presented as something co‑created: a felt sense that “love can move freely here,” supported by reliability, emotional responsiveness, and clear agreements between all partners. [web:21][web:22]\n    - “Secure” is reframed as a relational climate everyone helps to build through consistency, empathy, and honest communication about needs and boundaries. [web:21][web:22]\n    - In nonmonogamy, security grows when partners are transparent, follow through on commitments, and remain curious about each other’s inner worlds, especially when jealousy, fear, or uncertainty arise. [web:21][web:29]',
		position: {
			start: {
				line: 9,
				column: 1,
				offset: 785,
			},
			end: {
				line: 11,
				column: 226,
				offset: 1405,
			},
		},
	},
]
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]
			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'
							&& candidateHeading.attrs?.level
							&& 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
}
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 {
				eachNode.txt = tiptapToPlaintext(eachNode.content?.[0])
				eachNode.children = eachNode.children ?? []
				eachNode.children.push(...newkidsforheading)
				newkidsforheading = []
				nestedByHeadings.unshift(eachNode)
			}
		}
	} else {
		nestedByHeadings = [...markdownChildren]
	}
	return nestedByHeadings
}
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
}
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)
}
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
}
export const parsePastedMarkdown = async (markdownText: string): Promise<{ logsToInsert: ApplogForInsertOptionalAgent[], rootID: EntityID }> => {
	if (!isMarkdown(markdownText)) return null

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

	// const tree = fromMarkdown(value, {
	// 	extensions: [gfm()],
	// 	mdastExtensions: [gfmFromMarkdown()],
	// })
	// const result = toMarkdown(tree, { extensions: [gfmToMarkdown()] }) // test reversibility

	// Parse markdown to Tiptap JSON using markdownExtensions (which includes bulletList support)
	const tiptapJson = generateJSON(htmlFromMarkdown, markdownExtensions) as TiptapNode
	const maybeNestedUnderHeadings = reorganizeTiptapNodesByTypeAndLevel(tiptapJson.content ?? [])
	DEBUG.force({ htmlFromMarkdown, maybeNestedUnderHeadings, tiptapJson })

	if (!tiptapJson.content?.length) return null
	const logsToInsert: ApplogForInsertOptionalAgent[] = []
	const rootBuilder = BlockVM.buildNew({ content: serializeTiptapToVl(htmlToTiptap('<em>Pasted md</em>')) })
	logsToInsert.push(...rootBuilder.build())
	const rootID = rootBuilder.en
	let lastTopLevel: EntityID | null = null
	let currentParent: EntityID = rootID
	let lastSiblingUnderCurrent: EntityID | null = null
	const recurseChildren = (maybeNested: any[], parentID: EntityID) => {
		for (const eachNode of maybeNested || []) {
			let thisID
			if (['heading', 'paragraph'].includes(eachNode.type as string)) {
				const content = getBlockContent(eachNode)
				VERBOSE({ content })
				if (!content) continue
				const builder = BlockVM.buildNew({ content })
				logsToInsert.push(...builder.build())
				thisID = builder.en
				logsToInsert.push(...RelationVM.buildNew({ childOf: parentID, block: thisID, after: lastTopLevel }).build())
				lastTopLevel = thisID
				currentParent = thisID
				lastSiblingUnderCurrent = null
			} else if (eachNode.type === 'bulletList') {
				lastSiblingUnderCurrent = processBulletList(eachNode, currentParent, logsToInsert, lastSiblingUnderCurrent)
			}
			if (eachNode.children?.length) recurseChildren(eachNode.children, thisID ?? parentID)
		}
	}
	recurseChildren(maybeNestedUnderHeadings, rootID)
	if (logsToInsert.length === 0) return null

	DEBUG.force('[parseMarkdownBulletList] Result', { logsToInsert, rootID })
	return { logsToInsert, rootID }
}

export const getRelevancefromClipboard = async (clipboard: DataTransfer) => {
	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
		if (plainText) {
			const markdownResult = await parsePastedMarkdown(plainText)
			if (markdownResult) {
				return markdownResult
			}
			// If not markdown, fall through to let TipTap handle plain text insertion
		}
		imageInfo = getImagefromClipboard(clipboard)
		if (imageInfo) {
			return imageInfo
		}

		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 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 {
	}

	/* 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 {}
}

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) */
	}
}

export function getCursorPositionInContentEditable(element) {
	let range
	let preCaretRange
	let textContent
	const caretOffset = 0

	if (window.getSelection) {
		range = window?.getSelection()?.getRangeAt(0)
		if (!range) {
			return undefined
		}
		preCaretRange = range.cloneRange()
		preCaretRange.selectNodeContents(element)
	}

	return caretOffset
}
