import type { Message, Thread } from '@nylas-labs/cli-kit/v3'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import {
	Archive,
	ArrowLeft,
	Forward,
	Inbox,
	Loader2,
	MailOpen,
	Reply,
	ReplyAll,
	Star,
	Trash2,
} from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import { ThreadConversation } from '#features/mail/components/ThreadConversation'
import { MobileThreadResponseActions } from '#features/mail/components/ThreadResponseActions'
import {
	forwardDraftSearch,
	replyAllDraftSearch,
	replyDraftSearch,
	STAR_FILLED_CLASS,
} from '#features/mail/lib/mail-ui-model'
import { applyMailCacheEffect } from '#features/mail/state/mail-cache'
import { useUpdateThreadMutation } from '#features/mail/state/mail-mutations'
import {
	type MailThreadDetail,
	threadDetailQueryOptions,
	toMailThreadDetail,
} from '#features/mail/state/mail-queries'
import { getThreadMessages } from '#server/fns'
import { ScrollArea } from '#shared/components/ui/scroll-area'
import { useHorizontalSwipe } from '#shared/hooks/use-horizontal-swipe'
import { cn } from '#shared/lib/utils'

export const Route = createFileRoute('/mail/f/$folderId/t/$threadId')({
	validateSearch: (search): { baseFolderId?: string } => ({
		...(typeof search.baseFolderId === 'string' ? { baseFolderId: search.baseFolderId } : {}),
	}),
	loader: async ({ context, params }) =>
		context.queryClient.ensureQueryData(
			threadDetailQueryOptions(params.threadId, (threadId) => getThreadMessages({ data: { threadId } })),
		),
	component: ThreadView,
})

type PendingThreadAction = 'archive' | 'delete' | 'star' | 'unread'

function normalizeInitialThreadDetail(
	detail:
		| MailThreadDetail
		| {
				thread: Thread
				messages: Message[]
				mailboxEmail: string
				markedRead?: boolean
				ownmailDraftMessageIds?: string[]
		  },
): MailThreadDetail {
	return 'ownmailDraftMessageIds' in detail ? toMailThreadDetail(detail) : detail
}

const REPLY_SHORTCUT_INTERACTIVE_SELECTOR = [
	'input',
	'textarea',
	'select',
	'button',
	'a',
	'summary',
	'[contenteditable]:not([contenteditable="false"])',
	'[role="button"]',
	'[role="link"]',
	'[role="textbox"]',
	'[role="combobox"]',
	'[role="menuitem"]',
	'[role="option"]',
	'[role="switch"]',
	'[role="tab"]',
	'ownmail-email',
].join(',')

function shouldIgnoreReplyShortcut(event: KeyboardEvent): boolean {
	if (event.defaultPrevented) return true
	const composedPath = event.composedPath()
	const path = composedPath.length > 0 ? composedPath : [event.target]
	return path.some(
		(node) =>
			node instanceof HTMLElement &&
			(node.isContentEditable || node.matches(REPLY_SHORTCUT_INTERACTIVE_SELECTOR)),
	)
}

