import type { Editor } from '@tiptap/core'
import type { DOMOutputSpec, Node as ProseMirrorNode } from '@tiptap/pm/model'
import type { SuggestionOptions } from '@tiptap/suggestion'
import type { Thread } from '@wovin/core'
import type { Accessor, Setter } from 'solid-js'
import type { TiptapContent } from '../data/VMs/TypeMap'

import { Extension, generateJSON, mergeAttributes, Node } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Decoration, DecorationSet } from '@tiptap/pm/view'

import StarterKit from '@tiptap/starter-kit'
import Suggestion from '@tiptap/suggestion'
import { EntityID_LENGTH, getHashID, query } from '@wovin/core'
import { Logger } from 'besonders-logger'
import { groupBy, last } from 'lodash-es'
import stringify from 'safe-stable-stringify'
import { Markdown } from 'tiptap-markdown'
import { useAgent } from '../data/agent/AgentState'
import {
	getRegexForTagInContext,
	serializeTiptapToVl,
} from '../data/block-utils-nowin'
import {
	RE_AT_TAG_WITHCONTEXT,
	RE_HASH_TAG_WITHCONTEXT,
	RE_PLUS_TAG_WITHCONTEXT,
} from '../data/note3-regex-constants'
import { useRawThread } from '../ui/reactive'

const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars

export const baseExtensions = [
	StarterKit.configure({
		// hardBreak: false,

		// these don't make a lot of sense with it being already in a block tree
		listItem: false,
		orderedList: false,
		bulletList: false,
		// paragraph: {
		// 	HTMLAttributes: {
		// 		// class: 'min-h-[1rem]'
		// 	},
		// }),

		// Configure Link (included in StarterKit v3)
		link: {
			// Docs: https://tiptap.dev/docs/editor/api/marks/link#settings
			// validate: href => /^httpsXXR?:\/\//.test(href),
			protocols: ['note3', 'mailto', 'ftp'],
			openOnClick: false,
			autolink: true,
			linkOnPaste: true,
			HTMLAttributes: {
				class: 'link-note3',
			},
		},

		// Underline is also included in StarterKit v3 with default config
	}),
	// Note: Underline and Link are now included in StarterKit v3, no need to add separately
	// HardBreak,
	// HardBreak.extend({
	// 	addKeyboardShortcuts() {
	// 		return {
	// 			Enter: () => this.editor.commands.setHardBreak(),
	// 		}
	// 	},
	// }),
]

// Separate extensions for markdown parsing (includes bulletList/listItem)
export const markdownExtensions = [
	StarterKit.configure({
		// Configure Link with custom settings (included in StarterKit v3)
		link: {
			protocols: ['note3', 'mailto', 'ftp'],
			openOnClick: false,
			autolink: true,
			linkOnPaste: true,
			HTMLAttributes: {
				class: 'link-note3',
			},
		},
		// Underline is included in StarterKit v3 with default config
	}),
	Markdown.configure({
		bulletListMarker: '-',
		tightLists: true,
	}),
]

export function htmlToTiptap(html: string): TiptapContent {
	return generateJSON(html, baseExtensions)
}
export function htmlToSerializedTiptap(html: string): string {
	return serializeTiptapToVl(htmlToTiptap(html))
}

// type PasteHandler = (view: Editor['view'], event: ClipboardEvent, slice: Slice) => boolean | void
// type TipTapPasteHandler = (props: {
// 	state: Editor['state']
// 	range: Range
// 	match: ExtendedRegExpMatchArray
// 	commands: SingleCommands
// 	chain: () => ChainedCommands
// 	can: () => CanCommands
// }) => boolean

// export const CustomStackedPasteHandler = function(orderedHandlers: PasteHandler[]) {
// 	DEBUG('creating CustomStackedPasteHandler', { orderedHandlers })
// 	return function CustomStackedPasteHandler(view, event, slice) {
// 		let isHandled = false
// 		for (const eachHandler of orderedHandlers) {
// 			VERBOSE('stackedPaste calling', { eachHandler, with: { view, event, slice } })
// 			isHandled = !!(await eachHandler(view, event, slice))
// 		}
// 		return isHandled
// 	}
// }
export const SelectionDetectLinkHandler = function SelectionDetectLinkHandler(
	selectionLink: Accessor<{ href: string } | null>,
	setSelectionLink: Setter<{ href: string } | null>,
) {
	return Extension.create({
		name: 'SelectionDetectLinkHandler',
		// @ts-expect-error broken types upstream
		onSelectionUpdate({ editor }) {
			const { state: _state, state: { selection } } = editor
			const href = editor.getAttributes('link')?.href
			if (!selection.empty && !!href) {
				setSelectionLink({ href }) && DEBUG('setting selection', selection, { editor, href })
			} else if (selectionLink()) {
				setSelectionLink(null) || VERBOSE('unsetting isSelectionLinked')
			}
		},
	})
}

