import type { EntityID, Thread } from '@wovin/core'
import type { Accessor, Component } from 'solid-js'
import type { FakeBlock } from '../components/BlockTree'
import { createDebouncedMemo } from '@solid-primitives/memo'
import { makePersisted } from '@solid-primitives/storage'
import { useParams, useSearchParams } from '@solidjs/router'
import AddIcon from '@suid/icons-material/Add'
import Fab from '@suid/material/Fab'
import { lastWriteWins, observableArrayMap, queryAndMap, querySingleAndMap, withoutDeleted } from '@wovin/core'
import { observable } from '@wovin/core/mobx'
import { Logger } from 'besonders-logger'
import { createEffect, createMemo, createResource, createSignal, Match, Show, Suspense, Switch } from 'solid-js'
import { storageError } from '../appInit'
import { ApplogView } from '../components/ApplogView'
import { BlockTree } from '../components/BlockTree'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { createDeferredResource, Iconify, ResourceSpinner, Spinner } from '../components/mini-components'
import { useFilters } from '../components/PillFilter'
import { useAgent } from '../data/agent/AgentState'
import { DefaultAgentBanner, getSubOrShare, StorageErrorBanner } from '../data/agent/utils-agent'
import { blockThreadWithRecursiveKids } from '../data/block-utils-nowin'
import { ENTITY_DEF } from '../data/data-types'
import { createKidInFocusOrRoot, useGlobalInputHandlers } from '../data/keybindings'
import { useMatchTree } from '../data/match-tree'
import { plaintextStringToTiptap } from '../data/note3-utils-nodeps'
import { useSearchContext } from '../data/search'
import { parseSmartQuery } from '../data/smart-list'
import { retrievePubDataWithExtras } from '../ipfs/store-sync'
import { useAppSettings } from '../ui/app-settings'
import { LazyRender } from '../ui/lazy-render'
import { PreviewInfoPanel } from '../ui/preview'
import { DBContext, useBlocksMatchingSearch, useCurrentThread, useFocus, useRawThread, useRootsOfMaybeNested, withDS } from '../ui/reactive'
import { devMode, onClickOrLongPress, replaceSearchParamsInUrl, useLocationNavigate, useSingleUrlParam } from '../ui/utils-ui'

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

export const [skipUserSetup, setSkipUserSetup] = makePersisted(
	createSignal(false),
	{ name: 'note3.skipUserSetup' },
)

