import clsx from 'clsx';
import {
	ArrowLeftRight,
	CircleChevronUp,
	CirclePlus,
	ChevronDown,
	ChevronUp,
	Copy,
	Diff,
	Eye,
	FolderCode,
	GitMerge,
	GitPullRequest,
	GitPullRequestClosed,
	LoaderCircle,
	MessageSquarePlus,
	Pencil,
	RefreshCcwDot,
	Reply,
	Search,
	X,
} from 'lucide-solid';
import type { Did } from '@atcute/lexicons/syntax';
import { A, useNavigate, useParams, useSearchParams, useLocation } from '@solidjs/router';
import { createQuery, useQueryClient } from '../../lib/atproto-query';
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component, type JSX } from 'solid-js';
import { resolveActor } from '../../lib/api/identity';
import { createPull, createPullComment, fetchPullRoundPatch, getPull, listPullsPage, mergePull, setPullStatus } from '../../lib/api/pulls';
import { parseAtUri } from '../../lib/api/records';
import { compareBranches, getRepoBranches, type RepoContext, compareFork, listRepoRecords } from '../../lib/api/repos';
import { useAuth } from '../../lib/auth';
import { useIssuePreloader } from '../../lib/preloading';
import { Avatar, ErrorState, LoadingState, PaginationControls, StateBadge, ToggleButton, buttonStyles, cardStyles, textareaStyles } from '../../components/common';
import { AtUriPanel, BranchPill, DiffView, MarkdownBlock, PullComposeSkeleton, PullDiffSkeleton, PullDiffView, RepoListPageSkeleton, RepoListSkeleton, RepoThreadSkeleton, IssueOrPullRow, ThreadCommentView } from '../../components/repo';
import { RepoFrame, pullQueryKey, pullsQueryKey, useRepoQuery, pullPatchQueryKey } from './shared';
import { REPO_LIST_PAGE_LIMIT, formatRelativeTime, getErrorMessage, parseIntegerSearchParam, pullHref, uniqueCommenters, saveRecentIssueOrPull } from '../../lib/repo-utils';
import { buildPullFilterQuery, parsePastedPatchPrefill, parsePullFilter, type PullSourceMode, getRoundIndexForComment, buildPRCommentThreads } from './pulls-helpers';

export const PullsPage: Component = () => {
	const auth = useAuth();
	const repoQuery = useRepoQuery();
	const issuePreloader = useIssuePreloader();
	const [searchParams, setSearchParams] = useSearchParams();
	const filter = createMemo(() => parsePullFilter(searchParams.q));
	const stateFilter = createMemo(() => filter().state);
	const authorFilter = createMemo(() => filter().author);
	const textFilter = createMemo(() => filter().query);
	const filterQuery = createMemo(() => buildPullFilterQuery(filter()));
	const [filterDraft, setFilterDraft] = createSignal(filterQuery());
	const pageOffset = createMemo(() => parseIntegerSearchParam(searchParams.offset, 0, 0));
	const pageLimit = createMemo(() => parseIntegerSearchParam(searchParams.limit, REPO_LIST_PAGE_LIMIT, 1));
	const basePath = createMemo(() => {
		const repo = repoQuery.data;
		return repo ? `/${repo.owner.handle}/${repo.slug}/pulls` : '/';
	});
	createEffect(() => {
		setFilterDraft(filterQuery());
	});
	const setStateFilter = (state: 'open' | 'closed' | 'merged') => {
		setSearchParams({
			q: buildPullFilterQuery({ ...filter(), state }),
			offset: '0',
			limit: String(pageLimit()),
		});
	};
	const applyFilter = (event: SubmitEvent) => {
		event.preventDefault();
		setSearchParams({
			q: buildPullFilterQuery(parsePullFilter(filterDraft())),
			offset: '0',
			limit: String(pageLimit()),
		});
	};
	const resetFilter = () => {
		setSearchParams({
			q: 'state:open',
			offset: '0',
			limit: String(pageLimit()),
		});
	};

	const pullsQuery = createQuery(() => {
		const repo = repoQuery.data;
		return {
			queryKey: [...pullsQueryKey(repo?.repoDid ?? ''), stateFilter(), authorFilter(), textFilter(), pageOffset(), pageLimit()],
			enabled: Boolean(repo),
			queryFn: async () => {
				const author = authorFilter() ? (await resolveActor(authorFilter()!)).did : undefined;
				return listPullsPage(repo!, {
					state: stateFilter(),
					author,
					query: textFilter(),
					offset: pageOffset(),
					limit: pageLimit(),
				});
			},
		};
	});

	const activeCount = createMemo(() => {
		const data = pullsQuery.data;
		if (!data) return undefined;
		return data.totalCount === undefined ? `${pageOffset() + data.items.length}+` : String(data.totalCount);
	});
	const paginated = createMemo(() => pullsQuery.data?.items ?? []);

	return (
		<RepoFrame active="pulls" loadingFallback={<RepoListPageSkeleton kind="pulls" showCreate={Boolean(auth.agent())} />}>
			<div class="rounded bg-white p-4 dark:bg-gray-800">
				<div class="untangled-filter-grid">
					<div class="untangled-toggle-group">
						<ToggleButton
							active={stateFilter() === 'open'}
							onClick={() => setStateFilter('open')}
							label="open"
							meta={stateFilter() === 'open' ? activeCount() : undefined}
						/>
						<ToggleButton
							active={stateFilter() === 'merged'}
							onClick={() => setStateFilter('merged')}
							label="merged"
							meta={stateFilter() === 'merged' ? activeCount() : undefined}
						/>
						<ToggleButton
							active={stateFilter() === 'closed'}
							onClick={() => setStateFilter('closed')}
							label="closed"
							meta={stateFilter() === 'closed' ? activeCount() : undefined}
						/>
					</div>

					<form onSubmit={applyFilter} class="untangled-repo-filter-search">
						<input
							value={filterDraft()}
							onInput={(event) => setFilterDraft(event.currentTarget.value)}
							placeholder="search pulls..."
							class="untangled-repo-filter-input"
						/>
						<button type="button" class="untangled-repo-filter-reset" aria-label="reset filters" onClick={resetFilter}>
							<X class="size-4" />
						</button>
						<button type="submit" class="untangled-repo-filter-submit" aria-label="apply filters">
							<Search class="size-4" />
						</button>
					</form>

					<Show when={auth.agent() && repoQuery.data}>
						<A
							href={`/${repoQuery.data!.owner.handle}/${repoQuery.data!.slug}/pulls/new`}
							class={clsx(buttonStyles('primary'), 'col-start-3 justify-center no-underline hover:no-underline')}
						>
							<CirclePlus class="size-4" />
							new pull
						</A>
					</Show>
				</div>
			</div>

			<div class="mt-2">
				<Show
						when={!pullsQuery.isLoading}
						fallback={
							<RepoListSkeleton kind="pulls" />
						}
					>
					<Show
						when={!pullsQuery.error}
						fallback={
							<div class={cardStyles('p-6')}>
								<ErrorState message={getErrorMessage(pullsQuery.error)} />
							</div>
						}
					>
						<Show
							when={paginated().length > 0}
							fallback={
								<div class={cardStyles('px-6 py-4 text-sm text-gray-500 dark:text-gray-400')}>
									No {stateFilter()} pull requests
									<Show when={authorFilter()}>
										{' '}by {authorFilter()}
									</Show>
									.
								</div>
							}
						>
								<div class="flex flex-col gap-2">
									<div class="contents">
										<For each={paginated()}>
											{(pull) => (
												<IssueOrPullRow
													title={pull.value.title}
													number={pull.number || pull.rkey}
													state={pull.state}
													kind="pull"
													author={{ did: pull.author.did, handle: pull.author.handle }}
													createdAt={pull.value.createdAt}
													href={pullHref(repoQuery.data!, pull.number || pull.rkey)}
													{...issuePreloader(
														`${repoQuery.data!.owner.handle}/${repoQuery.data!.slug}`,
														'pull',
														pull.uri,
														pull.number || pull.rkey,
														repoQuery.data!,
													)}
													meta={
														<>
															<span>·</span>
															<span>{pull.value.rounds.length} round{pull.value.rounds.length === 1 ? '' : 's'}</span>
														</>
													}
												>
													<div class="mt-3 flex flex-wrap items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
														<span>targeting</span>
														<BranchPill name={pull.value.target.branch} />
														<span>from</span>
														<BranchPill name={pull.value.source?.branch ?? pull.value.target.branch} />
													</div>
												</IssueOrPullRow>
											)}
										</For>
								</div>
							</div>
							<Show when={repoQuery.data}>
								<PaginationControls
									basePath={basePath()}
									offset={pageOffset()}
									limit={pageLimit()}
									totalCount={pullsQuery.data?.totalCount}
									hasNext={pullsQuery.data?.hasNext}
									query={{ q: filterQuery() }}
								/>
							</Show>
						</Show>
					</Show>
				</Show>
			</div>
		</RepoFrame>
	);
};

