import {
	DndContext,
	type DragEndEvent,
	PointerSensor,
	closestCenter,
	useSensor,
	useSensors,
} from '@dnd-kit/core';
import {
	SortableContext,
	useSortable,
	verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useMutation, useQuery } from 'convex/react';
import {
	CaretDown,
	Gear,
	Hash,
	MicrophoneSlash,
	Monitor,
	Plus,
	SpeakerHigh,
	SpeakerSlash,
} from '@phosphor-icons/react';
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Avatar } from '@discord-clone/ui';
import { api } from '../../../../../convex/_generated/api';
import { useVoice } from '../../contexts/VoiceContext';
import { GuildHeaderDropdown } from './GuildHeaderDropdown';
import { ChannelListContextMenu } from './ChannelListContextMenu';
import { MobileServerActionsSheet } from './MobileServerActionsSheet';
import { useIsMobile } from '../../hooks/useIsMobile';
import { VoiceUserContextMenu } from '../voice/VoiceUserContextMenu';
import { MemberProfileModal } from '../member/MemberProfileModal';
import styles from './GuildNavbar.module.css';

type VoiceParticipant = {
	userId: string;
	username: string;
	avatarUrl?: string | null;
	isMuted?: boolean;
	isDeafened?: boolean;
	isScreenSharing?: boolean;
};

type ChannelDoc = {
	_id: string;
	name: string;
	type: string;
	categoryId?: string | null;
	position?: number | null;
};

type CategoryDoc = {
	_id: string;
	name: string;
	position: number;
};