let renderCount = 0
export const MainPage: Component<{ searchOpen: Accessor<boolean> }> = (props) => {
	;(renderCount++ > 0 ? WARN : DEBUG)('!!! MAINPAGE RENDER !!!', renderCount)

	const { locnav, location, navigate } = useLocationNavigate()
	const agent = useAgent()
	const appThread = useRawThread()
	const params = useParams()
	const focus = useFocus()
	const [search] = useSearchContext()
	const [urlParams, _setUrlParams] = useSearchParams()
	const activeFilters = useFilters()
	const appSettings = createMemo(() => useAppSettings()) // If the settingsID changes (in the future), the whole 'thing' is replaced
	// const isZenview = createMemo(() => location.pathname.startsWith('/zenview'))
	const isZenview = createMemo(() => urlParams.zen)

	useGlobalInputHandlers()

	const [pubFromUrl] = useSingleUrlParam('pub')
	const [previewFromUrl] = useSingleUrlParam('preview')
	const selectedPubOrPreview = createMemo(() => pubFromUrl() || previewFromUrl())
	const matchingSubOrShare = createMemo(() => selectedPubOrPreview() ? getSubOrShare(selectedPubOrPreview()) : null)
	// ? const searchStr = createMemo(() => urlParams.search)
	// if (pubFromUrl()) { // ? pull in BG when there is a pub
	// 	defer(async () => {
	// 		const pubCID = await resolveIPNS(pubFromUrl())
	// 		pullCarToEdge(pubCID)
	// 	})
	// }

	// PREVIEW //
	const previewID = createMemo(() => {
		if (DEBUG.isEnabled) DEBUG(`[previewID] memo`, { previewFromUrl: previewFromUrl(), pubFromUrl: pubFromUrl() })
		if (previewFromUrl()) return previewFromUrl()
		if (pubFromUrl() && !matchingSubOrShare()) return pubFromUrl()
		return null
	})
	const [previewData, { refetch: refetchPreview }] = useSubResource(() => previewID())
	const [displayThread] = createResource(() => {
		if (DEBUG.isEnabled) DEBUG(`[currentDS] resource source:`, { previewData, previewOrMissingPub: previewID() })
		return {
			previewID: previewID(),
			previewData: (!!previewID()) && previewData.state === 'ready' && previewData(),
			// focussed: params.blockID,
		}
	}, async ({ previewID, previewData }): Promise<Thread> => { // explicit type to no be strict about / expect it being e.g. ThreadWithoutFilters on the type
		if (DEBUG.isEnabled) DEBUG(`[currentDS] resource load`, { previewID, previewData, appThread })
		// await sleep(1000) // HACK: defer thread render to display spinner while loading
		if (previewID) {
			// if (!previewData) throw ERROR(`Invalid previewData state`, previewData)
			if (!previewData) return null
			return previewData.thread
		}
		// ? quick test shows this made things worse - but it *could* be an improvement in some situations? - probably not... eager pre-calc is mostly a bad idea
		// if (focussed) {
		// 	return withDS(appThread, () => useBlk(focussed).threadWithRecursiveKids)
		// }
		return appThread
	})
	if (DEBUG.isEnabled) createEffect(() => DEBUG(`currentDS state:`, displayThread.state))
	createEffect(() => {
		if (displayThread.latest && !!previewID() != displayThread.latest.readOnly) {
			ERROR(`!! READONLY ASSUMPTION DID NOT HOLD - tell manu!`, { pomp: previewID(), displayDS: displayThread.latest })
		}
	})

	// // const blocksMatchingSearch = createMemo(() => rawDS() && withDS(rawDS(), () => useBlocksMatchingSearch(search())))
	// // const blocksMatchingSearchAndParents = createMemo(() => rawDS() && withDS(rawDS(), () => useBlocksMatchingSearch(search())))
	const debouncedSearch = createDebouncedMemo(() => search(), 500)
	const [rootsResource] = createDeferredResource(() => ({
		displayThread: displayThread(),
		focussed: params.blockID,
		search: debouncedSearch(),
		searchOpen: props.searchOpen(),
		activeFilters: activeFilters(),
		urlPath: location.pathname,
		// urlParams: { ...urlParams },
		previewOrMissingPub: previewID(),
	}), async ({ displayThread, search, searchOpen, activeFilters, focussed, urlPath, previewOrMissingPub }) => {
		DEBUG(`[roots] finding roots of`, {
			displayThread,
			search,
			searchOpen,
			activeFilters,
			focussed,
			urlPath,
			// urlParams,
			previewOrMissingPub,
		})
		if (!displayThread) {
			return null
		}

		if (searchOpen) {
			// SEARCH
			if (focussed) {
				displayThread = blockThreadWithRecursiveKids(displayThread, focussed) // HACK?!
			}
			if (activeFilters.includes('by-tag')) { // doesSearchContainAnyTag(search)) {
				return {
					complete: observable.box(true), // HACK
					roots: [{
						fake: {
							type: 'smartlist',
							// WARN: this could work, but actually the whole resource will re-calc on search change
							get content() {
								return plaintextStringToTiptap(search)
							},
							get smartQuery() {
								return parseSmartQuery(search)
							},
						} satisfies FakeBlock,
					}],
				}
			}
			if (!search || search.length <= 2) return null // HACK: don't bother with short searches
			const blocksMatchingSearch = withDS(displayThread, () => useBlocksMatchingSearch(search))
			if (DEBUG.isEnabled) DEBUG(`[roots] blocksMatchingSearch`, blocksMatchingSearch)
			if (!blocksMatchingSearch.length) return null
			const matchingSearchButNotKids = withDS(displayThread, () => useRootsOfMaybeNested(blocksMatchingSearch))
			if (DEBUG.isEnabled) DEBUG(`[roots] matchingSearchButNotKids`, matchingSearchButNotKids)
			return {
				complete: observable.box(true),
				roots: observableArrayMap(() => matchingSearchButNotKids.map(blockID => ({ blockID })), {
					name: `searchRoots<${displayThread.nameAndSizeUntracked}>`,
				}),
			}
		} else if (focussed) {
			return { complete: observable.box(true), roots: [{ blockID: focussed as EntityID }] }
		} else if (previewOrMissingPub || urlPath === '/timeline') {
			// TIMELINE
			const lastWriteThread = lastWriteWins(displayThread) /* HACK no situation handling */
			const currentThread = withoutDeleted(lastWriteThread)
			const matchData = useMatchTree(currentThread, {
				order: 'date_desc',
			})
			// HACK: to filter out deleted roots
			const filteredRoots = observableArrayMap(() =>
				matchData.roots.filter(({ blockID }) => {
					return !queryAndMap(lastWriteThread, { en: blockID, at: ENTITY_DEF.isDeleted }, 'vl')[0]
				}),
			)
			return { ...matchData, roots: filteredRoots }
			// 	return observableArrayMap(() =>
			// 		withDS(displayDS, () => {
			// 			return useRoots().map(blockID => ({ blockID }))
			// 			// const rootRels = useKidRelationIDs(homeBlock())
			// 			// const homeBlocks = rootRels.map(rel => useRel(rel).block)
			// 			// LOG(`Home block kids:`, { rootRels, homeBlocks })
			// 			// return homeBlocks.map(blockID => ({ blockID }))
			// 		}), { name: `previewRoots<${displayDS.nameAndSizeUntracked}>` })
			// }
		}
		return {
			complete: observable.box(true),
			roots: [{ placeholderMode: true, fake: {} }],
		}
	})
	createEffect(() => DEBUG(`roots:`, rootsResource(), displayThread()))

	const addRootNode = (newRoot: boolean) => {
		WARN(`addRootNode`, event)
		// TODO: double tap / hold equivalent of ctrlKey on mobile
		createKidInFocusOrRoot(newRoot ? null : focus(), withDS(displayThread(), useCurrentThread), { location, navigate })
	}

	return (
		<main flex='1 ~ col items-center justify-start' w-full gap-4 pb-2>
			{
				/* <pre>urlP: {previewFromUrl()}</pre>
			<div key={previewFromUrl()}>
				<Suspense fallback={<spinner style='font-size: 3rem; --indicator-color: brown; --track-color: #000;'></spinner>}>
					<pre>DS: {currentDS()}</pre>
				</Suspense>
			</div> */
			}
			<Show when={displayThread.latest && !displayThread.latest?.readOnly}>
				<div
					fixed
					bottom-4
					right-4
					z-500
					use:onClickOrLongPress={(e, long) => {
						WARN(`Long press: `, e, long, onClickOrLongPress)
						e.preventDefault()
						addRootNode(long || e.ctrlKey)
					}}
				>
					<Fab
						size='small'
						color='primary'
						aria-label='add'
						// onClick={addRootNode}
					>
						<AddIcon />
					</Fab>
				</div>
			</Show>
			{/* <pre>{JSON.stringify(roots(), undefined, 2)}</pre> */}
			<Suspense fallback={<Spinner size='3rem' color='orange' />}>
				<Switch fallback='unknown state'>
					<Match when={previewData.error}>
						<div flex='~ col' w-full gap-4>
							<sl-alert variant='danger' open>
								<div flex='~ items-center gap-4'>
									<Iconify name='warning-bold' />
									<strong>Failed to load thread</strong>
								</div>
								<span font-mono>{previewData.error.message ?? JSON.stringify(previewData.error, undefined, 4)}</span>
							</sl-alert>
						</div>
					</Match>
					<Match when={previewData.loading}>
						<div flex='~ col' w-full gap-4>
							<sl-alert variant='primary' open w-full>
								<div flex='~ items-center gap-2'>
									<div flex='shrink-0' class='i-ph:eye-bold' />
									<strong overflow-hidden text-ellipsis>
										Loading preview for thread:
										{' '}
										<code>{previewID()}</code>
									</strong>
								</div>
							</sl-alert>
							<div flex='~ col items-center'>
								<Spinner size='3rem' color='purple' />
							</div>
						</div>
					</Match>
					<Match when={true}>
						{/* HACK: suspense necessary? */}
						<Suspense fallback={<sl-spinner style={{ 'font-size': '3rem', '--indicator-color': 'pink', '--track-color': '#000' }} />}>
							<Show when={displayThread()} fallback={<span text-red>No currentDS</span>}>
								<DBContext.Provider value={displayThread}>
									{
										/* <Show when={!isZenview()}> - HACK: Sidebar query needs index
										<Sidebar searchOpen={props.searchOpen} />
									</Show> */
									}
									<Show when={!displayThread().readOnly}>
										<DefaultAgentBanner />
									</Show>
									<Show when={storageError() && !isZenview()}>
										<StorageErrorBanner />
									</Show>
									{/* <pre>{JSON.stringify({f:focus(),r:roots()})}</pre> */}
									<div
										id='main-list'
										data-thread={displayThread().nameAndSizeUntracked}
										flex='~ col grow-1'
										w-full
										gap-2
									>
										<Show when={previewData() && previewFromUrl() && !isZenview()}>
											<PreviewInfoPanel
												targetThread={appThread}
												previewData={previewData}
												matchingSubOrShare={matchingSubOrShare()}
												refetch={refetchPreview}
											/>
										</Show>
										<Show when={true}>
											<Breadcrumbs
												focus={focus}
												showOverview={() =>
													!isZenview() && !!(params.blockID && params.blockID !== appSettings().homeBlock /* && roots.get().length > 1 */)}
											/>
										</Show>
										<ResourceSpinner
											resource={rootsResource}
											spinner={(
												<div flex='~ justify-center'>
													<Spinner size='3em' />
												</div>
											)}
										>
											<Show when={rootsResource()} fallback={props.searchOpen() ? '' : 'empty roots?!'} keyed>
												{
													/* <Show when={search()}>
														<em text-sm>Results: {roots()?.length}</em>
													</Show> */
												}
												<LazyRender
													items={() => [...rootsResource().roots]}
													fallback={<div>No roots - reload?</div>}
													reset={[focus(), search()]}
													allLoaded={() => rootsResource().complete.get()}
													loadMore={rootsResource().loadMore}
												>
													{(item, isInitial) => (
														<div
															data-lazy-block-id={item.blockID}
															data-lazy-search={item.search}
															class={isInitial ? '' : 'animate-[fade-in-up-small_1s_ease-in-out] animate-duration-[300ms]'}
														>
															{/* <pre>{JSON.stringify(id)}</pre> */}
															<BlockTree {...item} />
														</div>
													)}
												</LazyRender>
											</Show>
										</ResourceSpinner>
									</div>
									<div w-full>
										<Show when={isZenview()}>
											<div
												class='mb-1 box-border w-full'
												flex='~ items-center justify-between'
											>
												<Show when={previewData()?.info?.logs?.length}>
													{querySingleAndMap(previewData().info.logs, { at: 'pub/name' }, 'vl').get()}
												</Show>
												<a text-xs href={replaceSearchParamsInUrl(location, { zen: null })}>Exit Zen view</a>
											</div>
										</Show>
										<Show when={devMode() || !isZenview()}>
											<ApplogView max-h-42vh />
										</Show>
									</div>
								</DBContext.Provider>
							</Show>
						</Suspense>
					</Match>
				</Switch>
			</Suspense>
		</main>
	)
}

export function useSubResource(subID: Accessor<string | null>) {
	const appThread = useRawThread()

	const [previewData, { refetch }] = createResource(subID, async (previewOrMissingPub) => {
		try {
			if (DEBUG.isEnabled) DEBUG(`[useSubResource] for`, previewOrMissingPub)
			// await new Promise(resolve => setTimeout(resolve, 700)) // ; throw new Error(`Eddi`)
			if (!previewOrMissingPub) return null
			return await retrievePubDataWithExtras(appThread, previewOrMissingPub)
		} catch (err) {
			ERROR(`Failed to fetch preview`, previewOrMissingPub, err)
			throw new Error(`Failed to fetch preview: ${err.message}`)
		}
	})
	DEBUG.isEnabled && createEffect(() => {
		DEBUG(`[useSubResource]`, { data: previewData(), state: previewData.state, err: previewData.error })
	})

	return [previewData, { refetch }] as const
}
