import type { EntityID } from '@wovin/core/applog'
import type { ParentComponent } from 'solid-js'
import type { BlockPanelDef } from '../data/block-panels'
import type { SmartQuery } from '../data/smart-list'
import type { BlockVM } from '../data/VMs/BlockVM'
import type { FakeBlock } from './BlockTree'
import type { SortableBlockItem } from './SortableBlocks'
import { createLazyMemo } from '@solid-primitives/memo'
import { observableArrayMap } from '@wovin/core'
import { autorun, untracked } from '@wovin/core/mobx'
import { Logger } from 'besonders-logger'
import classNames from 'classnames'
import uniq from 'lodash-es/uniq'
import stringify from 'safe-stable-stringify'
import { createEffect, createMemo, createSignal, Match, mergeProps, on, Show, Switch, untrack } from 'solid-js'
import { BlockPanel, useDivergencePanel } from '../data/block-panels'
import { useBlockContext } from '../data/block-ui-helpers'
import { TAG_MIN_LENGTH } from '../data/note3-regex-constants'
import { plaintextStringToTiptap, tiptapToPlaintext } from '../data/note3-utils-nodeps'
import { useSearchContext } from '../data/search'
import { getSmartLists, parseSmartQuery, smartQueryIsError } from '../data/smart-list'
import { arrayReUseItemsIfEqual, findAllDeeplyNested } from '../data/utils-data'
import { useBlk } from '../data/VMs/BlockVM'
import { REL, useRel } from '../data/VMs/RelationVM'
import { useAppSettings } from '../ui/app-settings'
import { useCurrentThread, useEntityAt, useFocus, useIsTimeline, useKidRelations, useRawThread, useReadOnlyState, withDS } from '../ui/reactive'
import { BlockItem } from './BlockContent'
import { useBlockSetting } from './BlockSettings'
import { AUTOCOLLAPSE_DEPTH_DEFAULT, AUTOCOLLAPSE_DEPTH_FOCUSSED_DEFAULT, AUTOCOLLAPSE_DEPTH_TIMELINE_DEFAULT, AUTOCOLLAPSE_DEPTH_TIMELINE_SMARTLIST_DEFAULT, BlockContext, MAX_DEPTH } from './BlockTree'
import { KidsAsSurvey } from './KidsAsSurvey'
import { KidsAsTabs } from './KidsAsTabs'
import { SortableBlocks } from './SortableBlocks'

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