export function GuildNavbar() {
	const navigate = useNavigate();
	const location = useLocation();
	const segments = location.pathname.split('/').filter(Boolean);
	const selectedChannelId = segments[0] === 'channels' ? segments[2] || null : null;
	const serverSettings = useQuery(api.serverSettings.get);
	const categoriesRaw = useQuery(api.categories.list);
	const channelsRaw = useQuery(api.channels.list);

	const voice = useVoice();
	const voiceStates: Record<string, VoiceParticipant[]> =
		((voice as any)?.voiceStates ?? {}) as Record<string, VoiceParticipant[]>;

	const reorderChannels = useMutation(api.channels.reorderChannels);
	const reorderCategories = useMutation(api.categories.reorder);
	const renameChannel = useMutation(api.channels.rename);
	const deleteChannel = useMutation(api.channels.remove);
	const renameCategory = useMutation(api.categories.rename);
	const deleteCategory = useMutation(api.categories.remove);

	type ContextMenuState =
		| {
				x: number;
				y: number;
				kind: 'channel';
				channelId: string;
				channelName: string;
				channelType: string;
		  }
		| {
				x: number;
				y: number;
				kind: 'category';
				categoryId: string;
				categoryName: string;
		  };
	const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);

	// ── Unread tracking ─────────────────────────────────────────────
	// The readState API stores lastReadTimestamp per (user, channel).
	// We compare that to the latest message timestamp per channel to
	// derive an unread flag. There is no mentionCount tracking on the
	// backend yet, so the mention pill is wired up but will only render
	// if a future version of the API starts returning one.
	const userId =
		typeof localStorage !== 'undefined' ? localStorage.getItem('userId') : null;
	const readStates = useQuery(
		api.readState.getAllReadStates,
		userId ? { userId: userId as any } : 'skip',
	);
	const channelIdList = useMemo(
		() => (channelsRaw ?? []).map((c: any) => c._id as any),
		[channelsRaw],
	);
	const latestTimestamps = useQuery(
		api.readState.getLatestMessageTimestamps,
		channelIdList.length > 0 ? { channelIds: channelIdList } : 'skip',
	);

	const unreadMap = useMemo(() => {
		const readMap = new Map<string, number>();
		for (const r of readStates ?? []) {
			readMap.set(r.channelId as unknown as string, r.lastReadTimestamp);
		}
		const latestMap = new Map<string, number>();
		for (const l of latestTimestamps ?? []) {
			latestMap.set(l.channelId as unknown as string, l.latestTimestamp);
		}
		const out = new Map<
			string,
			{ unreadCount: number; mentionCount: number }
		>();
		for (const [chId, latest] of latestMap) {
			const lastRead = readMap.get(chId) ?? 0;
			const hasUnread = latest > lastRead;
			out.set(chId, {
				unreadCount: hasUnread ? 1 : 0,
				mentionCount: 0,
			});
		}
		return out;
	}, [readStates, latestTimestamps]);

	const [collapsedCategories, setCollapsedCategories] = useState<Set<string>>(new Set());
	const headerRef = useRef<HTMLButtonElement>(null);
	const [headerDropdownRect, setHeaderDropdownRect] = useState<DOMRect | null>(null);
	const [mobileServerSheetOpen, setMobileServerSheetOpen] = useState(false);
	const [voiceMenu, setVoiceMenu] = useState<
		{ x: number; y: number; userId: string; username: string } | null
	>(null);
	const [voiceProfileFor, setVoiceProfileFor] = useState<string | null>(null);
	const isMobile = useIsMobile();

	const categories = useMemo<CategoryDoc[]>(
		() =>
			(categoriesRaw ?? [])
				.slice()
				.sort((a: any, b: any) => (a.position ?? 0) - (b.position ?? 0))
				.map((c: any) => ({ _id: c._id, name: c.name, position: c.position ?? 0 })),
		[categoriesRaw],
	);
	const channels = useMemo<ChannelDoc[]>(
		() =>
			(channelsRaw ?? [])
				.slice()
				.sort((a: any, b: any) => (a.position ?? 0) - (b.position ?? 0))
				.map((c: any) => ({
					_id: c._id,
					name: c.name,
					type: c.type,
					categoryId: c.categoryId ?? null,
					position: c.position ?? 0,
				})),
		[channelsRaw],
	);

	const serverName = serverSettings?.serverName || 'Server';

	const toggleCategory = (id: string) => {
		setCollapsedCategories((prev) => {
			const next = new Set(prev);
			if (next.has(id)) next.delete(id);
			else next.add(id);
			return next;
		});
	};

	// Alt+Left / Alt+Right collapse / expand the category containing the
	// currently selected channel. Matches the new UI's keyboard shortcut.
	useEffect(() => {
		const onKeyDown = (e: KeyboardEvent) => {
			if (!e.altKey) return;
			if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
			if (!selectedChannelId) return;
			const target = e.target as HTMLElement | null;
			if (
				target &&
				(target.tagName === 'INPUT' ||
					target.tagName === 'TEXTAREA' ||
					target.isContentEditable)
			) {
				return;
			}
			const channel = (channelsRaw ?? []).find(
				(c: any) => c._id === selectedChannelId,
			);
			const catId = channel?.categoryId;
			if (!catId) return;
			e.preventDefault();
			setCollapsedCategories((prev) => {
				const next = new Set(prev);
				const isCollapsed = next.has(catId);
				if (e.key === 'ArrowLeft' && !isCollapsed) {
					next.add(catId);
				} else if (e.key === 'ArrowRight' && isCollapsed) {
					next.delete(catId);
				}
				return next;
			});
		};
		window.addEventListener('keydown', onKeyDown);
		return () => window.removeEventListener('keydown', onKeyDown);
	}, [channelsRaw, selectedChannelId]);

	const handleChannelClick = (channelId: string) => {
		// Voice channels auto-connect on click (desktop) when the user
		// isn't already in that specific channel — matches the new UI.
		// Mobile still just navigates; the user has to tap Join on the
		// channel view to start capturing audio.
		if (!isMobile) {
			const channel = (channelsRaw ?? []).find((c: any) => c._id === channelId);
			if (
				channel?.type === 'voice' &&
				(voice as any)?.activeChannelId !== channelId
			) {
				const myId =
					typeof localStorage !== 'undefined'
						? localStorage.getItem('userId')
						: null;
				if (myId) {
					void (voice as any)?.connectToVoice?.(
						channelId,
						channel.name,
						myId,
						false,
					);
				}
			}
		}
		navigate(`/channels/home/${channelId}`);
	};

	const handleCreateChannelInCategory = (categoryId: string) => {
		window.dispatchEvent(
			new CustomEvent('brycord:open-create-channel', { detail: { categoryId } }),
		);
	};

	const handleOpenChannelSettings = (channelId: string) => {
		window.dispatchEvent(
			new CustomEvent('brycord:open-channel-settings', { detail: { channelId } }),
		);
	};

	const toggleHeader = () => {
		if (isMobile) {
			setMobileServerSheetOpen((v) => !v);
			return;
		}
		if (headerDropdownRect) {
			setHeaderDropdownRect(null);
			return;
		}
		const rect = headerRef.current?.getBoundingClientRect();
		if (rect) setHeaderDropdownRect(rect);
	};

	const uncategorized = channels.filter((c) => !c.categoryId);
	const byCategory = new Map<string, ChannelDoc[]>();
	for (const c of channels) {
		if (c.categoryId) {
			if (!byCategory.has(c.categoryId)) byCategory.set(c.categoryId, []);
			byCategory.get(c.categoryId)!.push(c);
		}
	}

	// Drag-and-drop: pointer sensor with 4px activation distance so plain
	// clicks still select a channel. Categories are in one sortable list;
	// each category's channel list + the uncategorized list are separate
	// sortable contexts.
	const sensors = useSensors(
		useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
	);

	const handleDragEndCategories = (event: DragEndEvent) => {
		const { active, over } = event;
		if (!over || active.id === over.id) return;
		const activeIdx = categories.findIndex((c) => `cat:${c._id}` === active.id);
		const overIdx = categories.findIndex((c) => `cat:${c._id}` === over.id);
		if (activeIdx === -1 || overIdx === -1) return;
		const next = categories.slice();
		const [moved] = next.splice(activeIdx, 1);
		next.splice(overIdx, 0, moved);
		const updates = next.map((c, i) => ({ id: c._id as any, position: i }));
		void reorderCategories({ updates });
	};

	const handleDragEndChannels = (
		event: DragEndEvent,
		categoryId: string | null,
	) => {
		const { active, over } = event;
		if (!over || active.id === over.id) return;
		const list = categoryId ? (byCategory.get(categoryId) ?? []) : uncategorized;
		const activeIdx = list.findIndex((c) => `ch:${c._id}` === active.id);
		const overIdx = list.findIndex((c) => `ch:${c._id}` === over.id);
		if (activeIdx === -1 || overIdx === -1) return;
		const next = list.slice();
		const [moved] = next.splice(activeIdx, 1);
		next.splice(overIdx, 0, moved);
		const updates = next.map((c, i) => ({
			id: c._id as any,
			categoryId: (categoryId ?? undefined) as any,
			position: i,
		}));
		void reorderChannels({ updates });
	};

	return (
		<div className={styles.navbar}>
			<button
				ref={headerRef}
				type="button"
				className={`${styles.header} ${headerDropdownRect ? styles.headerOpen : ''}`}
				onClick={toggleHeader}
			>
				<span className={styles.serverName}>{serverName}</span>
				<CaretDown
					size={18}
					weight="bold"
					className={`${styles.headerCaret} ${headerDropdownRect ? styles.headerCaretOpen : ''}`}
				/>
			</button>

			{headerDropdownRect && (
				<GuildHeaderDropdown
					anchorRect={headerDropdownRect}
					onClose={() => setHeaderDropdownRect(null)}
					onServerSettings={() => {
						window.dispatchEvent(new CustomEvent('brycord:open-server-settings'));
					}}
					onInviteMembers={() => {
						window.dispatchEvent(new CustomEvent('brycord:open-invite'));
					}}
					onCreateChannel={() => {
						window.dispatchEvent(new CustomEvent('brycord:open-create-channel'));
					}}
					onCreateCategory={() => {
						window.dispatchEvent(new CustomEvent('brycord:open-create-category'));
					}}
				/>
			)}

			<div className={styles.channelList}>
				<DndContext
					sensors={sensors}
					collisionDetection={closestCenter}
					onDragEnd={(e) => handleDragEndChannels(e, null)}
				>
					<SortableContext
						items={uncategorized.map((c) => `ch:${c._id}`)}
						strategy={verticalListSortingStrategy}
					>
						{uncategorized.map((ch) => {
							const unread = unreadMap.get(ch._id);
							const participants = voiceStates[ch._id] ?? [];
							return (
								<SortableChannelRow
									key={ch._id}
									id={ch._id}
									name={ch.name}
									type={ch.type}
									selected={selectedChannelId === ch._id}
									unreadCount={unread?.unreadCount ?? 0}
									mentionCount={unread?.mentionCount ?? 0}
									participants={participants}
									onClick={() => handleChannelClick(ch._id)}
									onOpenSettings={() => handleOpenChannelSettings(ch._id)}
									onVoiceUserContextMenu={(p, e) => {
										setVoiceMenu({
											x: e.clientX,
											y: e.clientY,
											userId: p.userId,
											username: p.username,
										});
									}}
									onContextMenu={(e) => {
										e.preventDefault();
										e.stopPropagation();
										setContextMenu({
											x: e.clientX,
											y: e.clientY,
											kind: 'channel',
											channelId: ch._id,
											channelName: ch.name,
											channelType: ch.type,
										});
									}}
								/>
							);
						})}
					</SortableContext>
				</DndContext>

				<DndContext
					sensors={sensors}
					collisionDetection={closestCenter}
					onDragEnd={handleDragEndCategories}
				>
					<SortableContext
						items={categories.map((c) => `cat:${c._id}`)}
						strategy={verticalListSortingStrategy}
					>
						{categories.map((category) => {
							const catChannels = byCategory.get(category._id) ?? [];
							const isCollapsed = collapsedCategories.has(category._id);
							// Collapsed categories still show the currently
							// selected channel row so the user never loses
							// their place. Matches the new UI.
							const visibleChannels = isCollapsed
								? catChannels.filter((c) => c._id === selectedChannelId)
								: catChannels;
							return (
								<SortableCategory
									key={category._id}
									id={category._id}
									name={category.name}
									collapsed={isCollapsed}
									onToggle={() => toggleCategory(category._id)}
									onAddChannel={() => handleCreateChannelInCategory(category._id)}
									onContextMenu={(e) => {
										e.preventDefault();
										e.stopPropagation();
										setContextMenu({
											x: e.clientX,
											y: e.clientY,
											kind: 'category',
											categoryId: category._id,
											categoryName: category.name,
										});
									}}
								>
									{visibleChannels.length > 0 && (
										<DndContext
											sensors={sensors}
											collisionDetection={closestCenter}
											onDragEnd={(e) => handleDragEndChannels(e, category._id)}
										>
											<SortableContext
												items={visibleChannels.map((c) => `ch:${c._id}`)}
												strategy={verticalListSortingStrategy}
											>
												{visibleChannels.map((ch) => {
													const unread = unreadMap.get(ch._id);
													const participants = voiceStates[ch._id] ?? [];
													return (
														<SortableChannelRow
															key={ch._id}
															id={ch._id}
															name={ch.name}
															type={ch.type}
															selected={selectedChannelId === ch._id}
															unreadCount={unread?.unreadCount ?? 0}
															mentionCount={unread?.mentionCount ?? 0}
															participants={participants}
															onClick={() => handleChannelClick(ch._id)}
															onOpenSettings={() =>
																handleOpenChannelSettings(ch._id)
															}
															onVoiceUserContextMenu={(p, e) => {
																setVoiceMenu({
																	x: e.clientX,
																	y: e.clientY,
																	userId: p.userId,
																	username: p.username,
																});
															}}
															onContextMenu={(e) => {
																e.preventDefault();
																e.stopPropagation();
																setContextMenu({
																	x: e.clientX,
																	y: e.clientY,
																	kind: 'channel',
																	channelId: ch._id,
																	channelName: ch.name,
																	channelType: ch.type,
																});
															}}
														/>
													);
												})}
											</SortableContext>
										</DndContext>
									)}
								</SortableCategory>
							);
						})}
					</SortableContext>
				</DndContext>
			</div>

			<MobileServerActionsSheet
				isOpen={mobileServerSheetOpen}
				onClose={() => setMobileServerSheetOpen(false)}
			/>

			{voiceMenu && (
				<VoiceUserContextMenu
					x={voiceMenu.x}
					y={voiceMenu.y}
					userId={voiceMenu.userId}
					username={voiceMenu.username}
					onViewProfile={() => {
						setVoiceProfileFor(voiceMenu.userId);
						setVoiceMenu(null);
					}}
					onClose={() => setVoiceMenu(null)}
				/>
			)}

			<MemberProfileModal
				isOpen={!!voiceProfileFor}
				onClose={() => setVoiceProfileFor(null)}
				member={voiceProfileFor ? { userId: voiceProfileFor } : null}
			/>

			{contextMenu && (
				<ChannelListContextMenu
					x={contextMenu.x}
					y={contextMenu.y}
					channelId={contextMenu.kind === 'channel' ? contextMenu.channelId : undefined}
					categoryId={
						contextMenu.kind === 'category' ? contextMenu.categoryId : undefined
					}
					channelTypeLabel={
						contextMenu.kind === 'channel' && contextMenu.channelType === 'voice'
							? 'Voice Channel'
							: 'Channel'
					}
					onCreateChannel={() =>
						window.dispatchEvent(new CustomEvent('brycord:open-create-channel'))
					}
					onCreateCategory={() =>
						window.dispatchEvent(new CustomEvent('brycord:open-create-category'))
					}
					onRenameChannel={
						contextMenu.kind === 'channel'
							? () => {
									const next = window.prompt(
										'Rename channel',
										contextMenu.channelName,
									);
									setContextMenu(null);
									if (next && next.trim() && next.trim() !== contextMenu.channelName) {
										void renameChannel({
											id: contextMenu.channelId as any,
											name: next.trim(),
										});
									}
								}
							: undefined
					}
					onDeleteChannel={
						contextMenu.kind === 'channel'
							? () => {
									const confirmed = window.confirm(
										`Delete #${contextMenu.channelName}? This cannot be undone.`,
									);
									setContextMenu(null);
									if (confirmed) {
										void deleteChannel({ id: contextMenu.channelId as any });
									}
								}
							: undefined
					}
					onRenameCategory={
						contextMenu.kind === 'category'
							? () => {
									const next = window.prompt(
										'Rename category',
										contextMenu.categoryName,
									);
									setContextMenu(null);
									if (next && next.trim() && next.trim() !== contextMenu.categoryName) {
										void renameCategory({
											id: contextMenu.categoryId as any,
											name: next.trim(),
										});
									}
								}
							: undefined
					}
					onDeleteCategory={
						contextMenu.kind === 'category'
							? () => {
									const confirmed = window.confirm(
										`Delete category "${contextMenu.categoryName}"? Channels in it will become uncategorized.`,
									);
									setContextMenu(null);
									if (confirmed) {
										void deleteCategory({ id: contextMenu.categoryId as any });
									}
								}
							: undefined
					}
					onClose={() => setContextMenu(null)}
				/>
			)}
		</div>
	);
}