const HASH_DEFAULT_COLOR = '#4a4a4a' //  '#bae6fd'
const PLUS_DEFAULT_COLOR = '#a7ead0'
const AT_DEFAULT_COLOR = '#beb8db' // <= less saturated <= '#b4a9ea'
export const tagDefaultColorMap = {
	'#': HASH_DEFAULT_COLOR,
	'+': PLUS_DEFAULT_COLOR,
	'@': AT_DEFAULT_COLOR,
}

export function getContrastHighGreyForBg(color) {
	if (!color || color === undefined) {
		return null
	}

	// Removing # from the given hex color code
	const rgb = color.replace('#', '')

	// Separate the colors
	const red = Number.parseInt(rgb.substring(0, 2), 16)
	const green = Number.parseInt(rgb.substring(2, 4), 16)
	const blue = Number.parseInt(rgb.substring(4, 6), 16)

	// Calculate brightness value
	const brightness = Math.round(((red * 299) + (green * 587) + (blue * 114)) / 1000)

	// Return light or dark grey based on the brightness
	return brightness > 125 ? '#222' : '#ddd'
}

export function getStyleRulesForTag(ds: Thread, tagStringWithPrefix: string) {
	const tagID = getHashID(tagStringWithPrefix, EntityID_LENGTH)
	const prefix = tagID.slice(0, 1)
	const defaultTextCol = tagDefaultColorMap[prefix]
	const { text, bg } = getAllStyleAttr(ds, tagID)

	const bgRule = bg ? ` padding: 0.15em 0.25em 0.25em 0.25em; border-radius: 0.125rem; background-color: ${bg}; ` : ''
	// TODO set
	const textColor = text || (bg ? getContrastHighGreyForBg(bg) : defaultTextCol)
	const colorFromLogsRule = ` color: ${textColor}; `
	return colorFromLogsRule + bgRule
}
function getAllStyleAttr(ds: Thread, tagID: string) {
	const styleEntityQuery = query(ds, [{ en: tagID, at: att => att.startsWith('style/') }])
	const styleLogs = styleEntityQuery.isEmpty ? [] : styleEntityQuery.threadOfAllTrails.applogs
	const { 'style/text-color': textLogs = [], 'style/bg-color': bgLogs = [] } = groupBy(styleLogs, 'at')
	const text = textLogs.findLast(({ ag }) => ag === useAgent().ag)?.vl ?? last(textLogs)?.vl
	const bg = bgLogs.findLast(({ ag }) => ag === useAgent().ag)?.vl ?? last(bgLogs)?.vl
	return { bg, text }
}

type WordInfoT = { word: string, st: number, ed: number }[]
function getWordInfo({ editor }: { editor: Editor }) {
	const text = editor.getText()
	const textLength = text.length
	const currentPos = editor.state.selection.anchor - 1

	const words = getWordMap(text)

	const currentWordInfo = words.find(eachWord => currentPos >= eachWord.st && currentPos <= eachWord.ed) || { word: '', st: 0, ed: 0 }
	const currentWordSpan: { from: number, to: number } = { from: currentWordInfo.st + 1, to: currentWordInfo.ed + 1 }
	const returnObject = { words, currentPos, currentWordInfo, currentWordSpan }
	VERBOSE({ returnObject, editor, focused: editor.isFocused, text, textLength })
	return returnObject
}
// https://tiptap.dev/docs/editor/guide/typescript#command-type
declare module '@tiptap/core' {
	interface Commands {
		wordInfo: {
			/**
			 * returns { words, currentPos, currentWordInfo, currentWordSpan } for the Editor
			 */
			getWordInfo: () => ReturnType<typeof getWordInfo>
		}
	}
}

function getWordMap(text) {
	let nextStart = 0
	return text.split(' ').map((eachWord) => {
		const retObj = {
			word: eachWord,
			st: nextStart,
			ed: eachWord.length + nextStart,
		}
		nextStart = retObj.ed + 1 // include the space
		return retObj
	}) as { word: string, st: number, ed: number }[]
}
export const WordInfoExtension = Extension.create({
	name: 'wordInfo',
	// @ts-expect-error custom command return signature
	addCommands() {
		return {
			getWordInfo: () => getWordInfo,
		}
	},
})

