import type { RivetSse } from "@rivet-gg/cloud";
import { faArrowDown, faTriangleExclamation, Icon } from "@rivet-gg/icons";
import type { Virtualizer } from "@tanstack/react-virtual";
import { useCallback, useEffect, useRef, useState } from "react";
import { ErrorDetails } from "@/components/actors";
import { VirtualScrollArea } from "@/components/virtual-scroll-area";
import { AnsiText } from "./lib/ansi";
import { cn } from "./lib/utils";
import { ScrollArea } from "./ui/scroll-area";
import { Skeleton } from "./ui/skeleton";
import { useDeploymentLogsStream } from "./use-deployment-logs-stream";

const SKELETON_KEYS = [
	"a",
	"b",
	"c",
	"d",
	"e",
	"f",
	"g",
	"h",
	"i",
	"j",
	"k",
	"l",
	"m",
	"n",
	"o",
	"p",
	"q",
	"r",
	"s",
	"t",
	"u",
	"v",
	"w",
	"x",
	"y",
	"z",
	"aa",
	"ab",
	"ac",
	"ad",
	"ae",
	"af",
	"ag",
	"ah",
	"ai",
	"aj",
	"ak",
	"al",
	"am",
	"an",
];

interface DeploymentLogsProps {
	project?: string;
	namespace?: string;
	pool: string;
	filter?: string;
	region?: string;
	paused?: boolean;
	logsRef?: React.MutableRefObject<RivetSse.LogStreamEvent.Log[]>;
	regionLabelLength?: number;
}

interface LogRowData {
	className?: string;
	entry?: RivetSse.LogStreamEvent.Log;
	isSentinel?: boolean;
	isLoadingMore?: boolean;
	regionColumnWidth?: string;
}

function LogRow({
	entry,
	isSentinel,
	isLoadingMore,
	regionColumnWidth,
	...props
}: LogRowData) {
	if (isSentinel) {
		return (
			<div
				{...props}
				className={cn(
					"px-4 py-1 border-b text-muted-foreground/50 italic",
					props.className,
				)}
			>
				{isLoadingMore
					? "Loading older logs…"
					: "Scroll to top to load older logs"}
			</div>
		);
	}

	if (!entry) return null;

	return (
		<div
			{...props}
			className={cn("font-mono grid grid-cols-subgrid", props.className)}
		>
			<div
				className={cn(
					"grid gap-3 whitespace-pre-wrap break-words px-4 py-1 border-b",
					{
						"text-red-400": entry.data.severity === "error",
						"text-muted-foreground":
							entry.data.severity !== "error",
					},
				)}
				style={{
					gridTemplateColumns: `max-content ${regionColumnWidth ?? "16ch"} 3fr`,
				}}
			>
				<span className="text-neutral-500 shrink-0 select-none">
					{entry.data.timestamp}
				</span>
				{entry.data.region ? (
					<span className="text-neutral-600 shrink-0 select-none">
						[{entry.data.region}]
					</span>
				) : (
					<span />
				)}
				<span className="flex-1">
					<AnsiText text={entry.data.message} />
				</span>
			</div>
		</div>
	);
}