interface SortableChannelRowProps {
	id: string;
	name: string;
	type: string;
	selected: boolean;
	unreadCount?: number;
	mentionCount?: number;
	participants?: VoiceParticipant[];
	onClick: () => void;
	onOpenSettings: () => void;
	onContextMenu?: (e: React.MouseEvent) => void;
	onVoiceUserContextMenu?: (
		participant: VoiceParticipant,
		e: React.MouseEvent,
	) => void;
}

function SortableChannelRow({
	id,
	name,
	type,
	selected,
	unreadCount = 0,
	mentionCount = 0,
	participants = [],
	onClick,
	onOpenSettings,
	onContextMenu,
	onVoiceUserContextMenu,
}: SortableChannelRowProps) {
	const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
		useSortable({ id: `ch:${id}` });
	const style = {
		transform: CSS.Transform.toString(transform),
		transition,
		opacity: isDragging ? 0.4 : 1,
	};
	const isVoice = type === 'voice';
	const hasUnread = unreadCount > 0 && !selected;
	const hasMention = mentionCount > 0;
	const showParticipants = isVoice && participants.length > 0;
	return (
		<>
			<div
				ref={setNodeRef}
				style={style}
				className={`${styles.channelItemWrapper} ${hasUnread ? styles.channelItemUnread : ''}`}
				data-selected={selected ? 'true' : undefined}
				data-unread={hasUnread ? 'true' : undefined}
				onContextMenu={onContextMenu}
				{...attributes}
				{...listeners}
			>
				<button
					type="button"
					className={`${styles.channelItem} ${selected ? styles.channelSelected : ''}`}
					onClick={onClick}
				>
					{isVoice ? (
						<SpeakerHigh size={20} className={styles.channelIcon} />
					) : (
						<Hash size={20} weight="bold" className={styles.channelIcon} />
					)}
					<span
						className={`${styles.channelName} ${hasUnread ? styles.channelUnread : ''}`}
					>
						{name}
					</span>
					{hasMention && (
						<span className={styles.mentionBadge}>
							{mentionCount > 99 ? '99+' : mentionCount}
						</span>
					)}
				</button>
				<button
					type="button"
					className={styles.channelSettingsGear}
					onClick={(e) => {
						e.stopPropagation();
						onOpenSettings();
					}}
					aria-label="Edit Channel"
					title="Edit Channel"
				>
					<Gear size={16} />
				</button>
			</div>
			{showParticipants && (
				<div
					className={styles.voiceUsers}
					onPointerDown={(e) => e.stopPropagation()}
				>
					{participants.map((p) => (
						<div
							key={p.userId}
							className={styles.voiceUser}
							title={p.username}
							onContextMenu={(e) => {
								if (!onVoiceUserContextMenu) return;
								e.preventDefault();
								e.stopPropagation();
								onVoiceUserContextMenu(p, e);
							}}
						>
							<Avatar
								src={p.avatarUrl ?? null}
								size={20}
								fallback={p.username}
								className={styles.voiceUserAvatar}
							/>
							<span className={styles.voiceUserName}>{p.username}</span>
							{p.isMuted && (
								<MicrophoneSlash size={12} weight="fill" />
							)}
							{p.isDeafened && (
								<SpeakerSlash size={12} weight="fill" />
							)}
							{p.isScreenSharing && <Monitor size={12} weight="fill" />}
						</div>
					))}
				</div>
			)}
		</>
	);
}