export const TagHighlightExtension = Extension.create({
	name: 'tagHighlight',

	addProseMirrorPlugins() {
		const rawDS = useRawThread()
		return [
			new Plugin({
				props: {
					decorations(state) {
						const decorations = []

						state.doc.descendants((node, pos) => {
							if (!node.isText) return

							for (
								const [symbol, regex] of [
									['#', new RegExp(RE_HASH_TAG_WITHCONTEXT.source, 'g')] as const, // (i) global regex needs to be recreated https://stackoverflow.com/questions/52163499/regex-not-working-if-the-regex-string-is-stored-in-a-separate-javascript-file/52163813#52163813
									['+', new RegExp(RE_PLUS_TAG_WITHCONTEXT.source, 'g')] as const,
									['@', new RegExp(RE_AT_TAG_WITHCONTEXT.source, 'g')] as const,
								]
							) {
								for (const match of node.text.matchAll(regex)) {
									const tagWithSymbol = symbol + match[2]
									// if (tagWithSymbol === '+todo') continue // HACK don't overlap with todo extension
									const tagID = getHashID(tagWithSymbol, EntityID_LENGTH)
									const { text, bg } = getAllStyleAttr(rawDS, tagID)
									const textColor = text || (bg ? getContrastHighGreyForBg(bg) : tagDefaultColorMap[symbol])
									const colorFromLogsRule = `color: ${textColor}; `
									const bgRule = bg ? `padding: 0.15em 0.25em 0.25em 0.25em; background-color: ${bg}; ` : ''
									DEBUG({ tagID, match, text, textToUse: textColor, bg })
									decorations.push(
										Decoration.inline(
											pos + match.index + match[1].length, // start + whitespace (to start after whitespace)
											pos + match.index + match[1].length + 1 + match[2].length, // + symbol + tag length
											{
												class: 'tag-in-block tag-col-invert rounded-sm', // cursor-pointer // border-1 border-solid rounded-sm p-0.5 m-0.5 border-1 border-solid
												style: ` text-underline-offset: 0.2em; text-decoration-thickness: 1px; ${
													colorFromLogsRule
												}${bgRule}`,
												// onClick: (e) => { //TODO: onclick inline tags... not quite: https://discuss.prosemirror.net/t/how-to-handle-events-inside-decorations/1083/4
												// 	if (e.button === 1 && e.ctrlKey) setSearch(tagWithSymbol)
												// },
											},
										),
									)
								}
							}
						})

						if (!decorations.length) return
						return DecorationSet.create(state.doc, decorations)
					},
				},
			}),
		]
	},
})
export function CreateTokenHidingExtension(tokenArray: readonly string[]) {
	return Extension.create({
		name: `tagHide-${stringify(tokenArray)}`,

		addProseMirrorPlugins() {
			const rawDS = useRawThread()
			return [
				new Plugin({
					props: {
						decorations(state) {
							const decorations = []

							state.doc.descendants((node, pos) => {
								if (!node.isText) return

								for (
									const regex of tokenArray.map(eachToken => getRegexForTagInContext(eachToken, 'g'))
									// [
									// 	['#', new RegExp(RE_HASH_TAG_WITHCONTEXT.source, 'g')] as const, // (i) global regex needs to be recreated https://stackoverflow.com/questions/52163499/regex-not-working-if-the-regex-string-is-stored-in-a-separate-javascript-file/52163813#52163813
									// 	['+', new RegExp(RE_PLUS_TAG_WITHCONTEXT.source, 'g')] as const,
									// 	['@', new RegExp(RE_AT_TAG_WITHCONTEXT.source, 'g')] as const,
									// ]
								) {
									VERBOSE({ text: node.text, regex })
									for (const match of node.text.matchAll(regex)) {
										// const tagWithSymbol = symbol + match[2]
										// const tagID = getHashID(tagWithSymbol, EntityID_LENGTH)
										// const { text, bg } = getAllStyleAttr(rawDS, tagID)
										// const textColor = text || (bg ? getContrastHighGreyForBg(bg) : tagDefaultColorMap[symbol])
										// const colorFromLogsRule = `color: ${textColor}; `
										// const bgRule = bg ? `padding: 0.15em 0.25em 0.25em 0.25em; background-color: ${bg}; ` : ''
										DEBUG({ regex, match })
										decorations.push(
											Decoration.inline(
												pos + match.index + match[1].length, // start + whitespace (to start after whitespace)
												pos + match.index + match[1].length + 1 + match[2].length, // + symbol + tag length
												{
													class: 'hidden', // cursor-pointer // border-1 border-solid rounded-sm p-0.5 m-0.5 border-1 border-solid
													// style: 'text-underline-offset: 0.2em; text-decoration-thickness: 1px; ' + colorFromLogsRule + bgRule,
													// onClick: (e) => { //TODO: onclick inline tags... not quite: https://discuss.prosemirror.net/t/how-to-handle-events-inside-decorations/1083/4
													// 	if (e.button === 1 && e.ctrlKey) setSearch(tagWithSymbol)
													// },
												},
											),
										)
									}
								}
							})

							return DecorationSet.create(state.doc, decorations)
						},
					},
				}),
			]
		},
	})
}