export const NewPullPage: Component = () => {
	const auth = useAuth();
	const repoQuery = useRepoQuery();
	const navigate = useNavigate();
	const client = useQueryClient();
	const [sourceMode, setSourceMode] = createSignal<PullSourceMode>('branch');
	const [sourceBranch, setSourceBranch] = createSignal('');
	const [targetBranch, setTargetBranch] = createSignal('');
	const [patchText, setPatchText] = createSignal('');
	const [title, setTitle] = createSignal('');
	const [body, setBody] = createSignal('');
	const [touchedTitle, setTouchedTitle] = createSignal(false);
	const [touchedBody, setTouchedBody] = createSignal(false);
	const [previewingBody, setPreviewingBody] = createSignal(false);
	const [error, setError] = createSignal<string | null>(null);
	const [submitting, setSubmitting] = createSignal(false);

	const [selectedForkDid, setSelectedForkDid] = createSignal('');
	const [forkSourceBranch, setForkSourceBranch] = createSignal('');

	const viewerQuery = createQuery(() => {
		return {
			queryKey: ['viewer', auth.currentDid()],
			enabled: Boolean(auth.currentDid()),
			queryFn: async () => resolveActor(auth.currentDid()!),
		};
	});

	const myReposQuery = createQuery(() => {
		return {
			queryKey: ['myRepos', auth.currentDid()],
			enabled: Boolean(auth.currentDid() && sourceMode() === 'fork'),
			queryFn: async () => listRepoRecords(auth.currentDid()!),
		};
	});

	const forks = createMemo(() => {
		const repos = myReposQuery.data ?? [];
		const currentRepo = repoQuery.data;
		if (!currentRepo) return [];
		return repos.filter((r) => {
			const source = r.value.source;
			if (!source) return false;
			return source === currentRepo.repoDid || source === currentRepo.record.uri;
		});
	});

	const selectedForkRecord = createMemo(() => {
		return forks().find((f) => f.value.repoDid === selectedForkDid());
	});

	createEffect(() => {
		const availableForks = forks();
		if (availableForks.length > 0 && !selectedForkDid()) {
			setSelectedForkDid(availableForks[0].value.repoDid!);
		}
	});

	createEffect(() => {
		selectedForkDid();
		setForkSourceBranch('');
	});

	const selectedForkContext = createMemo<RepoContext | null>(() => {
		const forkRec = selectedForkRecord();
		const viewer = viewerQuery.data;
		if (!forkRec || !viewer) return null;
		return {
			owner: viewer,
			record: forkRec,
			repoDid: forkRec.value.repoDid!,
			slug: forkRec.value.name || forkRec.rkey,
			rkey: forkRec.rkey,
			knot: forkRec.value.knot,
		};
	});

	const forkBranchesQuery = createQuery(() => {
		const forkCtx = selectedForkContext();
		return {
			queryKey: ['branches', forkCtx?.repoDid],
			enabled: Boolean(sourceMode() === 'fork' && forkCtx),
			queryFn: async () => getRepoBranches(forkCtx!),
		};
	});

	createEffect(() => {
		const branches = forkBranchesQuery.data?.branches ?? [];
		if (branches.length === 0) return;
		if (!forkSourceBranch()) {
			const fallback = branches.find((branch) => branch.is_default)?.reference.name || branches[0].reference.name;
			setForkSourceBranch(fallback);
		}
	});

	const branchesQuery = createQuery(() => {
		const repo = repoQuery.data;
		return {
			queryKey: ['branches', repo?.repoDid],
			enabled: Boolean(repo),
			queryFn: async () => getRepoBranches(repo!),
		};
	});

	createEffect(() => {
		const branches = branchesQuery.data?.branches ?? [];
		if (branches.length === 0) return;

		if (!targetBranch()) {
			const fallback = branches.find((branch) => branch.is_default)?.reference.name || branches[0].reference.name;
			setTargetBranch(fallback);
		}

		if (!sourceBranch()) {
			const fallback =
				branches.find((branch) => branch.reference.name !== targetBranch())?.reference.name || branches[0].reference.name;
			setSourceBranch(fallback);
		}
	});

	const compareQuery = createQuery(() => {
		const repo = repoQuery.data;
		const source = sourceBranch();
		const target = targetBranch();
		const mode = sourceMode();
		return {
			queryKey: ['compare', repo?.repoDid, mode, target, source],
			enabled: Boolean(mode === 'branch' && repo && source && target && source !== target),
			queryFn: async () => compareBranches(repo!, target, source),
		};
	});

	const forkCompareQuery = createQuery(() => {
		const agent = auth.agent();
		const forkCtx = selectedForkContext();
		const target = targetBranch();
		const source = forkSourceBranch();
		const mode = sourceMode();
		return {
			queryKey: ['compare-fork', forkCtx?.repoDid, target, source],
			enabled: Boolean(mode === 'fork' && agent && forkCtx && target && source),
			queryFn: async () => compareFork(agent!, forkCtx!, target, source),
		};
	});

	createEffect(() => {
		if (sourceMode() !== 'branch') return;
		const comparison = compareQuery.data;
		if (!comparison || comparison.format_patch.length === 0) return;
		const patch = comparison.format_patch[0];
		if (!touchedTitle()) setTitle(patch.Title || '');
		if (!touchedBody()) setBody(patch.Body || '');
	});

	createEffect(() => {
		if (sourceMode() !== 'fork') return;
		const comparison = forkCompareQuery.data;
		if (!comparison || comparison.format_patch.length === 0) return;
		const patch = comparison.format_patch[0];
		if (!touchedTitle()) setTitle(patch.Title || '');
		if (!touchedBody()) setBody(patch.Body || '');
	});

	createEffect(() => {
		if (sourceMode() !== 'patch') return;
		const prefill = parsePastedPatchPrefill(patchText());
		if (!prefill.title) return;
		if (!touchedTitle()) setTitle(prefill.title);
		if (!touchedBody()) setBody(prefill.body);
	});

	const pastedPatch = createMemo(() => patchText().trim());
	const hasPastedPatch = createMemo(() => sourceMode() === 'patch' && Boolean(pastedPatch()));
	const activePatch = createMemo(() => {
		if (sourceMode() === 'patch') return pastedPatch();
		if (sourceMode() === 'fork') return forkCompareQuery.data?.patch ?? '';
		return compareQuery.data?.patch ?? '';
	});
	const hasComparison = createMemo(() => {
		if (sourceMode() === 'branch') return Boolean(compareQuery.data);
		if (sourceMode() === 'fork') return Boolean(forkCompareQuery.data);
		return false;
	});
	const commits = createMemo(() => {
		if (sourceMode() === 'branch') return compareQuery.data?.format_patch ?? [];
		if (sourceMode() === 'fork') return forkCompareQuery.data?.format_patch ?? [];
		return [];
	});
	const hasCommits = createMemo(() => commits().length > 0);
	const hasPatch = createMemo(() => Boolean(activePatch()));
	const showDetails = createMemo(() => hasCommits() || hasPatch());

	const submit = async (event: SubmitEvent) => {
		event.preventDefault();
		const agent = auth.agent();
		const repo = repoQuery.data;
		const patch = activePatch();
		const mode = sourceMode();

		if (!agent || !repo) {
			setError('Sign in first.');
			return;
		}

		if (!targetBranch()) {
			setError('Target branch required.');
			return;
		}

		if (mode === 'branch' && !sourceBranch()) {
			setError('Source branch required.');
			return;
		}

		if (mode === 'fork' && !selectedForkDid()) {
			setError('Fork selection required.');
			return;
		}

		if (mode === 'fork' && !forkSourceBranch()) {
			setError('Fork branch required.');
			return;
		}

		if (!patch) {
			setError(mode === 'patch' ? 'Paste a patch first.' : 'No patch available for the selected branches.');
			return;
		}

		if (!title().trim()) {
			setError('Title required.');
			return;
		}

		setSubmitting(true);
		setError(null);
		try {
			const created = await createPull(agent, repo, {
				title: title(),
				body: body(),
				sourceBranch: mode === 'patch' ? targetBranch() : (mode === 'fork' ? forkSourceBranch() : sourceBranch()),
				targetBranch: targetBranch(),
				patch,
				sourceRepoDid: mode === 'fork' ? (selectedForkDid() as Did) : undefined,
			});
			await client.invalidateQueries({ queryKey: pullsQueryKey(repo.repoDid) });
			navigate(pullHref(repo, created.rkey));
		} catch (cause) {
			setError(getErrorMessage(cause));
		} finally {
			setSubmitting(false);
		}
	};

	return (
		<RepoFrame active="pulls" loadingFallback={<PullComposeSkeleton />}>
			<div id="pr-compose-host" class="flex flex-col bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white">
				<form onSubmit={submit} class="flex flex-col gap-6">
					<section class="untangled-new-pr-step">
						<div class="untangled-new-pr-step-line untangled-new-pr-step-line-after" />
						<div class="untangled-new-pr-heading">
							<PullComposeSectionNumber value={1} />
							<h3 class="my-0 text-sm font-bold uppercase tracking-wide dark:text-white">source</h3>
						</div>
						<div class="ml-10 flex flex-col gap-3">
							<section class="flex flex-col gap-3">
								<div class="flex gap-1 p-1.5 rounded-md bg-slate-100 dark:bg-gray-900 border dark:border-gray-700 items-center justify-stretch">
									<PullSourceTab
										active={sourceMode() === 'branch'}
										title="Compare branches"
										description="Select a source branch"
										onClick={() => setSourceMode('branch')}
									/>
									<PullSourceTab
										active={sourceMode() === 'fork'}
										title="Compare forks"
										description="Select a fork and branch"
										onClick={() => setSourceMode('fork')}
									/>
									<PullSourceTab
										active={sourceMode() === 'patch'}
										title="Paste patch"
										description={
											<span>
												Paste a <code class="font-mono">git diff</code> or <code class="font-mono">git format-patch</code>
											</span>
										}
										onClick={() => setSourceMode('patch')}
									/>
								</div>

								<div class="flex flex-col md:flex-row md:flex-wrap gap-3">
									<div class="flex flex-col gap-1">
										<label for="targetBranch" class="text-xs uppercase tracking-wide text-gray-800 dark:text-gray-200">
											Merge into
										</label>
										<div class="flex flex-wrap gap-2 items-center">
											<select
												id="targetBranch"
												name="targetBranch"
												required
												value={targetBranch()}
												onChange={(event) => setTargetBranch(event.currentTarget.value)}
												class="peer p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
											>
												<option disabled selected={!targetBranch()}>
													Target branch
												</option>
												<For each={branchesQuery.data?.branches ?? []}>
													{(branch) => {
														const name = branch.reference.name;
														const isSource = sourceMode() === 'branch' && name === sourceBranch();
														return (
															<option value={name} disabled={isSource}>
																{name}
																{isSource ? ' (source)' : ''}
															</option>
														);
													}}
												</For>
											</select>
											<Show when={branchesQuery.isLoading}>
												<LoaderCircle class="size-4 animate-spin text-gray-500 dark:text-gray-400" />
											</Show>
										</div>
									</div>

									<Show when={sourceMode() === 'branch'}>
										<div id="patch-strategy" class="flex flex-col gap-1">
											<label for="sourceBranch" class="text-xs uppercase tracking-wide text-gray-800 dark:text-gray-200">
												Pull from
											</label>
											<div class="flex flex-wrap gap-2 items-center">
												<select
													id="sourceBranch"
													name="sourceBranch"
													required
													value={sourceBranch()}
													onChange={(event) => setSourceBranch(event.currentTarget.value)}
													class="peer p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
												>
													<option disabled selected={!sourceBranch()}>
														Source branch
													</option>
													<For each={branchesQuery.data?.branches ?? []}>
														{(branch, index) => {
															const name = branch.reference.name;
															const isTarget = name === targetBranch();
															return (
																<option value={name} disabled={isTarget}>
																	{name}
																	{index() === 0 ? ' (new)' : ''}
																	{isTarget ? ' (target)' : ''}
																</option>
															);
														}}
													</For>
												</select>
												<Show when={compareQuery.isFetching}>
													<LoaderCircle class="size-4 animate-spin text-gray-500 dark:text-gray-400" />
												</Show>
											</div>
										</div>
									</Show>

									<Show when={sourceMode() === 'fork'}>
										<div id="patch-strategy" class="flex flex-col gap-1">
											<label for="forkSelect" class="text-xs uppercase tracking-wide text-gray-800 dark:text-gray-200">
												Pull from
											</label>
											<div class="flex flex-wrap gap-2 items-center">
												<select
													id="forkSelect"
													name="fork"
													required
													value={selectedForkDid()}
													onChange={(event) => setSelectedForkDid(event.currentTarget.value)}
													class="peer p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
												>
													<option disabled selected={!selectedForkDid()}>
														Select a fork
													</option>
													<For each={forks()}>
														{(fork) => (
															<option value={fork.value.repoDid}>
																{viewerQuery.data?.handle ?? auth.currentDid()}/{fork.value.name || fork.rkey}
															</option>
														)}
													</For>
												</select>
												<Show when={myReposQuery.isLoading || viewerQuery.isLoading}>
													<LoaderCircle class="size-4 animate-spin text-gray-500 dark:text-gray-400" />
												</Show>
											</div>
										</div>

										<Show when={selectedForkDid()}>
											<div class="flex flex-col gap-1">
												<label for="forkSourceBranch" class="text-xs uppercase tracking-wide text-gray-800 dark:text-gray-200">
													Fork Branch
												</label>
												<div class="flex flex-wrap gap-2 items-center">
													<select
														id="forkSourceBranch"
														name="forkSourceBranch"
														required
														value={forkSourceBranch()}
														onChange={(event) => setForkSourceBranch(event.currentTarget.value)}
														class="peer p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
													>
														<option disabled selected={!forkSourceBranch()}>
															Source branch
														</option>
														<For each={forkBranchesQuery.data?.branches ?? []}>
															{(branch, index) => {
																const name = branch.reference.name;
																return (
																	<option value={name}>
																		{name}
																		{index() === 0 ? ' (new)' : ''}
																	</option>
																);
															}}
														</For>
													</select>
													<Show when={forkCompareQuery.isFetching || forkBranchesQuery.isLoading}>
														<LoaderCircle class="size-4 animate-spin text-gray-500 dark:text-gray-400" />
													</Show>
												</div>
											</div>
										</Show>
									</Show>
								</div>

								<Show when={sourceMode() === 'fork' && forks().length === 0 && !myReposQuery.isLoading && !viewerQuery.isLoading}>
									<div class="text-sm text-gray-500 dark:text-gray-400">
										No forks found for this repository.
									</div>
								</Show>

								<Show when={sourceMode() === 'patch'}>
									<div id="patch-upload">
										<div class="flex items-center justify-between gap-2">
											<p class="my-0">
												You can paste a <code>git diff</code> or a <code>git format-patch</code> patch series here.
											</p>
										</div>
										<textarea
											name="patch"
											id="patch"
											rows="12"
											value={patchText()}
											onInput={(event) => setPatchText(event.currentTarget.value)}
											class="w-full mt-2 resize-y font-mono dark:bg-gray-800 dark:text-white dark:border-gray-700"
											placeholder={`diff --git a/file.txt b/file.txt
index 1234567..abcdefg 100644
--- a/file.txt
+++ b/file.txt`}
										/>
									</div>
								</Show>

								<Show when={branchesQuery.error}>
									<div id="patch-error" class="error dark:text-red-300">
										{getErrorMessage(branchesQuery.error)}
									</div>
								</Show>
							</section>
						</div>
					</section>

					<section class="untangled-new-pr-step">
						<div
							class={clsx(
								'untangled-new-pr-step-line untangled-new-pr-step-line-before',
								showDetails() ? 'untangled-new-pr-step-line-through' : 'untangled-new-pr-step-line-fade',
							)}
						/>
						<div class="untangled-new-pr-heading">
							<PullComposeSectionNumber value={2} />
							<h3 class="my-0 text-sm font-bold uppercase tracking-wide dark:text-white">review</h3>
						</div>
						<div class="ml-10 flex flex-col gap-3">
							<section class="flex flex-col gap-3">
								<Switch>
									<Match when={sourceMode() === 'branch' && compareQuery.isLoading}>
										<div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800">
											<LoadingState label="Generating comparison..." />
										</div>
									</Match>
									<Match when={sourceMode() === 'branch' && compareQuery.error}>
										<div class="p-4 border border-red-200 dark:border-red-800 rounded bg-red-50 dark:bg-gray-800">
											<ErrorState message={getErrorMessage(compareQuery.error)} />
										</div>
									</Match>
									<Match when={sourceMode() === 'fork' && forkCompareQuery.isLoading}>
										<div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800">
											<LoadingState label="Generating comparison..." />
										</div>
									</Match>
									<Match when={sourceMode() === 'fork' && forkCompareQuery.error}>
										<div class="p-4 border border-red-200 dark:border-red-800 rounded bg-red-50 dark:bg-gray-800">
											<ErrorState message={getErrorMessage(forkCompareQuery.error)} />
										</div>
									</Match>
									<Match when={sourceMode() === 'patch' && !hasPastedPatch()}>
										<div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
											Paste a patch above to see a comparison.
										</div>
									</Match>
									<Match when={sourceMode() === 'patch' && hasPastedPatch()}>
										<div class="flex items-center justify-between gap-3 min-w-0 text-sm text-gray-500 dark:text-gray-400">
											<span class="flex-shrink-0">Pasted patch</span>
											<span class="inline-flex items-center gap-2 font-mono text-gray-600 dark:text-gray-300 truncate">
												<span>{targetBranch()}</span>
											</span>
										</div>
									</Match>
									<Match when={sourceMode() === 'branch' && (!sourceBranch() || !targetBranch() || sourceBranch() === targetBranch())}>
										<div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
											Pick a source and target above to see a comparison.
										</div>
									</Match>
									<Match when={sourceMode() === 'fork' && (!selectedForkDid() || !forkSourceBranch())}>
										<div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
											Pick a fork and source branch above to see a comparison.
										</div>
									</Match>
									<Match when={sourceMode() === 'branch' && hasComparison() && !hasCommits()}>
										<div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
											No commits between target and source. Make sure your source branch has commits not on the target.
										</div>
									</Match>
									<Match when={sourceMode() === 'fork' && hasComparison() && !hasCommits()}>
										<div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
											No commits between target and source. Make sure your source branch has commits not on the target.
										</div>
									</Match>
									<Match when={(sourceMode() === 'branch' || sourceMode() === 'fork') && hasCommits()}>
										<div class="flex flex-col gap-2">
											<div class="flex items-center justify-between gap-3 min-w-0 text-sm text-gray-500 dark:text-gray-400">
												<span class="flex-shrink-0">
													{commits().length} commit{commits().length === 1 ? '' : 's'}
												</span>
												<span class="inline-flex items-center gap-2 font-mono text-gray-600 dark:text-gray-300 truncate">
													<span>{targetBranch()}</span>
													<ArrowLeftRight class="w-4 h-4 flex-shrink-0" />
													<span>
														{sourceMode() === 'fork' ? `${viewerQuery.data?.handle ?? auth.currentDid()}/${selectedForkRecord()?.value.name || selectedForkRecord()?.rkey}:${forkSourceBranch()}` : sourceBranch()}
													</span>
												</span>
											</div>

											<ul class="flex flex-col gap-2">
												<For each={commits()}>
													{(patch) => (
														<li class="border border-gray-200 dark:border-gray-700 rounded bg-white dark:bg-gray-800 px-3 py-2 flex items-center gap-3">
															<span class="text-gray-700 dark:text-gray-300 flex-shrink-0">
																<GitPullRequest class="w-4 h-4" />
															</span>
															<span class="flex-1 min-w-0 truncate text-sm dark:text-gray-200">{patch.Title}</span>
															<span class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">
																<Show when={patch.AuthorDate}>
																	<span>{formatRelativeTime(patch.AuthorDate)}</span>
																</Show>
																<Show when={patch.SHA}>
																	<span class="font-mono bg-gray-100 dark:bg-gray-900 text-gray-700 dark:text-gray-300 px-2 py-0.5 rounded">
																		{patch.SHA.slice(0, 8)}
																	</span>
																</Show>
															</span>
															<Show when={patch.SHA}>
																<span class="-my-2 self-stretch w-px bg-gray-200 dark:bg-gray-700" />
																<button
																	type="button"
																	class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-gray-700 dark:text-gray-300"
																	title="Copy SHA"
																	onClick={() => void navigator.clipboard?.writeText(patch.SHA)}
																>
																	<Copy class="w-4 h-4" />
																</button>
																<Show when={repoQuery.data}>
																	{(repo) => (
																		<A
																			href={`/${repo().owner.handle}/${repo().slug}/tree/${patch.SHA}`}
																			class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-gray-700 dark:text-gray-300"
																			title="Browse repository at this commit"
																		>
																			<FolderCode class="w-4 h-4" />
																		</A>
																	)}
																</Show>
															</Show>
														</li>
													)}
												</For>
											</ul>
										</div>
									</Match>
								</Switch>

								<Show when={hasPatch()}>
									<div class="untangled-new-pr-diff">
										<DiffView patch={activePatch()} wrap maxHeight="40rem" />
									</div>
								</Show>
							</section>
						</div>
					</section>

					<Show when={showDetails()}>
						<section class="untangled-new-pr-step">
							<div class="untangled-new-pr-step-line untangled-new-pr-step-line-before untangled-new-pr-step-line-fade" />
							<div class="untangled-new-pr-heading">
								<PullComposeSectionNumber value={3} />
								<h3 class="my-0 text-sm font-bold uppercase tracking-wide dark:text-white">details</h3>
							</div>
							<div class="ml-10 flex flex-col gap-3">
								<section class="flex flex-col md:flex-row gap-6">
									<div class="flex-1 min-w-0 flex flex-col gap-4">
										<div class="flex flex-col gap-1">
											<label for="title" class="text-xs uppercase tracking-wide text-gray-800 dark:text-gray-200">
												title
											</label>
											<input
												type="text"
												name="title"
												id="title"
												value={title()}
												onInput={(event) => {
													setTouchedTitle(true);
													setTitle(event.currentTarget.value);
												}}
												class="w-full dark:bg-gray-800 dark:text-white dark:border-gray-700"
												placeholder="One-line summary of your change."
											/>
										</div>

										<div class="flex flex-col gap-2" data-md-editor="pull-body">
											<div class="btn-group self-start text-gray-600 dark:text-gray-300">
												<button
													type="button"
													class={clsx('btn-group-item group', !previewingBody() && 'active')}
													onClick={() => setPreviewingBody(false)}
												>
													<Pencil class="w-3.5 h-3.5" />
													Write
												</button>
												<button
													type="button"
													class={clsx('btn-group-item group', previewingBody() && 'active')}
													onClick={() => setPreviewingBody(true)}
												>
													<Eye class="w-3.5 h-3.5" />
													Preview
												</button>
											</div>
											<Show
												when={previewingBody()}
												fallback={
													<textarea
														id="pull-body"
														name="body"
														rows="6"
														value={body()}
														onInput={(event) => {
															setTouchedBody(true);
															setBody(event.currentTarget.value);
														}}
														class="w-full resize-y dark:bg-gray-800 dark:text-white dark:border-gray-700"
														placeholder="Describe your change. Markdown is supported."
													/>
												}
											>
										<div class="min-h-[6rem] p-3 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-900">
													<Show
														when={body().trim()}
														fallback={<span class="text-gray-400 dark:text-gray-500 italic">Nothing to preview.</span>}
													>
														<MarkdownBlock markdown={body()} />
													</Show>
												</div>
											</Show>
										</div>

										<div class="flex items-center justify-end gap-4 mt-auto">
											<button
												type="submit"
												class={clsx(buttonStyles('primary'), 'justify-center min-w-48')}
												disabled={submitting()}
											>
												<GitPullRequest class="w-4 h-4" />
												<Show when={submitting()} fallback="Create pull request">
													<span class="inline-flex items-center gap-2">
														Creating
														<LoaderCircle class="w-4 h-4 animate-spin" />
													</span>
												</Show>
											</button>
										</div>
									</div>
								</section>
							</div>
						</section>
					</Show>
				</form>

				<Show when={error()}>
					<div id="pull" class="error dark:text-red-300 mt-4 ml-10">
						{error()}
					</div>
				</Show>
			</div>
		</RepoFrame>
	);
};