export function DeploymentLogs({
	project,
	namespace,
	pool,
	filter,
	region,
	paused,
	logsRef,
	regionLabelLength,
}: DeploymentLogsProps) {
	// Region label gets brackets (2 chars) plus a small buffer so the column
	// has visual breathing room and tolerates glyph-width differences from
	// `ch` (measured from "0").
	const regionColumnWidth =
		regionLabelLength && regionLabelLength > 0
			? `${regionLabelLength + 4}ch`
			: "18ch";
	const {
		logs,
		isLoading,
		error,
		streamError,
		isLoadingMore,
		hasMore,
		loadMoreHistory,
	} = useDeploymentLogsStream({
		project: project ?? "",
		namespace: namespace ?? "",
		pool,
		filter,
		region,
		paused,
	});

	const viewportRef = useRef<HTMLDivElement>(null);
	const virtualizerRef = useRef<Virtualizer<HTMLDivElement, Element>>(null);
	const [follow, setFollow] = useState(true);
	// Track the log count before a load-more so we can restore scroll position.
	const prevLogCountRef = useRef(0);

	// Freeze displayed logs when not following so appended entries don't shift scroll.
	// Always update when following, and also when history is prepended (logs grew
	// from the front, detectable because the previously-first entry moved).
	const frozenLogsRef = useRef(logs);
	const frozenFirstIdRef = useRef<string | undefined>(undefined);
	if (follow) {
		frozenLogsRef.current = logs;
		frozenFirstIdRef.current = logs[0]?.data.insertId;
	} else if (
		logs.length > 0 &&
		logs[0]?.data.insertId !== frozenFirstIdRef.current
	) {
		// First entry changed — history was prepended. Accept the update.
		frozenLogsRef.current = logs;
		frozenFirstIdRef.current = logs[0]?.data.insertId;
	}
	const displayedLogs = follow ? logs : frozenLogsRef.current;

	// When hasMore, index 0 is the sentinel row; real logs start at index 1.
	const sentinelOffset = hasMore ? 1 : 0;
	const totalCount = displayedLogs.length + sentinelOffset;

	useEffect(() => {
		if (
			follow &&
			!isLoading &&
			virtualizerRef.current &&
			displayedLogs.length > 0
		) {
			// https://github.com/TanStack/virtual/issues/537
			const rafId = requestAnimationFrame(() => {
				virtualizerRef.current?.scrollToIndex(totalCount - 1, {
					align: "end",
				});
			});
			return () => cancelAnimationFrame(rafId);
		}
	}, [totalCount, displayedLogs.length, follow, isLoading]);

	// After prepending older history, restore scroll position by adjusting for
	// the pixel height of the newly prepended rows so the viewport stays in place.
	const wasLoadingMoreRef = useRef(false);
	const scrollOffsetBeforeLoadRef = useRef(0);
	const prevTotalSizeRef = useRef(0);
	useEffect(() => {
		if (
			wasLoadingMoreRef.current &&
			!isLoadingMore &&
			displayedLogs.length > prevLogCountRef.current
		) {
			const rafId = requestAnimationFrame(() => {
				const virtualizer = virtualizerRef.current;
				if (!virtualizer) return;
				const newTotalSize = virtualizer.getTotalSize();
				const addedHeight = newTotalSize - prevTotalSizeRef.current;
				virtualizer.scrollToOffset(
					scrollOffsetBeforeLoadRef.current + addedHeight,
					{ align: "start" },
				);
			});
			return () => cancelAnimationFrame(rafId);
		}
		wasLoadingMoreRef.current = isLoadingMore;
	}, [isLoadingMore, displayedLogs.length]);

	useEffect(() => {
		if (logsRef) {
			logsRef.current = logs;
		}
	}, [logs, logsRef]);

	const handleScrollChange = useCallback(
		(instance: Virtualizer<HTMLDivElement, Element>) => {
			const isAtBottom =
				(instance.range?.endIndex ?? 0) >= totalCount - 1;
			if (isAtBottom) {
				return setFollow(true);
			}
			if (instance.scrollDirection === "backward") {
				setFollow(false);
				// Load more when the sentinel row comes into view.
				if (
					(instance.range?.startIndex ?? 1) === 0 &&
					hasMore &&
					!isLoadingMore
				) {
					prevLogCountRef.current = displayedLogs.length;
					scrollOffsetBeforeLoadRef.current =
						instance.scrollOffset ?? 0;
					prevTotalSizeRef.current = instance.getTotalSize();
					loadMoreHistory();
				}
			}
		},
		[
			totalCount,
			displayedLogs.length,
			hasMore,
			isLoadingMore,
			loadMoreHistory,
		],
	);

	if (isLoading) {
		return (
			<div className="h-full flex flex-col ">
				<ScrollArea
					className="w-full h-full"
					viewportProps={{ className: "p-2" }}
				>
					{SKELETON_KEYS.map((key) => (
						<Skeleton
							key={key}
							className="w-full h-6 mb-2 last:mb-0"
						/>
					))}
				</ScrollArea>
			</div>
		);
	}

	if (logs.length === 0) {
		if (error) {
			return (
				<div className="h-full flex-1 flex items-center justify-center">
					<div className="max-w-md flex flex-col items-center justify-center flex-1">
						<Icon
							icon={faTriangleExclamation}
							className="text-red-500 mb-2 text-2xl"
						/>
						<div className="text-center">
							<div className="mb-1">Failed to load logs.</div>
							<ErrorDetails error={error} className="text-sm" />
						</div>
					</div>
				</div>
			);
		}
		return (
			<div className="h-full flex flex-1 flex-col items-center justify-center">
				<p>No logs available.</p>
				<p className="text-muted-foreground text-xs mt-1">
					Logs will appear here as they stream in.
				</p>
			</div>
		);
	}

	return (
		<div className="h-full font-mono text-xs text-neutral-100 overflow-hidden flex flex-col">
			{streamError ? (
				<div className="flex items-center gap-2 px-4 py-2 bg-destructive/20 text-destructive-foreground text-xs border-b border-destructive/40 shrink-0">
					<Icon icon={faTriangleExclamation} className="shrink-0" />
					<span>Stream error: {streamError}</span>
				</div>
			) : null}
			<div className="relative flex-1 min-h-0">
				<VirtualScrollArea<LogRowData>
					virtualizerRef={virtualizerRef}
					viewportRef={viewportRef}
					onChange={handleScrollChange}
					count={totalCount}
					estimateSize={() => 24}
					className="w-full h-full"
					scrollerProps={{
						className: "w-full",
					}}
					viewportProps={{}}
					getRowData={(index) => {
						if (hasMore && index === 0) {
							return { isSentinel: true, isLoadingMore };
						}
						return {
							entry: displayedLogs[index - sentinelOffset],
							regionColumnWidth,
						};
					}}
					row={LogRow}
				/>
				{!follow ? (
					<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10">
						<button
							type="button"
							className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-primary text-primary-foreground text-xs font-sans font-medium shadow-lg hover:bg-primary/90 transition-colors"
							onClick={() => {
								setFollow(true);
								virtualizerRef.current?.scrollToIndex(
									totalCount - 1,
									{ align: "end" },
								);
							}}
						>
							<Icon icon={faArrowDown} className="size-3" />
							Back to newest
						</button>
					</div>
				) : null}
			</div>
		</div>
	);
}