// wip  AutoComplete adapted from Mention  https://raw.githubusercontent.com/ueberdosis/tiptap/main/packages/extension-mention/src/mention.ts
export interface AutoCompleteOptions {
	HTMLAttributes: Record<string, any>
	/** @deprecated use renderText and renderHTML instead  */
	renderLabel?: (props: { options: AutoCompleteOptions, node: ProseMirrorNode }) => string
	renderText: (props: { options: AutoCompleteOptions, node: ProseMirrorNode }) => string
	renderHTML: (props: { options: AutoCompleteOptions, node: ProseMirrorNode }) => DOMOutputSpec
	deleteTriggerWithBackspace: boolean
	suggestion: Omit<SuggestionOptions, 'editor'>
}

export const AutoCompletePluginKey = new PluginKey('autocomplete')

export const AutoComplete = Node.create<AutoCompleteOptions>({
	name: 'autocomplete',

	addOptions() {
		return {
			HTMLAttributes: {},
			renderText({ options, node }) {
				return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
			},
			deleteTriggerWithBackspace: false,
			renderHTML({ options, node }) {
				return [
					'span',
					mergeAttributes(this.HTMLAttributes, options.HTMLAttributes),
					`${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`,
				]
			},
			suggestion: {
				char: '@',
				pluginKey: AutoCompletePluginKey,
				command: ({ editor, range, props }) => {
					// increase range.to by one when the next node is of type "text"
					// and starts with a space character
					const nodeAfter = editor.view.state.selection.$to.nodeAfter
					const overrideSpace = nodeAfter?.text?.startsWith(' ')

					if (overrideSpace) {
						range.to += 1
					}

					editor
						.chain()
						.focus()
						.insertContentAt(range, [
							{
								type: this.name,
								attrs: props,
							},
							{
								type: 'text',
								text: ' ',
							},
						])
						.run()

					window.getSelection()?.collapseToEnd()
				},
				allow: ({ state, range }) => {
					const $from = state.doc.resolve(range.from)
					const type = state.schema.nodes[this.name]
					const allow = !!$from.parent.type.contentMatch.matchType(type)

					return allow
				},
			},
		}
	},

	group: 'inline',

	inline: true,

	selectable: false,

	atom: true,

	addAttributes() {
		return {
			id: {
				default: null,
				parseHTML: element => element.getAttribute('data-id'),
				renderHTML: (attributes) => {
					if (!attributes.id) {
						return {}
					}

					return {
						'data-id': attributes.id,
					}
				},
			},

			label: {
				default: null,
				parseHTML: element => element.getAttribute('data-label'),
				renderHTML: (attributes) => {
					if (!attributes.label) {
						return {}
					}

					return {
						'data-label': attributes.label,
					}
				},
			},
		}
	},

	parseHTML() {
		return [
			{
				tag: `span[data-type="${this.name}"]`,
			},
		]
	},

	renderHTML({ node, HTMLAttributes }) {
		if (this.options.renderLabel !== undefined) {
			console.warn('renderLabel is deprecated use renderText and renderHTML instead')
			return [
				'span',
				mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes),
				this.options.renderLabel({
					options: this.options,
					node,
				}),
			]
		}
		const mergedOptions = { ...this.options }

		mergedOptions.HTMLAttributes = mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes)
		const html = this.options.renderHTML({
			options: mergedOptions,
			node,
		})

		if (typeof html === 'string') {
			return [
				'span',
				mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes),
				html,
			]
		}
		return html
	},

	renderText({ node }) {
		if (this.options.renderLabel !== undefined) {
			console.warn('renderLabel is deprecated use renderText and renderHTML instead')
			return this.options.renderLabel({
				options: this.options,
				node,
			})
		}
		return this.options.renderText({
			options: this.options,
			node,
		})
	},

	addKeyboardShortcuts() {
		return {
			Backspace: () =>
				this.editor.commands.command(({ tr, state }) => {
					let isAutoComplete = false
					const { selection } = state
					const { empty, anchor } = selection

					if (!empty) {
						return false
					}

					state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
						if (node.type.name === this.name) {
							isAutoComplete = true
							tr.insertText(
								this.options.deleteTriggerWithBackspace ? '' : this.options.suggestion.char || '',
								pos,
								pos + node.nodeSize,
							)

							return false
						}
					})

					return isAutoComplete
				}),
		}
	},

	addProseMirrorPlugins() {
		return [
			Suggestion({
				editor: this.editor,
				...this.options.suggestion,
			}),
		]
	},
})
