import { type ReactElement, type ReactNode, useEffect, useRef, useState } from 'react';

import type { AppBskyActorDefs, AppBskyFeedDefs } from '@atcute/bluesky';

import { useQueryClient } from '@tanstack/react-query';

import { onVisibilityChange } from '#/lib/browser/visibility';
import { DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS } from '#/lib/constants/feeds';
import { cleanError, isNetworkError } from '#/lib/errors';
import { useNonReactiveCallback } from '#/lib/hooks/use-non-reactive-callback';
import type { Richtext } from '#/lib/rich-text';

import { usePostAuthorShadowFilter } from '#/state/cache/profile-shadow';
import { postCreated } from '#/state/events';
import { useFeedFeedbackContext } from '#/state/feed-feedback';
import { STALE } from '#/state/queries';
import type { FeedDescriptor } from '#/state/queries/feed-descriptor';
import {
	type FeedPostSlice,
	type FeedPostSliceItem,
	pollLatest,
	RQKEY,
	usePostFeedQuery,
} from '#/state/queries/post-feed';
import { useSession } from '#/state/session';

import { SuggestedFollows } from '#/components/FeedInterstitials';
import { List, type ListRef, type ListRenderItemInfo } from '#/components/List/List';
import * as ListTail from '#/components/List/ListTail';
import { PostFeedLoadingPlaceholder } from '#/components/PostFeed/PostFeedLoadingPlaceholder';
import { RichText } from '#/components/RichText';
import { TrendingInterstitial, useShowTrendingInterstitial } from '#/components/TrendingInterstitial';

import { m } from '#/paraglide/messages';
import { useFocusEffect, useIsFocused } from '#/router';

import { FeedShutdownMsg } from './FeedShutdownMsg';
import * as css from './PostFeed.css';
import { PostFeedErrorMessage } from './PostFeedErrorMessage';
import { PostFeedItem } from './PostFeedItem';
import { ShowLessFollowup } from './ShowLessFollowup';
import { ViewFullThread } from './ViewFullThread';

export type FeedRow =
	| {
			type: 'loading';
			key: string;
	  }
	| {
			type: 'empty';
			key: string;
	  }
	| {
			type: 'error';
			key: string;
	  }
	| {
			type: 'feedShutdownMsg';
			key: string;
			feedUri: string;
	  }
	| {
			type: 'sliceItem';
			key: string;
			slice: FeedPostSlice;
			indexInSlice: number;
			showReplyTo: boolean;
	  }
	| {
			type: 'sliceViewFullThread';
			key: string;
			uri: string;
	  }
	| {
			type: 'interstitialFollows';
			key: string;
	  }
	| {
			type: 'interstitialTrending';
			key: string;
	  }
	| {
			type: 'showLessFollowup';
			key: string;
	  }
	| {
			type: 'description';
			key: string;
			value: Richtext | string;
	  };

export function getItemsForFeedback(feedRow: FeedRow): {
	item: FeedPostSliceItem;
	feedContext: string | undefined;
	reqId: string | undefined;
}[] {
	if (feedRow.type === 'sliceItem') {
		// Report only the post this row actually rendered, not its unshown slice siblings.
		const item = feedRow.slice.items[feedRow.indexInSlice];
		if (!item) {
			return [];
		}
		return [{ feedContext: feedRow.slice.feedContext, item, reqId: feedRow.slice.reqId }];
	} else {
		return [];
	}
}

const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY;

const FEED_ITEM_HEIGHT_ESTIMATE = 300;