const PullComposeSectionNumber: Component<{ value: number }> = (props) => (
	<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300 text-xs font-bold">
		{props.value}
	</span>
);

const PullSourceTab: Component<{
	active: boolean;
	title: string;
	description: JSX.Element;
	onClick: () => void;
}> = (props) => (
	<button
		type="button"
		class={clsx(
			'group flex-1 p-3 text-left hover:no-underline flex flex-col gap-1 rounded',
			props.active
				? 'bg-white dark:bg-gray-800 shadow-sm cursor-default border border-gray-200 dark:border-gray-700'
				: 'bg-transparent hover:bg-white/50 dark:hover:bg-gray-800/50',
		)}
		onClick={props.onClick}
	>
		<span class="font-medium text-sm dark:text-white flex items-center gap-2">{props.title}</span>
		<span class="text-xs text-gray-500 dark:text-gray-400">{props.description}</span>
	</button>
);

const updatePullStatusData = (
	data: unknown,
	pullUri: string,
	state: 'open' | 'closed' | 'merged',
): unknown => {
	if (!data || typeof data !== 'object') return data;

	if ('pull' in data) {
		const detail = data as { pull?: { uri?: string; state?: string } };
		if (detail.pull?.uri === pullUri) {
			return {
				...detail,
				pull: {
					...detail.pull,
					state,
				},
			};
		}
	}

	if ('items' in data) {
		const page = data as { items?: Array<{ uri?: string; state?: string }> };
		if (Array.isArray(page.items)) {
			return {
				...page,
				items: page.items.map((pull) => (pull.uri === pullUri ? { ...pull, state } : pull)),
			};
		}
	}

	return data;
};