export const TreeItem: ParentComponent<{
	blockID?: EntityID
	fake?: FakeBlock
	kidBlockIDs?: readonly EntityID[]
	relationID?: EntityID
	parentRelationID?: EntityID // TODO: from context?
	class?: string
}> = (
	_props,
) => {
	const props = mergeProps({ relationID: null, parentRelationID: null }, _props) // ? why
	const debugName = untracked(() =>
		props.blockID ?? (props.fake
			? stringify({
					...props.fake,
					content: props.fake.content ? tiptapToPlaintext(props.fake.content) : undefined,
				})
			: null),
	) // ?? stringify(omit(props.smartList, 'blocks')))
	if (!debugName) throw ERROR(`<TreeItem> with missing info what to render`, props)
	if (props.blockID && props.fake) throw ERROR(`<TreeItem> with conflicting info what to render`, props)
	VERBOSE(`[TreeItem#${debugName}] created`)

	const rawThread = useRawThread()
	// const appThread = getApplogDB()
	const currentThread = useCurrentThread()
	const readOnly = useReadOnlyState()
	const blockID = createMemo(() => props.blockID)
	const isRealBlock = untrack(() => !!props.blockID)
	const block = isRealBlock ? useBlk(props.blockID) : props.fake
	const isDeleted = createMemo(() => (block as BlockVM).isDeleted)

	const focus = useFocus()
	const isTimeline = useIsTimeline()
	const [search] = useSearchContext()
	const parentContext = useBlockContext()
	const depth = (parentContext?.depth ?? 0) + 1
	const appSettings = useAppSettings()

	// Get autocollapse depths from settings or use defaults
	const getAutocollapseDepth = () => appSettings.autocollapseDepth ?? AUTOCOLLAPSE_DEPTH_DEFAULT
	const getAutocollapseDepthFocussed = () => appSettings.autocollapseDepthFocussed ?? AUTOCOLLAPSE_DEPTH_FOCUSSED_DEFAULT
	const getAutocollapseDepthTimeline = () => appSettings.autocollapseDepthTimeline ?? AUTOCOLLAPSE_DEPTH_TIMELINE_DEFAULT
	const getAutocollapseDepthTimelineSmartlist = () => appSettings.autocollapseDepthTimelineSmartlist ?? AUTOCOLLAPSE_DEPTH_TIMELINE_SMARTLIST_DEFAULT
	// const block = useBlockVM(blockID)
	// const kidRelationIDs = useKidRelationIDs(props.blockID)
	const kidRelationIDs = isRealBlock
		&& observableArrayMap(() =>
			withDS(
				rawThread,
				() => useKidRelations(props.blockID).map(({ en }) => en), /* not using useKidRelationIDs because this one orders them */
			),
		)

	const [isRelExpanded, setRelExpanded] = props.relationID
		? withDS(currentThread, () => useEntityAt<boolean>(props.relationID, REL.isExpanded, true))
		: createSignal(true)
	createEffect(() => VERBOSE(`<TreeItem#${debugName}> isRelExpanded=${isRelExpanded()}`))
	const shouldAutoCollapse = createMemo(() => {
		if (parentContext?.autoExpandDepth()) return false
		// In timeline view, smartlists are collapsed at depth 0
		if (isTimeline() && block.type === 'smartlist') {
			return depth > getAutocollapseDepthTimelineSmartlist()
		}
		// In timeline view, regular blocks collapse at depth 1
		if (isTimeline()) {
			return depth > getAutocollapseDepthTimeline()
		}
		// Default autocollapse logic for focus/normal view
		return (block.type === 'smartlist' // if we are a smartlist...
			&& focus() !== blockID() // ... and not focussed
			&& !(search() && !parentContext?.smartList)) // ... and not the top smartlist in the search
			|| parentContext?.smartList // or, if we are in a smartlist
			|| (parentContext && !parentContext?.id) // or, if we are in a smartlist's kids (HACK way to find out)
			|| (depth > (focus() ? getAutocollapseDepthFocussed() : getAutocollapseDepth())) // or, if we at a certain depth
	})
	const [isExpandedOverride, setIsExpandedOverride] = createSignal<boolean | null>(untrack(() => shouldAutoCollapse() ? false : null))
	createEffect(on(() => focus(), () => {
		// if you focus a block that was root previously, this is needed to expand it (and vice versa)
		/* if (focus()) */ setIsExpandedOverride(shouldAutoCollapse() ? false : null)
	}))

	const [autoExpandDepth, setAutoExpandDepth] = createSignal<number>(Math.max(0, parentContext?.autoExpandDepth() - 1))
	createEffect(on(() => parentContext?.autoExpandDepth(), (parentAutoExpandDepth) => {
		setAutoExpandDepth(Math.max(0, parentAutoExpandDepth - 1))
	}))

	const isExpanded = createMemo(() => {
		VERBOSE(
			`[Autocollapse#${debugName}]`,
			`isRelExpanded=${isRelExpanded()}`,
			`isExpandedOverride=${isExpandedOverride()}`,
			`depth=${depth}`,
		)
		if (autoExpandDepth() > 0) return true
		return isExpandedOverride() ?? isRelExpanded()
	})
	const setExpanded = (state: boolean) => {
		if (readOnly()) {
			setIsExpandedOverride(state)
		} else {
			setRelExpanded(state)
			/* if (state)  */ setIsExpandedOverride(null)
		}
		if (!state && autoExpandDepth() > 0) {
			setAutoExpandDepth(0)
		}
		return state
	}

	const [panel, setPanelValue] = createSignal<BlockPanelDef>(null)
	const setPanel = (newPanel: BlockPanelDef) => {
		if (panel()) WARN(`[setPanel] but we already have a panel:`, { prev: panel(), new: newPanel })
		setPanelValue(newPanel) // HACK: handle double panel
	}
	if (isRealBlock) {
		useDivergencePanel(props.blockID, setPanel)
	}

	// let blockSpan
	// onMountFx(() => {
	//  interact(blockSpan)
	//  	.draggable({ inertia: true })
	//  	.on('dragend', function(event) {
	//  		const prevSpeed = event._interaction.prevEvent.speed
	//  		event._interaction.prevEvent.speed = 601
	//  		event.swipe = event.getSwipe()
	//  		event.swipe.speed = prevSpeed
	//  		if (!event.swipe) {
	//  			DEBUG('no swipe', event, event.getSwipe())
	//  			return
	//  		}
	//  		const { angle, speed, right, left } = event.swipe
	//  		LOG('swipe', { angle, speed, right, left })
	//  		if (right) indentBlk(relation)
	//  		if (left) outdentBlk(relation, parentRelation)
	//  	})
	//  	.on('doubletap', function(event: PointerEvent) {
	//  		stopPropagation(event)
	//  		LOG('double', event, { setRadialPos, traceSet })
	//  		const { clientX: screenX, clientY: screenY } = event
	//  		traceSet({ screenX, screenY })
	//  	})
	// })()
	const kidItemsFromRelations = createLazyMemo(function kidItemsFromRelations() {
		DEBUG(`<TreeItem#${debugName}.kidItemsFromRelations>`, { kidRelationIDs, blockID: blockID(), block })
		if (kidRelationIDs) {
			return kidRelationIDs.map((relationID) => {
				const rel = useRel(relationID)
				const block = useBlk(rel.block)
				if (!block) WARN('rel refers to missing block', { block, rel })
				return ({
					id: relationID,
					relationID,
					blockID: rel.block,
					type: block?.type,
				})
			}) satisfies SortableBlockItem[]
		}
	})
	const smartQueryOrError = createLazyMemo(() => {
		if (block.type !== 'smartlist') return null

		const parentQuery = parentContext?.smartList?.query
		const ourQuery = block.smartQuery?.source.trim()
		if (!ourQuery || ourQuery.length < TAG_MIN_LENGTH + 1) return null // HACK: should use smartQuery parsing?
		const tagsQueryStr = parentQuery?.source
			? (parentQuery.source.includes(' ') ? `(${parentQuery.source}) AND ${ourQuery}` : `${parentQuery?.source} ${ourQuery}`) // HACK to simplify
			: ourQuery // ? how to join queries?
		const query = parseSmartQuery(tagsQueryStr)
		DEBUG('[smartQueryOrError]', query)
		return query
	})
	const smartListItems = createLazyMemo(function smartListItems() {
		// if (!isRealBlock) return null
		DEBUG(`<TreeItem#${debugName}.smartListItems>`, { blockID: props.blockID, fake: props.fake, block, parentContext })
		const [exclusiveSetting] = blockID() ? useBlockSetting<boolean>(blockID(), 'smart_list/exclusive_groups') : [() => false]

		if (block.type === 'smartlist') {
			// I'm doing some reactivity magic here, I think mainly to not trigger a reactive update if the smartlist contents change
			// all things that come from solid here:
			const query = smartQueryOrError()
			if (smartQueryIsError(query)) return null
			const excludeBlocks = new Set([props.blockID])
			const kidItemsFromRel = kidItemsFromRelations()?.filter(({ type }) => type === 'smartlist')
			// because mobx will not react to solid changes:
			return untrack(() =>
				// untrack = solid, you're done here...
				observableArrayMap(() => {
					const stickySmartListBlocks = kidItemsFromRel?.map((item) => {
						const block = useBlk(item.blockID)
						if (!block.smartQuery) return item
						const smartLists = withDS(rawThread, () =>
							getSmartLists(block.smartQuery, { excludeBlocks, exclusiveGroups: exclusiveSetting() }))
						DEBUG(`<TreeItem#${debugName}.smartListsFromStatic>`, { smartLists, excludeBlocks, exclusiveGroups: exclusiveSetting() })
						smartLists?.forEach(({ blocks }) => {
							return blocks.forEach(blockID =>
								excludeBlocks.add(blockID),
							)
						})
						return { ...item, smartLists }
					})

					const items: SortableBlockItem[] = stickySmartListBlocks ?? []

					const dynamicSmartLists = withDS(rawThread, () => getSmartLists(query, { excludeBlocks, exclusiveGroups: exclusiveSetting() }))
					for (const smartList of dynamicSmartLists) {
						items.push({
							id: `SmartList#${props.blockID}`,
							fake: {
								content: plaintextStringToTiptap(smartList.title),
								// type: 'smartlist',
								// smartQuery: query,
								// ? get smartQuery() {
								// 	return query
								// }
							},
							kidBlockIDs: smartList.blocks,
						})
					}

					DEBUG(`<TreeItem#${debugName}.smartListItems> =>`, { query, stickySmartListBlocks, dynamicSmartLists, items })
					return items
				}),
			)
		}
	})

	const allKidItems = createLazyMemo(function kidItems(oldMemo: SortableBlockItem[] | undefined) {
		if (!isExpanded()) return null
		if (DEBUG.isEnabled) {
			DEBUG(`<TreeItem#${debugName}.allKidItems>`, {
				kidRelationIDs,
				blockID: blockID(),
				block,
				kidItemsFromRelations: kidItemsFromRelations(),
				kidBlockIDs: props.kidBlockIDs,
				smartListItems: smartListItems(),
			})
		}
		let items: SortableBlockItem[] = []
		let kidItemsFromRel = kidItemsFromRelations()
		if (kidItemsFromRel) {
			if (block.type === 'smartlist') {
				kidItemsFromRel = kidItemsFromRel.filter(({ type }) => type !== 'smartlist') // HACK: refactor
			}
			// (i) concat bc. https://stackoverflow.com/a/51860949
			items = items.concat(kidItemsFromRel)
		}
		if (props.kidBlockIDs) {
			items = items.concat(props.kidBlockIDs.map(blockID => ({ id: `FakeDragRelation_${blockID}`, blockID })))
		}
		// if (!kidRelationIDs && blockID()) {
		// 	items.push({ id: `FakeDragRelation_${blockID}`, blockID: blockID() })
		// } else if (props.smartList?.blocks) {
		// 	items.push(...props.smartList.blocks.map(kidID => ({ id: `FakeDragRelation_${kidID}`, blockID: kidID })))
		// } else {
		// if (blockIDs) return props.blockIDs.map(blockID => ({ id: blockID, blockID }))
		if (smartListItems()) {
			if (smartListItems().length === 1) { // ? && smartListItems()[0].title ==='Other')
				// HACK: move to getSmartList logic?
				smartListItems()[0].kidBlockIDs.map(blockID => items.push({ id: `FakeDragRelation_${blockID}`, blockID }))
			} else {
				items = items.concat(smartListItems()) // .map(kidID => ({ id: `FakeDragRelation-${kidID}`, blockID: kidID })))
			}
		}
		// }
		DEBUG(`<TreeItem#${debugName}.allKidItems> => `, items)
		return arrayReUseItemsIfEqual(oldMemo, items) // Re-using bc. <For> checks by identity - https://stackoverflow.com/a/70820352
	})
	const likelyHasKids = createMemo(() => {
		// (i) this is trying to *estimate* if kids exist without needing to calculate the list
		if (block.type === 'smartlist') return true // we would show 'no matches'
		if (isExpanded()) return allKidItems().length
		return kidRelationIDs?.length || props.kidBlockIDs?.length
	})
	const concatMaybeArrays = (...maybeArrays) => {
		DEBUG({ maybeArrays })
		let returnArray = []
		for (const eachMaybeArray of maybeArrays) {
			if (eachMaybeArray?.length) returnArray = returnArray.concat(eachMaybeArray)
		}
		return returnArray
	}
	const [tagsHiddenSetting] = isRealBlock ? useBlockSetting(props.blockID, 'smart_list/hide_parent_tags', true) : [() => true] // TODO default
	const hiddenTags = () => {
		const tags = uniq(concatMaybeArrays(
			parentContext?.hiddenTags(),
			tagsHiddenSetting()
			&& (smartQueryOrError() && !smartQueryIsError(smartQueryOrError())
				? [...findAllDeeplyNested((smartQueryOrError() as SmartQuery).ast, t => t?.type === 'TAG')]
						.map(({ symbol, name }: any) => symbol + name)
				: null),
		))
		DEBUG(`[hiddenTags#${props.blockID}]`, { query: smartQueryOrError(), tags })
		return tags
	}

	return (
		<BlockContext.Provider
			value={{
				id: blockID(),
				relationToParent: props.relationID,
				setPanel,
				autoExpandDepth,
				setAutoExpandDepth,
				parentContext,
				depth,
				hiddenTags,
				smartList: (smartQueryOrError()
					? {
							get query() {
								if (smartQueryIsError(smartQueryOrError())) return null
								return smartQueryOrError()
							},
						}
					: null),
			}}
		>
			<div flex='~ col' rounded class={classNames(_props.class, 'tree-item', '-mt-1', 'pl-1')}>
				<Switch>
					<Match when={panel()}>
						<BlockPanel blockID={blockID()} relationID={props.relationID} panel={panel} />
					</Match>
					<Match when={true /* = else */}>
						<BlockItem
							blockID={blockID()}
							fake={
								props.fake /*! blockID() && props.smartList
                ? {
                    content: (props.smartList.title ?? props.smartList.query) +
                        ` (${props.smartList.count ?? props.smartList.blocks.length ?? '?'})`,
                }
                : null */
							}
							{...{ isExpanded, setExpanded }}
							relationID={props.relationID}
							hasKids={!!likelyHasKids()}
						/>

						<Show when={isExpanded() && likelyHasKids()}>
							<Show when={!allKidItems().length || (smartQueryOrError() && smartQueryIsError(smartQueryOrError()))}>
								<div pl-10 flex='~ items-center' w-full>
									<span ml-2 mb--1 text-xs class={smartQueryIsError(smartQueryOrError()) ? 'text-red' : 'opacity-40'} whitespace-pre>
										<Show when={smartQueryIsError(smartQueryOrError())}>
											Error: {smartQueryOrError().error}
										</Show>
										<Show when={!smartQueryIsError(smartQueryOrError())}>
											({search()?.length >= 3 ? 'no matches' : 'enter a search term to see matching blocks'})
										</Show>
									</span>
								</div>
							</Show>
							{/* FIXME: <Collapse seems to recreate children on value change */}
							{
								/* <Collapse value={/*NcollapsedBcDepth() &&  * / isExpanded()} class='transition-property-[height] duration-300'>
									coll? {JSON.stringify(isExpanded())} */
							}
							{
								/* <ResourceSpinner
									resource={deferredKidItems}
									spinner={
										<div pl-10 flex='~ items-center' w-full animate-pulse>
											<span my--1 opacity-40>··· ({kidCount()})</span>
										</div>
									}
								> */
							}
							<Show when={block.type === 'tabs' && allKidItems()}>
								<KidsAsTabs blockID={blockID()} items={allKidItems()} />
							</Show>
							<Show when={block.type === 'survey' && allKidItems()}>
								<KidsAsSurvey blockID={blockID()} items={allKidItems()} />
							</Show>
							<Show when={block.type !== 'tabs' && block.type !== 'survey' && allKidItems()}>
								<SortableBlocks
									class='sortable-blocks ml-5 mt-2 rounded-b-lg'
									blockID={blockID()}
									items={allKidItems()}
								>
									{(kid) => {
										DEBUG(`<TreeItem#${debugName}.ForEach(kids).kid>`, kid)
										if (kid.relationID) {
											const rel = useRel(kid.relationID)
											untrack(() =>
												DEBUG(
													`<TreeItem#${debugName}.kid#${kid.relationID}> creating block: ${rel.block}`,
												),
											)
											DEBUG.isEnabled
											&& autorun(() =>
												untrack(() => DEBUG(`<TreeItem#${debugName}.kid#${kid.relationID}> data`, { kid: { ...kid }, rel: { ...rel } })),
											)
											return (
												<div>
													<TreeItem
														class='sortable-block'
														blockID={rel.block}
														relationID={kid.relationID}
														parentRelationID={props.relationID}
													/>
												</div>
											)
										} else {
											DEBUG('kid without relation', kid)
											return (
												<div>
													<TreeItem class='sortable-block' {...kid} />
												</div>
											)
										}
									}}
								</SortableBlocks>
							</Show>
							{/* </ResourceSpinner> */}
							{/* </Collapse> */}
						</Show>

						<Show when={/* NcollapsedBcDepth() && */ isExpanded() && depth >= MAX_DEPTH}>
							<div flex='~ justify-center' text-red>Children hidden</div>
						</Show>
					</Match>
				</Switch>
			</div>
			{
				/*
					<Match when={!block.get() || (mirrorID() && !mirroredBlock())}>
						<SimpleTreeItem blockID={blockID} mirroring={mirrorID()} missing={true} relation={relation} />
					</Match>
					<Match when={block.get().mirrors}>
						<EditableTreeItem blockID={block.get().mirrors as string} mirroring={blockID}></EditableTreeItem>
					</Match>
					<Match when={!block.get().mirrors}>
						<EditableTreeItem blockID={blockID} relation={relation} parentRelation={parentRelation}></EditableTreeItem>
					</Match>
				</Switch> */
			}
		</BlockContext.Provider>
	)
}