export function PostFeed({
	feed,
	description,
	ignoreFilterFor,
	pollInterval,
	disablePoll,
	scrollElRef,
	onScrolledDownChange,
	onHasNew,
	renderEmptyState,
	savedFeedConfig,
}: {
	feed: FeedDescriptor;
	/** shown as the first row, above the posts. for feeds whose description is part of the surface. */
	description?: Richtext | string;
	ignoreFilterFor?: string;
	pollInterval?: number;
	disablePoll?: boolean;
	scrollElRef?: ListRef;
	onHasNew?: (v: boolean) => void;
	onScrolledDownChange?: (isScrolledDown: boolean) => void;
	renderEmptyState: () => ReactElement;
	savedFeedConfig?: AppBskyActorDefs.SavedFeed;
}): ReactNode {
	const feedgenUri = feed.type === 'feedgen' ? feed.uri : undefined;
	const isDiscover = feedgenUri === DISCOVER_FEED_URI;

	const queryClient = useQueryClient();
	const { currentAccount, hasSession } = useSession();
	const feedFeedback = useFeedFeedbackContext();
	const isFocused = useIsFocused();

	const lastFetchRef = useRef<number | null>(null);
	if (lastFetchRef.current === null) {
		// oxlint-disable-next-line react/purity -- initialize once
		lastFetchRef.current = Date.now();
	}

	const showTrendingInterstitial = useShowTrendingInterstitial({
		enabled: hasSession && isDiscover,
	});

	const [hasPressedShowLessUris, setHasPressedShowLessUris] = useState(() => new Set<string>());
	const onPressShowLess = (interaction: AppBskyFeedDefs.Interaction) => {
		if (interaction.item) {
			const uri = interaction.item;
			setHasPressedShowLessUris((prev) => new Set([...prev, uri]));
		}
	};

	const opts = { enabled: isFocused, ignoreFilterFor };
	const {
		data,
		error,
		fetchNextPage,
		hasNextPage,
		isError,
		isFetched,
		isFetching,
		isFetchingNextPage,
		refetch,
	} = usePostFeedQuery(feed, opts);
	const lastFetchedAt = data?.pages[0]?.fetchedAt;
	const isEmpty = !isFetching && !data?.pages?.some((page) => page.slices.length);

	useEffect(() => {
		if (lastFetchedAt) {
			lastFetchRef.current = lastFetchedAt;
		}
	}, [lastFetchedAt]);

	const checkForNew = useNonReactiveCallback(async () => {
		if (!data?.pages[0] || isFetching || !onHasNew || !isFocused || disablePoll) {
			return;
		}

		// Discover always has fresh content.
		if (isDiscover) {
			return onHasNew(true);
		}

		try {
			if (await pollLatest(data.pages[0])) {
				if (isEmpty) {
					void refetch();
				} else {
					onHasNew(true);
				}
			}
		} catch (e) {
			// this runs on a poll, so a dropped connection is expected and would only be noise
			if (!isNetworkError(e)) {
				console.error('Poll latest failed', feed, e);
			}
		}
	});

	const isScrolledDownRef = useRef(false);
	const handleScrolledDownChange = (isScrolledDown: boolean) => {
		isScrolledDownRef.current = isScrolledDown;
		onScrolledDownChange?.(isScrolledDown);
	};

	const myDid = currentAccount?.did || '';
	const showsOwnPosts = feed.type === 'following' || (feed.type === 'author' && feed.did === myDid);
	useEffect(() => {
		if (!showsOwnPosts) {
			return;
		}

		return postCreated.subscribe(() => {
			if (!isScrolledDownRef.current) {
				void queryClient.invalidateQueries({ queryKey: RQKEY(feed) });
			}
		});
	}, [feed, queryClient, showsOwnPosts]);

	useFocusEffect(() => {
		if (!disablePoll) {
			const timeSinceFirstLoad = Date.now() - lastFetchRef.current!;
			if (isEmpty || timeSinceFirstLoad > CHECK_LATEST_AFTER) {
				void checkForNew();
			}
		}
	});

	useFocusEffect(() => {
		const unsubscribe = onVisibilityChange((visible) => {
			// check for new when the document becomes visible
			if (visible) {
				void checkForNew();
			}
		});
		let stopPolling: (() => void) | undefined;
		if (pollInterval) {
			// check for new on interval
			const i = setInterval(() => {
				void checkForNew();
			}, pollInterval);
			stopPolling = () => clearInterval(i);
		}
		return () => {
			unsubscribe();
			stopPolling?.();
		};
	});

	const blockedOrMutedAuthors = usePostAuthorShadowFilter(
		// author feeds have their own handling
		feed.type === 'author' ? undefined : data?.pages,
	);

	const feedItems: FeedRow[] = ((): FeedRow[] => {
		// wraps a slice item, and replaces it with a showLessFollowup item
		// if the user has pressed show less on it
		const sliceItem = (row: Extract<FeedRow, { type: 'sliceItem' }>) => {
			const uri = row.slice.items[row.indexInSlice]?.uri;
			if (uri && hasPressedShowLessUris.has(uri)) {
				return {
					type: 'showLessFollowup',
					key: row.key,
				} as const;
			} else {
				return row;
			}
		};

		let feedKind: 'discover' | 'profile' | undefined;
		if (isDiscover) {
			feedKind = 'discover';
		} else if (feed.type === 'author' && feed.view === 'posts') {
			feedKind = 'profile';
		}

		const arr: FeedRow[] = [];
		if (feedgenUri !== undefined && KNOWN_SHUTDOWN_FEEDS.includes(feedgenUri)) {
			arr.push({
				type: 'feedShutdownMsg',
				key: 'feedShutdownMsg',
				feedUri: feedgenUri,
			});
		}
		if (isFetched) {
			if (isError && isEmpty) {
				arr.push({
					type: 'error',
					key: 'error',
				});
			} else if (isEmpty) {
				arr.push({
					type: 'empty',
					key: 'empty',
				});
			} else if (data) {
				let sliceIndex = -1;

				for (const page of data.pages) {
					for (const slice of page.slices) {
						sliceIndex++;

						if (hasSession) {
							switch (feedKind) {
								case 'discover': {
									// only reserve a row (and its border) for trending when it will actually render
									if (sliceIndex === 0 && showTrendingInterstitial) {
										arr.push({
											type: 'interstitialTrending',
											key: 'interstitialTrending-' + sliceIndex,
										});
									} else if (sliceIndex === 30) {
										arr.push({
											type: 'interstitialFollows',
											key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
										});
									}
									break;
								}
								case 'profile': {
									if (sliceIndex === 5) {
										arr.push({
											type: 'interstitialFollows',
											key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
										});
									}
									break;
								}
							}
						}

						if (slice.items.some((item) => blockedOrMutedAuthors.includes(item.post.author.did))) {
							// skip
						} else if (slice.isIncompleteThread && slice.items.length >= 3) {
							const beforeLast = slice.items.length - 2;
							const last = slice.items.length - 1;
							arr.push(
								sliceItem({
									type: 'sliceItem',
									key: slice.items[0]!._reactKey,
									slice: slice,
									indexInSlice: 0,
									showReplyTo: false,
								}),
							);
							arr.push({
								type: 'sliceViewFullThread',
								key: slice._reactKey + '-viewFullThread',
								uri: slice.items[0]!.uri,
							});
							arr.push(
								sliceItem({
									type: 'sliceItem',
									key: slice.items[beforeLast]!._reactKey,
									slice: slice,
									indexInSlice: beforeLast,
									showReplyTo:
										slice.items[beforeLast]!.parentAuthor?.did !== slice.items[beforeLast]!.post.author.did,
								}),
							);
							arr.push(
								sliceItem({
									type: 'sliceItem',
									key: slice.items[last]!._reactKey,
									slice: slice,
									indexInSlice: last,
									showReplyTo: false,
								}),
							);
						} else {
							for (let i = 0; i < slice.items.length; i++) {
								const item = slice.items[i]!;
								arr.push(
									sliceItem({
										type: 'sliceItem',
										key: item._reactKey,
										slice: slice,
										indexInSlice: i,
										showReplyTo: i === 0,
									}),
								);
							}
						}
					}
				}
			}
		} else {
			if (isFocused) {
				arr.push({
					type: 'loading',
					key: 'loading',
				});
			}
		}

		if (description) {
			arr.unshift({
				type: 'description',
				key: 'description',
				value: description,
			});
		}

		return arr;
	})();

	const onPressTryAgain = () => {
		void refetch();
		onHasNew?.(false);
	};

	const renderItem = ({ item: row, index: rowIndex }: ListRenderItemInfo<FeedRow>) => {
		switch (row.type) {
			case 'empty': {
				return renderEmptyState();
			}
			case 'error': {
				return (
					<PostFeedErrorMessage
						feedDesc={feed}
						error={error ?? undefined}
						onPressTryAgain={onPressTryAgain}
						savedFeedConfig={savedFeedConfig}
						topBorder={rowIndex !== 0}
					/>
				);
			}
			case 'loading': {
				return <PostFeedLoadingPlaceholder topBorder={rowIndex !== 0} />;
			}
			case 'feedShutdownMsg': {
				return <FeedShutdownMsg feedUri={row.feedUri} topBorder={rowIndex !== 0} />;
			}
			case 'interstitialFollows': {
				return <SuggestedFollows feed={feed} />;
			}
			case 'interstitialTrending': {
				return <TrendingInterstitial />;
			}
			case 'description': {
				return (
					<div className={css.description}>
						<RichText color="textContrastHigh" size="md" value={row.value} />
					</div>
				);
			}
			case 'sliceItem': {
				const slice = row.slice;
				const indexInSlice = row.indexInSlice;
				const item = slice.items[indexInSlice]!;
				return (
					<PostFeedItem
						post={item.post}
						record={item.record}
						postNumbering={item.postNumbering}
						reason={indexInSlice === 0 ? slice.reason : undefined}
						feedContext={slice.feedContext}
						reqId={slice.reqId}
						moderation={item.moderation}
						parentAuthor={item.parentAuthor}
						showReplyTo={row.showReplyTo}
						isThreadParent={isThreadParentAt(slice.items, indexInSlice)}
						isThreadChild={isThreadChildAt(slice.items, indexInSlice)}
						isParentBlocked={item.isParentBlocked}
						isParentNotFound={item.isParentNotFound}
						hideTopBorder={rowIndex === 0 && indexInSlice === 0}
						rootPost={slice.items[0]!.post}
						onShowLess={onPressShowLess}
					/>
				);
			}
			case 'sliceViewFullThread': {
				return <ViewFullThread uri={row.uri} />;
			}
			case 'showLessFollowup': {
				return <ShowLessFollowup topBorder={rowIndex !== 0} />;
			}
		}
	};

	const onItemSeen = (item: FeedRow) => {
		feedFeedback.onItemSeen(item);
	};

	return (
		<List
			ref={scrollElRef}
			data={feedItems}
			keyExtractor={(item: FeedRow) => item.key}
			estimateHeight={FEED_ITEM_HEIGHT_ESTIMATE}
			renderItem={renderItem}
			ListFooterComponent={
				<ListTail.Frame border={!isEmpty}>
					{isFetchingNextPage ? (
						<ListTail.Pending />
					) : isError && !isEmpty ? (
						<ListTail.Error message={cleanError(error)} onRetry={() => void fetchNextPage()} />
					) : !isEmpty && !isFetching && !hasNextPage ? (
						<ListTail.End>{m['common.list.endOfList']()}</ListTail.End>
					) : null}
				</ListTail.Frame>
			}
			onScrolledDownChange={handleScrolledDownChange}
			onEndReached={() => {
				if (isError) {
					return;
				}
				void fetchNextPage();
			}}
			onEndReachedThreshold={2}
			onItemSeen={onItemSeen}
		/>
	);
}

export function isThreadParentAt(arr: Array<unknown>, i: number) {
	if (arr.length === 1) {
		return false;
	}
	return i < arr.length - 1;
}

export function isThreadChildAt(arr: Array<unknown>, i: number) {
	if (arr.length === 1) {
		return false;
	}
	return i > 0;
}