export const PullPage: Component = () => {
	const auth = useAuth();
	const repoQuery = useRepoQuery();
	const params = useParams();
	const client = useQueryClient();
	const location = useLocation();
	const pullRef = createMemo(() => params.pullRef ?? '');
	const [roundIndex, setRoundIndex] = createSignal(0);
	const [comment, setComment] = createSignal('');
	const [composerOpen, setComposerOpen] = createSignal(false);
	const [workingAction, setWorkingAction] = createSignal<'comment' | 'state' | 'merge' | null>(null);
	const working = createMemo(() => workingAction() !== null);
	const [error, setError] = createSignal<string | null>(null);
	const [mobileDrawerOpen, setMobileDrawerOpen] = createSignal(false);

	const pullQuery = createQuery(() => {
		const repo = repoQuery.data;
		return {
			queryKey: pullQueryKey(repo?.repoDid ?? '', pullRef()),
			enabled: Boolean(repo && pullRef()),
			queryFn: async () => getPull(repo!, pullRef()),
		};
	});

	const commentsByRound = createMemo(() => {
		const detailData = pullQuery.data;
		if (!detailData) return {};
		const rounds = detailData.pull.value.rounds || [];
		const groups: Record<number, typeof detailData.comments> = {};
		for (let i = 0; i < rounds.length; i++) {
			groups[i] = [];
		}
		for (const comment of detailData.comments) {
			const roundVal = (comment.value as any).pullRoundIdx ?? (comment.value as any).round;
			const roundIdx = typeof roundVal === 'number'
				? roundVal
				: getRoundIndexForComment(comment.value.createdAt, rounds);
			if (!groups[roundIdx]) {
				groups[roundIdx] = [];
			}
			groups[roundIdx].push(comment);
		}
		return groups;
	});


	createEffect(() => {
		const rounds = pullQuery.data?.pull.value.rounds.length ?? 0;
		if (rounds > 0) {
			setRoundIndex(rounds - 1);
		}
	});

	createEffect(() => {
		const repo = repoQuery.data;
		const detail = pullQuery.data;
		if (repo && detail) {
			saveRecentIssueOrPull(repo.owner.handle, repo.slug, {
				uri: detail.pull.uri,
				kind: 'pull',
				title: detail.pull.value.title,
				number: detail.pull.number,
				state: detail.pull.state,
				author: detail.pull.author.handle,
			});
		}
	});

	createEffect(() => {
		const hash = location.hash;
		const cleanHash = hash.startsWith('#') ? hash.slice(1) : hash;
		if (cleanHash && cleanHash.startsWith('comment-') && pullQuery.data?.comments) {
			setTimeout(() => {
				const element = document.getElementById(cleanHash);
				if (element) {
					const details = element.closest('details');
					if (details) {
						details.open = true;
					}
					// Remove highlight from any other elements first
					document.querySelectorAll('.untangled-comment-highlight').forEach((el) => {
						el.classList.remove('untangled-comment-highlight');
					});
					element.scrollIntoView({ behavior: 'smooth', block: 'center' });
					element.classList.add('untangled-comment-highlight');
				}
			}, 150);
		}
	});

	const patchQuery = createQuery(() => {
		const detail = pullQuery.data;
		return {
			queryKey: pullPatchQueryKey(detail?.pull.uri ?? '', roundIndex()),
			enabled: Boolean(detail),
			queryFn: async () => fetchPullRoundPatch(detail!.pull, roundIndex()),
		};
	});

	const invalidate = async (repo: RepoContext, pullRef: string, pullUri: string) => {
		await client.invalidateQueries({ queryKey: pullsQueryKey(repo.repoDid) });
		await client.invalidateQueries({ queryKey: pullQueryKey(repo.repoDid, pullRef) });
		await client.invalidateQueries({ queryKey: pullQueryKey(repo.repoDid, parseAtUri(pullUri).rkey) });
	};

	const updatePullStatusCaches = (
		repo: RepoContext,
		currentPullRef: string,
		pullUri: string,
		state: 'open' | 'closed' | 'merged',
	) => {
		client.setQueryData(pullQueryKey(repo.repoDid, currentPullRef), (data: unknown) =>
			updatePullStatusData(data, pullUri, state),
		);
		client.setQueryData(pullQueryKey(repo.repoDid, parseAtUri(pullUri).rkey), (data: unknown) =>
			updatePullStatusData(data, pullUri, state),
		);
		client.setQueriesData<unknown>({ queryKey: pullsQueryKey(repo.repoDid) }, (data: unknown) =>
			updatePullStatusData(data, pullUri, state),
		);
	};

	const markPullQueriesStale = async (repo: RepoContext, currentPullRef: string, pullUri: string) => {
		await client.invalidateQueries({ queryKey: pullsQueryKey(repo.repoDid), refetchType: 'none' });
		await client.invalidateQueries({ queryKey: pullQueryKey(repo.repoDid, currentPullRef), refetchType: 'none' });
		await client.invalidateQueries({ queryKey: pullQueryKey(repo.repoDid, parseAtUri(pullUri).rkey), refetchType: 'none' });
	};

	const toggleState = async () => {
		const agent = auth.agent();
		const repo = repoQuery.data;
		const detail = pullQuery.data;
		if (!agent || !repo || !detail) {
			return;
		}

		const nextState = detail.pull.state === 'open' ? 'closed' : 'open';
		setWorkingAction('state');
		setError(null);
		try {
			await setPullStatus(agent, detail.pull.uri, nextState);
			updatePullStatusCaches(repo, pullRef(), detail.pull.uri, nextState);
			await markPullQueriesStale(repo, pullRef(), detail.pull.uri);
		} catch (cause) {
			setError(getErrorMessage(cause));
		} finally {
			setWorkingAction(null);
		}
	};

	const mergeCurrentPull = async () => {
		const agent = auth.agent();
		const repo = repoQuery.data;
		const detail = pullQuery.data;
		if (!agent || !repo || !detail || detail.pull.state !== 'open') {
			return;
		}

		const latestRoundIndex = detail.pull.value.rounds.length - 1;
		if (latestRoundIndex < 0) {
			setError('No pull request patch is available to merge');
			return;
		}

		const confirmed = window.confirm(
			`Are you sure you want to merge pull #${detail.pull.number || detail.pull.rkey} into the ${detail.pull.value.target.branch} branch?`,
		);
		if (!confirmed) return;

		setWorkingAction('merge');
		setError(null);
		try {
			const patch =
				roundIndex() === latestRoundIndex && patchQuery.data
					? patchQuery.data
					: await fetchPullRoundPatch(detail.pull, latestRoundIndex);
			await mergePull(agent, repo, {
				pull: detail.pull,
				patch,
			});
			updatePullStatusCaches(repo, pullRef(), detail.pull.uri, 'merged');
			await markPullQueriesStale(repo, pullRef(), detail.pull.uri);
			await client.invalidateQueries({ queryKey: ['repo-tree', repo.repoDid] });
			await client.invalidateQueries({ queryKey: ['repo-log', repo.repoDid] });
			await client.invalidateQueries({ queryKey: ['repo-branches', repo.repoDid] });
			await client.invalidateQueries({ queryKey: ['repo-languages', repo.repoDid] });
		} catch (cause) {
			setError(getErrorMessage(cause));
		} finally {
			setWorkingAction(null);
		}
	};

	const addComment = async (event: SubmitEvent) => {
		event.preventDefault();
		const agent = auth.agent();
		const repo = repoQuery.data;
		const detail = pullQuery.data;
		if (!agent || !repo || !detail || !comment().trim()) {
			return;
		}

		setWorkingAction('comment');
		setError(null);
		try {
			await createPullComment(
				agent,
				detail.pull.uri,
				detail.pull.cid ?? '',
				comment(),
				detail.pull.value.rounds.length - 1,
			);
			setComment('');
			setComposerOpen(false);
			await invalidate(repo, pullRef(), detail.pull.uri);
		} catch (cause) {
			setError(getErrorMessage(cause));
		} finally {
			setWorkingAction(null);
		}
	};

	const participants = createMemo(() => {
		const detail = pullQuery.data;
		if (!detail) return [];
		const seen = new Set<string>();
		const result: Array<{ did: string; handle: string }> = [];
		const add = (participant: { did: string; handle: string }) => {
			if (seen.has(participant.did)) return;
			seen.add(participant.did);
			result.push(participant);
		};
		add(detail.pull.author);
		for (const commenter of uniqueCommenters(detail.comments)) add(commenter);
		return result;
	});

	const roundLabel = createMemo(() => `round #${roundIndex()}`);

	const statusEvent = createMemo(() => {
		const state = pullQuery.data?.pull.state;
		if (state === 'merged') {
			return {
				icon: <GitMerge class="size-4" />,
				text: 'pull request successfully merged',
				class: 'untangled-pr-history-event-merged',
			};
		}
		if (state === 'closed') {
			return {
				icon: <GitPullRequestClosed class="size-4" />,
				text: 'pull request closed',
				class: 'untangled-pr-history-event-closed',
			};
		}
		return {
			icon: <GitPullRequest class="size-4" />,
			text: 'pull request opened',
			class: 'untangled-pr-history-event-open',
		};
	});

	return (
		<RepoFrame active="pulls" loadingFallback={<RepoThreadSkeleton kind="pull" />}>
			<Switch>
				<Match when={pullQuery.isLoading}>
					<RepoThreadSkeleton kind="pull" />
				</Match>
				<Match when={pullQuery.error}>
					<div class={cardStyles('p-6')}>
						<ErrorState message={getErrorMessage(pullQuery.error)} />
					</div>
				</Match>
				<Match when={pullQuery.data}>
					{(detail) => (
						<div class="untangled-pr-detail">
							<div class="untangled-pr-header-grid">
								<section class="untangled-pr-summary-card">
									<header>
											<h1 class="m-0 text-2xl">
												{detail().pull.value.title}{' '}
												<span class="text-gray-500 dark:text-gray-400">#{detail().pull.number || detail().pull.rkey}</span>
											</h1>
									</header>

									<div class="mt-3 flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
										<StateBadge state={detail().pull.state} kind="pull" />
										<span>opened by</span>
										<Avatar did={detail().pull.author.did} size="size-6" />
										<a href={`/${detail().pull.author.did}`} class="text-gray-700 dark:text-gray-200">{detail().pull.author.handle}</a>
										<span class="select-none">·</span>
										<span>{formatRelativeTime(detail().pull.value.createdAt)}</span>
										<span class="select-none">·</span>
										<span>targeting</span>
										<BranchPill name={detail().pull.value.target.branch} />
									</div>

									<Show when={detail().pull.value.body}>
										<div class="mt-8 min-w-0 text-base leading-7 text-gray-700 dark:text-gray-300">
											<MarkdownBlock markdown={detail().pull.value.body!} />
										</div>
									</Show>

									<div class="mt-4 flex flex-wrap items-center justify-end gap-2">
										<Show when={auth.agent()}>
											<Show when={detail().pull.state === 'open'}>
												<button
													type="button"
													class={buttonStyles('primary')}
													disabled={working() || detail().pull.value.rounds.length === 0}
													onClick={() => void mergeCurrentPull()}
												>
													<Show when={workingAction() === 'merge'} fallback={<GitMerge class="size-4" />}>
														<LoaderCircle class="size-4 animate-spin" />
													</Show>
													merge
												</button>
											</Show>
											<Show when={detail().pull.state !== 'merged'}>
												<button type="button" class={buttonStyles()} disabled={working()} onClick={() => void toggleState()}>
													<Show
														when={workingAction() === 'state'}
														fallback={
															<Show when={detail().pull.state === 'open'} fallback={<RefreshCcwDot class="size-4" />}>
																<GitPullRequestClosed class="size-4" />
															</Show>
														}
													>
														<LoaderCircle class="size-4 animate-spin" />
													</Show>
													{detail().pull.state === 'open' ? 'close pull' : 'reopen pull'}
												</button>
											</Show>
										</Show>
									</div>
									<Show when={error()}>
										<p class="mt-3 text-sm text-red-500">{error()}</p>
									</Show>
								</section>

								<aside class="untangled-pr-meta untangled-thread-meta">
									<div class="untangled-pr-meta-section">
										<h2>Labels</h2>
										<p>None yet.</p>
									</div>
									<div class="untangled-pr-meta-section">
										<h2>Assignee</h2>
										<p>None yet.</p>
									</div>
									<div class="untangled-pr-meta-section">
										<h2>
											Participants <span>{participants().length}</span>
										</h2>
										<div class="mt-3 flex flex-wrap gap-2">
											<For each={participants()}>
												{(participant) => <Avatar did={participant.did} size="size-7" />}
											</For>
										</div>
									</div>
									<AtUriPanel uri={detail().pull.uri} />
								</aside>
							</div>

							<Switch>
								<Match when={patchQuery.isLoading}>
									<PullDiffSkeleton />
								</Match>
								<Match when={patchQuery.error}>
									<div class={cardStyles('p-6')}>
										<ErrorState message={getErrorMessage(patchQuery.error)} />
									</div>
								</Match>
								<Match when={patchQuery.data}>
									<PullDiffView
										patch={patchQuery.data!}
										roundLabel={roundLabel()}
										storageKey={`reviewed:${repoQuery.data!.owner.handle}/${repoQuery.data!.slug}/pulls/${detail().pull.number || parseAtUri(detail().pull.uri).rkey}/round/${roundIndex()}`}
										mobileDrawerOpen={mobileDrawerOpen()}
										onMobileDrawerClose={() => setMobileDrawerOpen(false)}
										history={
											<details
												open={true}
												class="rounded-t shadow-lg md:shadow-none group/panel w-full"
											>
												<summary
													class={clsx(
														"h-12 flex gap-4 items-center justify-between",
														"rounded-t cursor-pointer list-none px-4",
														"text-white md:text-black md:dark:text-white",
														detail().pull.state === 'open'
															? 'bg-green-600 dark:bg-green-700'
															: detail().pull.state === 'merged'
															? 'bg-purple-600 dark:bg-purple-700'
															: 'bg-red-600 dark:bg-red-700',
														"md:bg-white md:dark:bg-gray-800",
														"shadow-sm border-t md:border-x border-gray-200 dark:border-gray-700",
														"md:pointer-events-none"
													)}
													onClick={(e) => {
														e.preventDefault();
														setMobileDrawerOpen(!mobileDrawerOpen());
													}}
												>
													<h3 class="text-base font-semibold text-white md:text-gray-900 md:dark:text-white">history</h3>
													<span class="pointer-events-auto cursor-text flex items-center gap-2 text-sm text-white md:text-gray-500 md:dark:text-gray-400">
														<span>
															{detail().pull.value.rounds.length} round{detail().pull.value.rounds.length === 1 ? '' : 's'}
														</span>
														<span class="select-none before:content-['\00B7']"></span>
														<span>
															{detail().comments.length} comment{detail().comments.length === 1 ? '' : 's'}
														</span>
														<span class="md:hidden inline ml-2">
															<Show when={mobileDrawerOpen()} fallback={<ChevronUp class="size-4" />}>
																<ChevronDown class="size-4" />
															</Show>
														</span>
													</span>
												</summary>
												<div class="max-h-[85vh] md:max-h-[calc(100vh-3rem-3rem)] w-full flex flex-col gap-4 overflow-y-auto bg-slate-100 dark:bg-gray-900 md:bg-transparent p-0 untangled-pr-history-body">
													<For each={detail().pull.value.rounds}>
													{(round, index) => {
														const isSelected = () => index() === roundIndex();
														const isLatest = () => index() === detail().pull.value.rounds.length - 1;
														const roundComments = () => commentsByRound()[index()] || [];
														const roundThreads = createMemo(() => buildPRCommentThreads(roundComments()));

														return (
															<div
																class={clsx(
																	'w-full border overflow-clip shadow-sm text-sm',
																	index() === 0 ? 'rounded-b' : 'rounded',
																	index() > 0 && 'mt-4',
																	isSelected()
																		? 'bg-blue-50/25 border-blue-200 dark:bg-blue-900/10 dark:border-blue-900'
																		: 'bg-gray-50 border-gray-200 dark:bg-gray-900 dark:border-gray-700'
																)}
															>
																{/* Submission Header */}
																<div
																	class={clsx(
																		'px-6 py-4 flex gap-2 border-b',
																		isSelected()
																			? 'bg-blue-50 dark:bg-blue-950 border-blue-100 dark:border-blue-900'
																			: 'bg-white dark:bg-gray-800 border-gray-100 dark:border-gray-700'
																	)}
																>
																	<div class="shrink-0">
																		<Avatar did={detail().pull.author.did} size="size-8 mr-1" />
																	</div>
																	<div class="flex-1 min-w-0 flex flex-col justify-center">
																		<div class="flex items-center justify-between">
																			<span
																				class={clsx(
																					'inline-flex items-center gap-2 text-sm',
																					isSelected()
																						? 'text-gray-600 dark:text-gray-300'
																						: 'text-gray-500 dark:text-gray-400'
																				)}
																			>
																				<a
																					href={`/${detail().pull.author.did}`}
																					class={clsx(
																						'hover:underline font-medium',
																						isSelected()
																							? 'text-gray-800 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-200'
																							: 'text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300'
																					)}
																				>
																					{detail().pull.author.handle}
																				</a>
																				<span>submitted</span>
																				<span
																					class={clsx(
																						'px-2 py-0.5 rounded font-mono text-xs border',
																						isSelected()
																							? 'text-blue-800 dark:text-white bg-blue-100 dark:bg-blue-600 border-blue-200 dark:border-blue-500'
																							: 'text-black dark:text-white bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600'
																					)}
																				>
																					#{index()}
																				</span>
																				<span class="select-none">·</span>
																				<span>{formatRelativeTime(round.createdAt)}</span>
																			</span>
																			
																			<Show when={!isSelected()}>
																				<button
																					type="button"
																					class="btn-flat p-2 flex items-center gap-2 no-underline hover:no-underline text-sm font-medium cursor-pointer"
																					onClick={() => setRoundIndex(index())}
																				>
																					<Diff class="size-4" />
																					diff
																				</button>
																			</Show>
																		</div>
																	</div>
																</div>

																{/* Comments section */}
																<Show when={roundComments().length > 0}>
																	<details
																		class="relative ml-10 pr-4 group/comments mt-0"
																		open={isSelected()}
																	>
																		<summary class="cursor-pointer list-none select-none">
																			<div class="hidden group-open/comments:block absolute -left-8 top-0 bottom-0 w-16 flex items-center justify-center pointer-events-none">
																				<div class="absolute left-1/2 -translate-x-1/2 top-0 bottom-0 w-px bg-gray-200 dark:bg-gray-700"></div>
																			</div>
																			<div class="group-open/comments:hidden block relative py-2">
																				<div class="absolute -left-8 top-0 bottom-0 w-16 flex items-center justify-center pointer-events-none">
																					<div class="absolute left-1/2 -translate-x-1/2 h-1/3 top-0 bottom-0 w-px bg-gray-200 dark:bg-gray-700"></div>
																				</div>
																				<span class="text-gray-500 dark:text-gray-400 text-sm flex items-center gap-1.5 -ml-2 relative hover:text-gray-600 dark:hover:text-gray-300">
																					<CirclePlus class="size-3.5" />
																					Expand {roundComments().length} comment{roundComments().length === 1 ? '' : 's'}
																				</span>
																			</div>
																		</summary>

																		<div class="relative border-l border-gray-200 dark:border-gray-700 pr-4 pb-2">
																			<For each={roundThreads()}>
																				{(thread) => (
																					<div class="flex flex-col -ml-4">
																						<ThreadCommentView
																							id={`comment-${thread.item.rkey}`}
																							author={{ handle: thread.item.author.handle, did: thread.item.author.did }}
																							createdAt={thread.item.value.createdAt}
																							markdown={thread.item.value.body}
																							isAuthor={thread.item.author.did === detail().pull.author.did}
																						/>
																						<Show when={thread.replies.length > 0}>
																							<div class="relative ml-10 border-l border-gray-200 dark:border-gray-700">
																								<For each={thread.replies}>
																									{(reply) => (
																										<div class="-ml-4">
																											<ThreadCommentView
																												id={`comment-${reply.rkey}`}
																												author={{ handle: reply.author.handle, did: reply.author.did }}
																												createdAt={reply.value.createdAt}
																												markdown={reply.value.body}
																												isAuthor={reply.author.did === detail().pull.author.did}
																												isReply={true}
																											/>
																										</div>
																									)}
																								</For>
																							</div>
																						</Show>
																					</div>
																				)}
																			</For>
																			
																			<button
																				type="button"
																				class="flex items-center gap-2 -ml-2 relative cursor-pointer text-sm text-gray-500 dark:text-gray-400 mt-4 pb-4 transition-colors hover:text-gray-600 dark:hover:text-gray-300"
																				onClick={(e) => {
																					const detailsEl = e.currentTarget.closest('details');
																					if (detailsEl) detailsEl.open = false;
																				}}
																			>
																				<CircleChevronUp class="size-4" />
																				Collapse comment{roundComments().length === 1 ? '' : 's'}
																			</button>
																		</div>
																	</details>
																</Show>

																{/* Latest round status and comment form */}
																<Show when={isLatest()}>
																	{/* State event banner */}
																	<Show when={detail().pull.state !== 'open'}>
																		<div class={clsx('untangled-pr-history-event mx-4 mb-3 mt-3 text-sm font-medium', statusEvent().class)}>
																			{statusEvent().icon}
																			<span>{statusEvent().text}</span>
																		</div>
																	</Show>
																	
																	{/* Reply/Comment Form Actions Strip */}
																	<Show when={auth.agent()}>
																		<div class="bg-gray-50 dark:bg-gray-900 px-4 py-3 border-t border-gray-200 dark:border-gray-700 flex flex-col gap-3">
																			<Show when={!composerOpen()}>
																				<div class="flex items-center justify-start">
																					<button
																						type="button"
																						class="btn-flat p-2 flex items-center gap-2 no-underline hover:no-underline text-sm font-medium cursor-pointer"
																						onClick={() => setComposerOpen(true)}
																					>
																						<MessageSquarePlus class="size-4" />
																						comment
																					</button>
																				</div>
																			</Show>

																			<Show when={composerOpen()}>
																				<form onSubmit={addComment} class="flex flex-col gap-2 w-full">
																					<textarea
																						value={comment()}
																						onInput={(event) => setComment(event.currentTarget.value)}
																						class={textareaStyles()}
																						rows="4"
																						placeholder="Add to the discussion..."
																					/>
																					<Show when={error()}>
																						<p class="text-sm text-red-500">{error()}</p>
																					</Show>
																					<div class="flex items-center justify-end gap-2 text-sm w-full">
																						<button
																							type="button"
																							class="btn-flat text-red-500 dark:text-red-400 flex items-center gap-1.5 cursor-pointer font-medium"
																							onClick={() => {
																								setComment('');
																								setComposerOpen(false);
																							}}
																						>
																							<X class="size-4" />
																							<span>cancel</span>
																						</button>
																						
																						<button
																							type="submit"
																							class={clsx(buttonStyles('primary'), 'justify-center')}
																							disabled={working() || !comment().trim()}
																						>
																							<Show when={workingAction() === 'comment'} fallback={<Reply class="size-4" />}>
																								<LoaderCircle class="size-4 animate-spin" />
																							</Show>
																							<span>reply</span>
																						</button>
																					</div>
																				</form>
																			</Show>
																		</div>
																	</Show>
																</Show>
															</div>
														);
													}}
												</For>
											</div>
										</details>
										}
									/>
								</Match>
							</Switch>
						</div>
					)}
				</Match>
			</Switch>
		</RepoFrame>
	);
};