interface SortableCategoryProps {
	id: string;
	name: string;
	collapsed: boolean;
	onToggle: () => void;
	onAddChannel: () => void;
	onContextMenu?: (e: React.MouseEvent) => void;
	children?: ReactNode;
}

function SortableCategory({
	id,
	name,
	collapsed,
	onToggle,
	onAddChannel,
	onContextMenu,
	children,
}: SortableCategoryProps) {
	const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
		useSortable({ id: `cat:${id}` });
	const style = {
		transform: CSS.Transform.toString(transform),
		transition,
		opacity: isDragging ? 0.4 : 1,
	};
	return (
		<div ref={setNodeRef} style={style} className={styles.category}>
			<div
				className={styles.categoryRow}
				onContextMenu={onContextMenu}
				{...attributes}
				{...listeners}
			>
				<button type="button" className={styles.categoryHeader} onClick={onToggle}>
					<CaretDown
						size={12}
						weight="bold"
						className={`${styles.categoryChevron} ${
							collapsed ? styles.categoryChevronCollapsed : ''
						}`}
					/>
					<span className={styles.categoryName}>{name}</span>
				</button>
				<button
					type="button"
					className={styles.categoryAddButton}
					onClick={(e) => {
						e.stopPropagation();
						onAddChannel();
					}}
					aria-label="Create Channel"
					title="Create Channel"
				>
					<Plus size={16} weight="bold" />
				</button>
			</div>
			{children}
		</div>
	);
}