function ThreadView() {
	const initialDetail = Route.useLoaderData()
	const { folderId, threadId } = Route.useParams()
	const { baseFolderId } = Route.useSearch()
	const queryClient = useQueryClient()
	const { data: detail } = useQuery({
		...threadDetailQueryOptions(
			threadId,
			/* v8 ignore next -- @preserve production query wiring is covered through the isolated route screen and query-option tests */
			(id) => getThreadMessages({ data: { threadId: id } }),
		),
		initialData: normalizeInitialThreadDetail(initialDetail),
	})
	const { thread, messages, mailboxEmail, markedRead } = detail
	const updateThread = useUpdateThreadMutation()
	const navigate = useNavigate()
	const [error, setError] = useState<string | null>(null)
	const [starred, setStarred] = useState(thread.starred)
	const [pendingAction, setPendingAction] = useState<PendingThreadAction | null>(null)
	const goBackToList = useCallback(
		() =>
			navigate({
				to: '/mail/f/$folderId',
				params: { folderId },
				search: baseFolderId ? { baseFolderId } : {},
			}),
		[baseFolderId, folderId, navigate],
	)
	const swipeHandlers = useHorizontalSwipe(goBackToList)
	const lastMessage = messages.at(-1)
	const isArchived = folderId === 'archive' || thread.folders?.includes('archive') === true
	const reply = useCallback(() => {
		/* v8 ignore next -- every exposed reply entry point requires a latest message -- @preserve */
		if (!lastMessage) return
		navigate({
			to: '/mail/compose',
			search: { folderId, threadId, ...replyDraftSearch(lastMessage) },
		})
	}, [folderId, lastMessage, navigate, threadId])
	const replyAll = useCallback(() => {
		/* v8 ignore next -- every exposed reply-all entry point requires a latest message -- @preserve */
		if (!lastMessage) return
		navigate({
			to: '/mail/compose',
			search: {
				folderId,
				threadId,
				...replyAllDraftSearch(lastMessage, mailboxEmail),
			},
		})
	}, [folderId, lastMessage, mailboxEmail, navigate, threadId])
	const forward = useCallback(() => {
		/* v8 ignore next -- every exposed forward entry point requires a latest message -- @preserve */
		if (!lastMessage) return
		navigate({
			to: '/mail/compose',
			search: { folderId, threadId, ...forwardDraftSearch(lastMessage) },
		})
	}, [folderId, lastMessage, navigate, threadId])

	useEffect(() => {
		setStarred(thread.starred)
	}, [thread.starred])

	const act = useCallback(
		async (
			action: PendingThreadAction,
			input: { unread?: boolean; starred?: boolean; folder?: string },
			leave = false,
		) => {
			/* v8 ignore next -- every toolbar action is disabled while the request is pending -- @preserve */
			if (pendingAction) return
			setError(null)
			const previousStarred = starred
			if (typeof input.starred === 'boolean') setStarred(input.starred)
			setPendingAction(action)
			try {
				await updateThread.mutateAsync({ threadId, ...input })
				if (leave) {
					await navigate({
						to: '/mail/f/$folderId',
						params: { folderId },
						search: baseFolderId ? { baseFolderId } : {},
					})
				}
			} catch {
				/* v8 ignore start -- a failed optimistic star action restores the previous state before surfacing its error -- @preserve */
				if (typeof input.starred === 'boolean') setStarred(previousStarred)
				setError('Action failed')
				/* v8 ignore stop -- @preserve */
			} finally {
				setPendingAction(null)
			}
		},
		[baseFolderId, folderId, navigate, pendingAction, starred, threadId, updateThread],
	)

	useEffect(() => {
		function onKeyDown(event: KeyboardEvent) {
			const key = event.key.toLowerCase()
			if (key === 'r') {
				const isModified = event.repeat || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey
				if (isModified || shouldIgnoreReplyShortcut(event)) return
				if (document.querySelector('[role="dialog"]') || !lastMessage) return
				event.preventDefault()
				reply()
				return
			}

			const target = event.target as HTMLElement | null
			const isTyping =
				target?.tagName === 'INPUT' || target?.tagName === 'TEXTAREA' || target?.isContentEditable
			if (isTyping || event.repeat || event.metaKey || event.ctrlKey || event.altKey) return
			if (document.querySelector('[role="dialog"]')) return
			if (key === 'e') {
				event.preventDefault()
				act('archive', { folder: isArchived ? 'inbox' : 'archive' }, true)
			}
			if (event.key === '#') {
				event.preventDefault()
				act('delete', { folder: 'trash' }, true)
			}
			if (key === 's') {
				event.preventDefault()
				act('star', { starred: !starred })
			}
			if (key === 'u') {
				event.preventDefault()
				act('unread', { unread: true }, true)
			}
			if (event.key === 'Escape') {
				event.preventDefault()
				navigate({
					to: '/mail/f/$folderId',
					params: { folderId },
					search: baseFolderId ? { baseFolderId } : {},
				})
			}
		}
		window.addEventListener('keydown', onKeyDown)
		return () => window.removeEventListener('keydown', onKeyDown)
	}, [act, baseFolderId, folderId, isArchived, lastMessage, navigate, reply, starred])

	useEffect(() => {
		if (markedRead) {
			applyMailCacheEffect(queryClient, { type: 'thread.read', threadId, unread: false, thread })
		}
	}, [markedRead, queryClient, thread, threadId])

	return (
		<div
			{...swipeHandlers}
			onTouchMove={(event) => {
				if (event.touches.length > 1) swipeHandlers.onTouchCancel()
			}}
			onTouchEnd={(event) => {
				if (event.touches.length > 0) {
					swipeHandlers.onTouchCancel()
					return
				}
				swipeHandlers.onTouchEnd(event)
			}}
			data-testid="thread-reader"
			className="flex min-h-0 min-w-0 flex-1 flex-col bg-background"
			style={{ touchAction: 'pan-y pinch-zoom' }}
		>
			<div className="flex h-14 shrink-0 items-center gap-1 border-b border-border px-3">
				<button
					type="button"
					onClick={goBackToList}
					aria-label="Back to list"
					className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground active:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring xl:hidden"
				>
					<ArrowLeft className="h-5 w-5" />
				</button>
				<IconButton
					label={
						pendingAction === 'archive'
							? isArchived
								? 'Returning to inbox'
								: 'Archiving'
							: isArchived
								? 'Return to inbox'
								: 'Archive'
					}
					disabled={pendingAction !== null}
					loading={pendingAction === 'archive'}
					onClick={() => act('archive', { folder: isArchived ? 'inbox' : 'archive' }, true)}
				>
					{pendingAction === 'archive' ? (
						<Loader2 className="h-4 w-4 animate-spin" />
					) : isArchived ? (
						<Inbox className="h-4 w-4" />
					) : (
						<Archive className="h-4 w-4" />
					)}
				</IconButton>
				<IconButton
					label={pendingAction === 'delete' ? 'Deleting' : 'Delete'}
					disabled={pendingAction !== null}
					loading={pendingAction === 'delete'}
					onClick={() => act('delete', { folder: 'trash' }, true)}
				>
					{pendingAction === 'delete' ? (
						<Loader2 className="h-4 w-4 animate-spin" />
					) : (
						<Trash2 className="h-4 w-4" />
					)}
				</IconButton>
				<IconButton
					label={
						pendingAction === 'star' ? (starred ? 'Starring' : 'Unstarring') : starred ? 'Unstar' : 'Star'
					}
					disabled={pendingAction !== null}
					loading={pendingAction === 'star'}
					onClick={() => act('star', { starred: !starred })}
				>
					{pendingAction === 'star' ? (
						<Loader2 className="h-4 w-4 animate-spin" />
					) : (
						<Star className={cn('h-4 w-4', starred && STAR_FILLED_CLASS)} />
					)}
				</IconButton>
				<IconButton
					label={pendingAction === 'unread' ? 'Marking unread' : 'Mark unread'}
					disabled={pendingAction !== null}
					loading={pendingAction === 'unread'}
					onClick={() => act('unread', { unread: true }, true)}
				>
					{pendingAction === 'unread' ? (
						<Loader2 className="h-4 w-4 animate-spin" />
					) : (
						<MailOpen className="h-4 w-4" />
					)}
				</IconButton>

				{lastMessage ? (
					<div className="ml-auto hidden items-center gap-1 sm:flex">
						<ActionButton label="Reply" onClick={reply}>
							<Reply className="h-4 w-4" />
						</ActionButton>
						<ActionButton label="Reply all" onClick={replyAll}>
							<ReplyAll className="h-4 w-4" />
						</ActionButton>
						<ActionButton label="Forward" onClick={forward}>
							<Forward className="h-4 w-4" />
						</ActionButton>
					</div>
				) : null}
			</div>
			{error ? <ErrorBanner message={error} /> : null}

			<ScrollArea
				key={threadId}
				aria-label="Thread conversation"
				className="min-h-0 flex-1"
				overflowIndicatorClassName="from-muted/80 dark:from-background/80"
			>
				<ThreadConversation thread={thread} messages={messages} />
			</ScrollArea>

			{lastMessage ? (
				<div className="shrink-0 border-t border-border bg-background px-5 py-3 lg:px-8">
					<MobileThreadResponseActions onReply={reply} onReplyAll={replyAll} onForward={forward} />
					<button
						type="button"
						onClick={reply}
						className="hidden w-full items-center gap-3 rounded-lg border border-border bg-muted/30 px-4 py-3 text-left text-sm text-muted-foreground transition-colors hover:border-ring/30 hover:bg-muted/50 hover:text-foreground sm:flex"
					>
						<Reply className="h-4 w-4 shrink-0" />
						<span>Write a reply…</span>
					</button>
				</div>
			) : null}
		</div>
	)
}

function IconButton({
	label,
	onClick,
	disabled = false,
	loading = false,
	children,
}: {
	label: string
	onClick?: () => void
	disabled?: boolean
	loading?: boolean
	children: React.ReactNode
}) {
	return (
		<button
			type="button"
			onClick={onClick}
			aria-label={label}
			title={label}
			disabled={disabled}
			aria-busy={loading || undefined}
			className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground active:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-wait disabled:opacity-50 xl:h-9 xl:w-9"
		>
			{children}
		</button>
	)
}

function ActionButton({
	label,
	onClick,
	children,
}: {
	label: string
	onClick?: () => void
	children: React.ReactNode
}) {
	return (
		<button
			type="button"
			onClick={onClick}
			className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
		>
			{children}
			<span className="hidden md:inline">{label}</span>
		</button>
	)
}

export function ErrorBanner({ message }: { message: string }) {
	const isQuota = message.startsWith('QUOTA:')
	return (
		<p className="mx-4 mt-3 rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive">
			{isQuota ? message.slice(6).trim() : message}
		</p>
	)
}
