/**
 * RolesView — full "Roles & Permissions" editor used inside
 * ServerSettingsModal. Takes over the whole settings surface: the
 * sidebar shows a back-to-settings button + create-role + role list
 * (not the tab list), and the main column shows the editor for the
 * selected role (display + toggles + permissions).
 *
 * Layout / visual language is ported from the Fluxer GuildRolesTab
 * screenshot the user shared, adapted to our Convex role schema:
 *   - Single server, no mentionable field in our schema (omitted)
 *   - Owner role is immutable and never shows here (filtered out)
 *   - Saves are direct `api.roles.update` calls — no explicit Save
 *     button / unsaved-changes banner; reactive queries update the
 *     other UI surfaces instantly.
 */
import { useMemo, useState, type ReactNode } from 'react';
import { useMutation, useQuery } from 'convex/react';
import {
	DndContext,
	type DragEndEvent,
	PointerSensor,
	closestCenter,
	useSensor,
	useSensors,
} from '@dnd-kit/core';
import {
	SortableContext,
	useSortable,
	verticalListSortingStrategy,
	arrayMove,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import {
	CaretLeft,
	CaretRight,
	MagnifyingGlass,
	Plus,
	Trash,
} from '@phosphor-icons/react';
import { api } from '../../../../../convex/_generated/api';
import type { Id } from '../../../../../convex/_generated/dataModel';
import styles from './RolesView.module.css';

interface RoleDoc {
	_id: Id<'roles'>;
	name: string;
	color: string;
	position?: number;
	permissions?: Record<string, boolean>;
	isHoist?: boolean;
}

interface UseRolesViewOptions {
	onBack: () => void;
}

interface RolesViewSlots {
	sidebar: ReactNode;
	content: ReactNode;
	/**
	 * Optional override for the modal's content header. When the
	 * user has uncommitted edits across any role, this renders a
	 * yellow "unsaved changes" banner with Reset / Save Changes
	 * buttons (replacing the default title + close button). When
	 * there are no drafts, it's `null` and the caller falls back
	 * to its default header row.
	 */
	header: ReactNode | null;
	hasChanges: boolean;
	/**
	 * Full-screen mobile role list — shown by `MobileServerSettings`
	 * instead of the desktop sidebar when `activeTab === 'roles'`
	 * and no role is yet selected. Tapping a row flips `selectedId`
	 * via the hook's own state, so the mobile caller only needs to
	 * check `selectedId` to decide whether to show this or the
	 * editor.
	 */
	mobileList: ReactNode;
	/** Currently selected role id (null if nothing selected). */
	selectedId: string | null;
	/** Clear the current selection — used by the mobile "Back to
	 *  Roles" control to pop back to the list view. */
	clearSelection: () => void;
}

/**
 * Pending edits for a single role. Only fields the user touched
 * are present; a field set to `undefined` (or missing) means "use
 * the server value". Saving walks the Map and only sends the
 * defined fields to `api.roles.update`.
 */
interface RoleDraft {
	name?: string;
	color?: string;
	isHoist?: boolean;
	permissions?: Record<string, boolean>;
}

interface PermissionMeta {
	key: string;
	label: string;
	description: string;
}

// Our backend currently ships a small permission set — labels and
// descriptions are local metadata, not part of the schema, so new
// permissions added server-side must also be added here to be
// editable in the UI.
const PERMISSIONS: PermissionMeta[] = [
	{
		key: 'manage_channels',
		label: 'Manage Channels',
		description: 'Create, edit, or delete channels and categories.',
	},
	{
		key: 'manage_roles',
		label: 'Manage Roles',
		description: 'Create, edit, or delete roles below your highest role.',
	},
	{
		key: 'manage_messages',
		label: 'Manage Messages',
		description: 'Delete messages by other members and pin any message.',
	},
	{
		key: 'create_invite',
		label: 'Create Invite',
		description: 'Invite other people to the server.',
	},
	{
		key: 'embed_links',
		label: 'Embed Links',
		description: 'Render link previews for URLs sent in messages.',
	},
	{
		key: 'attach_files',
		label: 'Attach Files',
		description: 'Upload images, videos, audio, and other files.',
	},
	{
		key: 'move_members',
		label: 'Move Members',
		description: 'Move members between voice channels.',
	},
	{
		key: 'mute_members',
		label: 'Mute Members',
		description: 'Server-mute other members in voice channels.',
	},
	{
		key: 'manage_nicknames',
		label: 'Manage Nicknames',
		description: 'Change the nicknames of other members.',
	},
];

const DEFAULT_COLOR = '#99aab5';

function isValidHex(value: string): boolean {
	return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
}

/**
 * Sortable sidebar row — wraps the visual role button with the
 * dnd-kit sortable bindings so pointer drags move the row in-place.
 * The click handler still fires for normal taps because the sensor
 * only activates after a 6px drag (configured on the parent).
 */
function SortableRoleRow({
	role,
	isActive,
	onClick,
}: {
	role: RoleDoc;
	isActive: boolean;
	onClick: () => void;
}) {
	const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
		useSortable({ id: role._id });
	const style = {
		transform: CSS.Transform.toString(transform),
		transition,
		opacity: isDragging ? 0.6 : undefined,
	};
	return (
		<button
			ref={setNodeRef}
			type="button"
			className={`${styles.roleItem} ${isActive ? styles.roleItemActive : ''}`}
			style={style}
			onClick={onClick}
			{...attributes}
			{...listeners}
		>
			<span
				className={styles.roleDot}
				style={{ background: role.color || DEFAULT_COLOR }}
			/>
			<span className={styles.roleName}>{role.name}</span>
		</button>
	);
}

/**
 * Hook — returns the two JSX slots the caller renders inside the
 * settings modal shell. Not a component because it returns a plain
 * object, which React components are not allowed to do.
 */
export function useRolesView({ onBack }: UseRolesViewOptions): RolesViewSlots {
	const allRoles = (useQuery(api.roles.list, {}) ?? []) as RoleDoc[];
	// Owner is permanently frozen — see convex/roles.ts. It's filtered
	// out here so the sidebar never shows it and the editor never
	// targets it. Backend mutations reject any Owner edit as a second
	// line of defence.
	//
	// The sidebar renders two groups:
	//   - `draggableRoles` — everything except Owner and @everyone, in
	//     descending position order. These are the rows the user can
	//     drag to reorder.
	//   - `everyoneRole` — pinned to the bottom of the list regardless
	//     of its stored position so it always reads as the fallback
	//     bucket.
	const { draggableRoles, everyoneRole } = useMemo(() => {
		const everyone = allRoles.find((r) => r.name === '@everyone') ?? null;
		const others = allRoles
			.filter((r) => r.name !== 'Owner' && r.name !== '@everyone')
			.sort((a, b) => (b.position ?? 0) - (a.position ?? 0));
		return { draggableRoles: others, everyoneRole: everyone };
	}, [allRoles]);

	// Flat list used by the editor lookup (by selected id). Keeps
	// the @everyone row targetable from the editor without dragging.
	const roles = useMemo(
		() => (everyoneRole ? [...draggableRoles, everyoneRole] : draggableRoles),
		[draggableRoles, everyoneRole],
	);

	const createRole = useMutation(api.roles.create);
	const updateRole = useMutation(api.roles.update);
	const removeRole = useMutation(api.roles.remove);
	const reorderRoles = useMutation(api.roles.reorder);

	// dnd-kit sensors — `distance: 6` means a 6px drag is required
	// before a drop is triggered, so a normal click still selects a
	// role without accidentally starting a drag.
	const sensors = useSensors(
		useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
	);

	const handleDragEnd = async (event: DragEndEvent) => {
		const { active, over } = event;
		if (!over || active.id === over.id) return;
		const oldIndex = draggableRoles.findIndex((r) => r._id === active.id);
		const newIndex = draggableRoles.findIndex((r) => r._id === over.id);
		if (oldIndex === -1 || newIndex === -1) return;
		const nextOrder = arrayMove(draggableRoles, oldIndex, newIndex);
		// Rewrite positions top-down. Higher array index = lower
		// position number, matching the descending-position sort
		// everywhere else. Leaves 10 unit gaps between rows so new
		// rows can be inserted later without a full renumber.
		const updates = nextOrder.map((r, idx) => ({
			id: r._id,
			position: (nextOrder.length - idx) * 10,
		}));
		setError(null);
		try {
			await reorderRoles({ updates });
		} catch (err: any) {
			setError(err?.message ?? 'Failed to reorder roles');
		}
	};

	const [selectedId, setSelectedId] = useState<Id<'roles'> | null>(null);
	const [permissionSearch, setPermissionSearch] = useState('');
	const [drafts, setDrafts] = useState<Map<string, RoleDraft>>(new Map());
	const [saving, setSaving] = useState(false);
	const [error, setError] = useState<string | null>(null);

	const selected = useMemo(
		() => roles.find((r) => r._id === selectedId) ?? null,
		[roles, selectedId],
	);

	/** Merge any pending draft for the given role on top of the
	 *  server state so the editor shows the in-progress values. */
	const getEffectiveRole = (role: RoleDoc): RoleDoc => {
		const draft = drafts.get(role._id);
		if (!draft) return role;
		return {
			...role,
			name: draft.name ?? role.name,
			color: draft.color ?? role.color,
			isHoist: draft.isHoist ?? role.isHoist,
			permissions: draft.permissions ?? role.permissions,
		};
	};

	const selectedWithDraft = selected ? getEffectiveRole(selected) : null;
	const hasChanges = drafts.size > 0;

	const updateDraft = (roleId: string, changes: Partial<RoleDraft>) => {
		setError(null);
		setDrafts((prev) => {
			const next = new Map(prev);
			const existing = next.get(roleId) ?? {};
			next.set(roleId, { ...existing, ...changes });
			return next;
		});
	};

	const handleCreate = async () => {
		setError(null);
		try {
			const created: any = await createRole({
				name: 'new role',
				color: DEFAULT_COLOR,
				permissions: {},
				isHoist: false,
				position: 0,
			});
			if (created?._id) setSelectedId(created._id);
		} catch (err: any) {
			setError(err?.message ?? 'Failed to create role');
		}
	};

	const handleDelete = async () => {
		if (!selected) return;
		if (!confirm(`Delete role "${selected.name}"?`)) return;
		setError(null);
		try {
			await removeRole({ id: selected._id });
			// Drop any pending draft for this role — it no longer exists.
			setDrafts((prev) => {
				const next = new Map(prev);
				next.delete(selected._id);
				return next;
			});
			setSelectedId(null);
		} catch (err: any) {
			setError(err?.message ?? 'Failed to delete role');
		}
	};

	const handleReset = () => {
		setDrafts(new Map());
		setError(null);
	};

	const handleSaveChanges = async () => {
		if (drafts.size === 0 || saving) return;
		setError(null);
		setSaving(true);
		try {
			// Persist every dirty role in parallel. For each draft we
			// only forward the fields that were actually touched — an
			// invalid hex colour is silently skipped so the user can
			// still save the rest of their edits.
			await Promise.all(
				Array.from(drafts.entries()).map(([id, draft]) => {
					const role = allRoles.find((r) => r._id === id);
					if (!role) return undefined;
					const updates: Record<string, unknown> = {
						id: id as Id<'roles'>,
					};
					if (draft.name !== undefined) {
						const clean = draft.name.trim();
						if (clean) updates.name = clean;
					}
					if (draft.color !== undefined && isValidHex(draft.color)) {
						updates.color = draft.color;
					}
					if (draft.isHoist !== undefined) {
						updates.isHoist = draft.isHoist;
					}
					if (draft.permissions !== undefined) {
						updates.permissions = draft.permissions;
					}
					return updateRole(updates as any);
				}),
			);
			setDrafts(new Map());
		} catch (err: any) {
			setError(err?.message ?? 'Failed to save changes');
		} finally {
			setSaving(false);
		}
	};

	const filteredPermissions = useMemo(() => {
		const q = permissionSearch.trim().toLowerCase();
		if (!q) return PERMISSIONS;
		return PERMISSIONS.filter(
			(p) =>
				p.label.toLowerCase().includes(q) ||
				p.description.toLowerCase().includes(q),
		);
	}, [permissionSearch]);

	const currentPerms = selectedWithDraft?.permissions ?? {};

	// Render the sidebar + main content. The caller is responsible for
	// wrapping these in the modal shell — RolesView returns a fragment
	// with two children via the `sidebar` / `content` render props.
	return {
		sidebar: (
			<div className={styles.sidebarInner}>
				<button type="button" className={styles.backButton} onClick={onBack}>
					<CaretLeft size={14} weight="bold" />
					Back to Settings
				</button>
				<div className={styles.sidebarSectionTitle}>Roles</div>
				<button
					type="button"
					className={styles.createButton}
					onClick={handleCreate}
				>
					<Plus size={14} weight="bold" />
					Create Role
				</button>
				<DndContext
					sensors={sensors}
					collisionDetection={closestCenter}
					onDragEnd={handleDragEnd}
				>
					<SortableContext
						items={draggableRoles.map((r) => r._id)}
						strategy={verticalListSortingStrategy}
					>
						<div className={styles.roleList}>
							{draggableRoles.length === 0 && !everyoneRole && (
								<div className={styles.emptyNote}>No custom roles yet.</div>
							)}
							{draggableRoles.map((role) => (
								<SortableRoleRow
									key={role._id}
									role={role}
									isActive={selectedId === role._id}
									onClick={() => setSelectedId(role._id)}
								/>
							))}
							{everyoneRole && (
								<button
									key={everyoneRole._id}
									type="button"
									className={`${styles.roleItem} ${selectedId === everyoneRole._id ? styles.roleItemActive : ''}`}
									onClick={() => setSelectedId(everyoneRole._id)}
								>
									<span
										className={styles.roleDot}
										style={{ background: everyoneRole.color || DEFAULT_COLOR }}
									/>
									<span className={styles.roleName}>{everyoneRole.name}</span>
								</button>
							)}
						</div>
					</SortableContext>
				</DndContext>
			</div>
		),
		content: !selected || !selectedWithDraft ? (
			<div className={styles.editorEmpty}>
				Select a role on the left to edit it, or create a new one.
			</div>
		) : (
			<>
				{error && <div className={styles.errorBanner}>{error}</div>}

				<div className={styles.headerRow}>
					<div className={styles.headerText}>
						<h2 className={styles.headerTitle}>
							Edit "{selectedWithDraft.name}"
						</h2>
						<p className={styles.headerSubtitle}>
							Configure role settings and permissions
						</p>
					</div>
					{selected.name !== '@everyone' && (
						<button
							type="button"
							className={styles.deleteButton}
							onClick={handleDelete}
						>
							<Trash size={14} weight="bold" />
							Delete Role
						</button>
					)}
				</div>

				<div className={styles.sectionHeading}>Display</div>
				<div className={styles.displayRow}>
					<div className={styles.fieldGroup}>
						<label className={styles.fieldLabel} htmlFor="role-name-input">
							Role Name
						</label>
						<input
							id="role-name-input"
							className={styles.textInput}
							type="text"
							value={selectedWithDraft.name}
							onChange={(e) =>
								updateDraft(selected._id, { name: e.target.value })
							}
							maxLength={100}
						/>
					</div>
					<div className={styles.fieldGroup}>
						<label className={styles.fieldLabel} htmlFor="role-color-input">
							Role Color
						</label>
						<div className={styles.colorWrap}>
							<input
								id="role-color-input"
								className={`${styles.textInput} ${styles.colorInput}`}
								type="text"
								value={selectedWithDraft.color || ''}
								onChange={(e) =>
									updateDraft(selected._id, { color: e.target.value })
								}
								placeholder="#99aab5"
							/>
							<button
								type="button"
								className={styles.colorSwatchButton}
								aria-label="Pick a color"
							>
								<span
									className={styles.colorSwatchInner}
									style={{
										background: isValidHex(selectedWithDraft.color || '')
											? selectedWithDraft.color
											: DEFAULT_COLOR,
									}}
								/>
								<input
									className={styles.hiddenColorInput}
									type="color"
									value={
										isValidHex(selectedWithDraft.color || '')
											? selectedWithDraft.color
											: DEFAULT_COLOR
									}
									onChange={(e) =>
										updateDraft(selected._id, { color: e.target.value })
									}
								/>
							</button>
						</div>
						<p className={styles.fieldHelp}>
							Type a color (hex) or use the picker.
						</p>
					</div>
				</div>

				<div className={styles.toggleRow}>
					<div className={styles.toggleText}>
						<span className={styles.toggleTitle}>
							Show this role separately
						</span>
						<span className={styles.toggleDescription}>
							Lists members with this role in their own section in the member
							list.
						</span>
					</div>
					<button
						type="button"
						className={`${styles.toggle} ${selectedWithDraft.isHoist ? styles.toggleOn : ''}`}
						onClick={() =>
							updateDraft(selected._id, {
								isHoist: !selectedWithDraft.isHoist,
							})
						}
						aria-pressed={!!selectedWithDraft.isHoist}
						aria-label="Show this role separately"
					>
						<span className={styles.toggleThumb} />
					</button>
				</div>

				<div className={styles.clearRow}>
					<p className={styles.clearText}>
						Use this button to quickly clear all permissions.
					</p>
					<button
						type="button"
						className={styles.clearButton}
						onClick={() => updateDraft(selected._id, { permissions: {} })}
					>
						Clear Permissions
					</button>
				</div>

				<div className={styles.sectionHeading}>Permissions</div>
				<div className={styles.searchRow}>
					<div className={styles.searchWrap}>
						<MagnifyingGlass
							className={styles.searchIcon}
							size={16}
							weight="regular"
						/>
						<input
							className={styles.searchInput}
							type="text"
							placeholder="Search Permissions..."
							value={permissionSearch}
							onChange={(e) => setPermissionSearch(e.target.value)}
						/>
					</div>
				</div>
				<div className={styles.permissionList}>
					{filteredPermissions.length === 0 ? (
						<div className={styles.permissionEmpty}>
							No permissions match your search.
						</div>
					) : (
						filteredPermissions.map((perm) => {
							const enabled = !!currentPerms[perm.key];
							return (
								<div key={perm.key} className={styles.permissionRow}>
									<div className={styles.permissionInfo}>
										<span className={styles.permissionTitle}>
											{perm.label}
										</span>
										<span className={styles.permissionDescription}>
											{perm.description}
										</span>
									</div>
									<button
										type="button"
										className={`${styles.toggle} ${enabled ? styles.toggleOn : ''}`}
										onClick={() =>
											updateDraft(selected._id, {
												permissions: { ...currentPerms, [perm.key]: !enabled },
											})
										}
										aria-pressed={enabled}
										aria-label={perm.label}
									>
										<span className={styles.toggleThumb} />
									</button>
								</div>
							);
						})
					)}
				</div>
			</>
		),
		header: hasChanges ? (
			<div className={styles.unsavedBar}>
				<span className={styles.unsavedText}>
					Careful! You have unsaved changes.
				</span>
				<div className={styles.unsavedActions}>
					<button
						type="button"
						className={styles.resetButton}
						onClick={handleReset}
						disabled={saving}
					>
						Reset
					</button>
					<button
						type="button"
						className={styles.saveButton}
						onClick={handleSaveChanges}
						disabled={saving}
					>
						{saving ? 'Saving…' : 'Save Changes'}
					</button>
				</div>
			</div>
		) : null,
		hasChanges,
		selectedId,
		clearSelection: () => setSelectedId(null),
		mobileList: (
			<div className={styles.mobileList}>
				{error && <div className={styles.errorBanner}>{error}</div>}
				<div className={styles.mobileListHeader}>
					<h3 className={styles.mobileListTitle}>Roles</h3>
					<button
						type="button"
						className={styles.mobileCreateButton}
						onClick={handleCreate}
					>
						<Plus size={14} weight="bold" />
						Create Role
					</button>
				</div>
				{draggableRoles.length === 0 && !everyoneRole && (
					<div className={styles.emptyNote}>No custom roles yet.</div>
				)}
				{draggableRoles.map((role) => (
					<button
						key={role._id}
						type="button"
						className={styles.mobileRoleRow}
						onClick={() => setSelectedId(role._id)}
					>
						<span
							className={styles.roleDot}
							style={{ background: role.color || DEFAULT_COLOR }}
						/>
						<span className={styles.mobileRoleRowName}>{role.name}</span>
						<CaretRight
							size={18}
							weight="bold"
							className={styles.mobileRoleRowCaret}
						/>
					</button>
				))}
				{everyoneRole && (
					<button
						type="button"
						className={styles.mobileRoleRow}
						onClick={() => setSelectedId(everyoneRole._id)}
					>
						<span
							className={styles.roleDot}
							style={{ background: everyoneRole.color || DEFAULT_COLOR }}
						/>
						<span className={styles.mobileRoleRowName}>
							{everyoneRole.name}
						</span>
						<CaretRight
							size={18}
							weight="bold"
							className={styles.mobileRoleRowCaret}
						/>
					</button>
				)}
			</div>
		),
	};
}
