import clsx from 'clsx';
import {
	ArrowLeft,
	Copy,
	CopyCheck,
	Ellipsis,
	FolderCode,
} from 'lucide-solid';
import { A, useNavigate, useParams, useSearchParams } from '@solidjs/router';
import { createQuery } from '../../lib/atproto-query';
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js';
import {
	getRepoBranches,
	getRepoCommit,
	getRepoLog,
	getRepoTags,
	type CommitEntry,
	type CommitSignature,
	type DiffTextLine,
	type GitHashValue,
	type RepoContext,
	type RepoDiff,
	type RepoDiffFile,
	type TagResponse,
} from '../../lib/api/repos';
import { Avatar, ErrorState, PaginationControls, PlaceholderAvatar, SkeletonBlock, cardStyles } from '../../components/common';
import { DiffView } from '../../components/repo';
import { RepoFrame, useRepoQuery } from './shared';
import {
	commitHref,
	commitsHref,
	formatRelativeTime,
	getErrorMessage,
	parseIntegerSearchParam,
	safeDecode,
	treeHref,
} from '../../lib/repo-utils';

const COMMITS_PAGE_LIMIT = 60;
const commitListSkeletonRows = Array.from({ length: COMMITS_PAGE_LIMIT });
const commitDiffSkeletonFiles = Array.from({ length: 2 });
const commitDiffSkeletonTreeRows = Array.from({ length: 8 });
const commitDiffSkeletonLines = Array.from({ length: 9 });

const hashBytesToHex = (value: number[]): string =>
	value.map((byte) => byte.toString(16).padStart(2, '0')).join('');

const normalizeHashValue = (value?: GitHashValue): string => {
	if (!value) return '';
	if (typeof value === 'string') return value;
	if (Array.isArray(value)) return hashBytesToHex(value);
	return value.String ?? value.string ?? '';
};

const commitHash = (commit: CommitEntry): string => commit.this || normalizeHashValue(commit.hash);

const primaryParentHash = (commit: CommitEntry): string =>
	commit.parent || commit.parent_hashes?.map(normalizeHashValue).find(Boolean) || '';

const splitCommitMessage = (message: string) => {
	const trimmed = message.trimEnd();
	const [subject, ...bodyParts] = trimmed.split(/\n\n/);
	return {
		subject: subject?.trim() || 'no commit message',
		body: bodyParts.join('\n\n').trim(),
	};
};

const commitTime = (commit: CommitEntry): string =>
	commit.committer?.When || commit.author?.When || '';

const commitAuthor = (commit: CommitEntry): CommitSignature | undefined =>
	commit.author ?? commit.committer;

const personLabel = (person?: CommitSignature): string =>
	person?.Name || person?.Email || 'unknown author';

const personDid = (person?: CommitSignature): string | undefined =>
	person?.Email?.startsWith('did:') ? person.Email : undefined;

const formatFullTime = (value?: string): string => {
	if (!value) return 'unknown time';
	const date = new Date(value);
	if (Number.isNaN(date.getTime())) return value;
	return new Intl.DateTimeFormat(undefined, {
		dateStyle: 'medium',
		timeStyle: 'short',
	}).format(date);
};

const commitListPageHref = (repo: RepoContext, ref: string, page: number): string =>
	page <= 1 ? commitsHref(repo, ref) : `${commitsHref(repo, ref)}?page=${page}`;

const addRefLabel = (map: Map<string, string[]>, hash: string | undefined, label: string | undefined) => {
	if (!hash || !label) return;
	const existing = map.get(hash) ?? [];
	if (!existing.includes(label)) {
		map.set(hash, [...existing, label]);
	}
};

const tagTargetHash = (tag: TagResponse['tags'][number]): string => {
	const fullTag = tag.tag as (TagResponse['tags'][number]['tag'] & {
		Target?: GitHashValue;
		target?: GitHashValue;
	}) | undefined;
	return normalizeHashValue(fullTag?.Target ?? fullTag?.target) || tag.hash;
};

const formatRange = (start: number, count: number): string =>
	count === 1 ? String(start) : `${start},${count}`;

const diffLinePrefix = (line: DiffTextLine): ' ' | '-' | '+' => {
	if (line.Op === 1 || line.Op === '-') return '-';
	if (line.Op === 2 || line.Op === '+') return '+';
	return ' ';
};

const renderFragment = (fragment: RepoDiffFile['text_fragments'][number]): string => {
	let text = `@@ -${formatRange(fragment.OldPosition, fragment.OldLines)} +${formatRange(fragment.NewPosition, fragment.NewLines)} @@`;
	if (fragment.Comment) {
		text += ` ${fragment.Comment}`;
	}
	text += '\n';

	for (const line of fragment.Lines ?? []) {
		const rendered = `${diffLinePrefix(line)}${line.Line ?? ''}`;
		text += rendered.endsWith('\n') ? rendered : `${rendered}\n`;
	}

	return text;
};

const renderDiffFile = (file: RepoDiffFile): string => {
	let oldName = file.name.old;
	let newName = file.name.new;
	if (!oldName) oldName = newName;
	if (!newName) newName = oldName;

	let text = `diff --git a/${oldName} b/${newName}\n`;
	if (file.is_new) {
		text += 'new file mode 100644\n';
		text += '--- /dev/null\n';
		text += `+++ b/${newName}\n`;
	} else if (file.is_delete) {
		text += 'deleted file mode 100644\n';
		text += `--- a/${oldName}\n`;
		text += '+++ /dev/null\n';
	} else if (file.is_rename) {
		text += `rename from ${oldName}\n`;
		text += `rename to ${newName}\n`;
		text += `--- a/${oldName}\n`;
		text += `+++ b/${newName}\n`;
	} else {
		text += `--- a/${oldName}\n`;
		text += `+++ b/${newName}\n`;
	}

	if (file.is_binary) {
		text += `Binary files a/${oldName} and b/${newName} differ\n`;
		return text;
	}

	for (const fragment of file.text_fragments ?? []) {
		text += renderFragment(fragment);
	}

	return text;
};

const renderUnifiedDiff = (diff?: RepoDiff): string =>
	diff?.diff.map(renderDiffFile).join('') ?? '';

const CommitIdentity: Component<{ person?: CommitSignature; size?: string }> = (props) => (
	<span class="flex items-center gap-1 min-w-0">
		<Show
			when={personDid(props.person)}
			fallback={<PlaceholderAvatar size={props.size ?? 'size-6'} iconSize="size-4" />}
		>
			{(did) => <Avatar did={did()} size={props.size ?? 'size-6'} />}
		</Show>
		<span class="truncate">{personLabel(props.person)}</span>
	</span>
);

const CopyShaButton: Component<{ hash: string }> = (props) => {
	const [copied, setCopied] = createSignal(false);

	const copy = () => {
		void navigator.clipboard?.writeText(props.hash);
		setCopied(true);
		window.setTimeout(() => setCopied(false), 1500);
	};

	return (
		<button
			type="button"
			class="p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
			title="Copy SHA"
			aria-label="Copy SHA"
			onClick={copy}
		>
			<Show when={copied()} fallback={<Copy class="w-4 h-4" />}>
				<CopyCheck class="w-4 h-4" />
			</Show>
		</button>
	);
};

const CommitHashLink: Component<{ repo: RepoContext; hash: string }> = (props) => (
	<A
		href={commitHref(props.repo, props.hash)}
	class="no-underline hover:underline text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900 px-2 py-1 rounded flex items-center gap-2"
>
	{props.hash.slice(0, 8)}
</A>
);

const RefLabels: Component<{ labels: string[] }> = (props) => (
	<For each={props.labels}>
		{(label) => (
			<span class="ml-2 text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 inline-flex items-center">
				{label}
			</span>
		)}
	</For>
);

const CommitMessageCell: Component<{
	repo: RepoContext;
	commit: CommitEntry;
	hash: string;
	labels: string[];
}> = (props) => {
	const [expanded, setExpanded] = createSignal(false);
	const message = createMemo(() => splitCommitMessage(props.commit.message));

	return (
		<div>
			<A href={commitHref(props.repo, props.hash)} class="dark:text-white no-underline hover:underline">
				{message().subject}
			</A>
			<Show when={message().body}>
				<button
					type="button"
					class="ml-1 py-0.5 px-1 bg-gray-200 hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600 rounded"
					aria-label={expanded() ? 'Hide commit message body' : 'Show commit message body'}
					onClick={() => setExpanded(!expanded())}
				>
					<Ellipsis class="w-3 h-3" />
				</button>
			</Show>
			<RefLabels labels={props.labels} />
			<Show when={expanded() && message().body}>
				<p class="mt-1 text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap">{message().body}</p>
			</Show>
		</div>
	);
};

const CommitDesktopRow: Component<{
	repo: RepoContext;
	commit: CommitEntry;
	labels: string[];
}> = (props) => {
	const hash = createMemo(() => commitHash(props.commit));
	const when = createMemo(() => commitTime(props.commit));

	return (
		<div class="grid grid-cols-14 gap-4 py-3">
			<div class="align-top col-span-3">
				<CommitIdentity person={commitAuthor(props.commit)} />
			</div>
			<div class="align-top font-mono flex items-start col-span-3">
				<CommitHashLink repo={props.repo} hash={hash()} />
				<div class="inline-flex ml-1">
					<CopyShaButton hash={hash()} />
					<A
						href={treeHref(props.repo, hash())}
						class="p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
						title="Browse repository at this commit"
						aria-label="Browse repository at this commit"
					>
						<FolderCode class="w-4 h-4" />
					</A>
				</div>
			</div>
			<div class="align-top col-span-6">
				<CommitMessageCell repo={props.repo} commit={props.commit} hash={hash()} labels={props.labels} />
			</div>
			<div class="align-top justify-self-end text-gray-500 dark:text-gray-400 col-span-2">
				<time datetime={when()} title={formatFullTime(when())}>
					{formatRelativeTime(when())}
				</time>
			</div>
		</div>
	);
};

const CommitMobileRow: Component<{
	repo: RepoContext;
	commit: CommitEntry;
	labels: string[];
	last: boolean;
}> = (props) => {
	const hash = createMemo(() => commitHash(props.commit));
	const when = createMemo(() => commitTime(props.commit));

	return (
		<div
			class={clsx(
				'relative p-2 mb-2',
				!props.last && 'border-b border-gray-200 dark:border-gray-700',
			)}
		>
			<div class="text-base">
				<div class="flex items-center justify-between gap-2">
					<div class="flex-1 min-w-0">
						<CommitMessageCell repo={props.repo} commit={props.commit} hash={hash()} labels={props.labels} />
					</div>
					<A
						href={treeHref(props.repo, hash())}
						class="p-1 mr-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
						title="Browse repository at this commit"
						aria-label="Browse repository at this commit"
					>
						<FolderCode class="w-4 h-4" />
					</A>
				</div>
			</div>

			<div class="text-xs mt-2 text-gray-500 dark:text-gray-400 flex items-center flex-wrap gap-y-2">
				<span class="font-mono">
					<CommitHashLink repo={props.repo} hash={hash()} />
				</span>
				<span class="mx-1 before:content-['·'] before:select-none" />
				<CommitIdentity person={commitAuthor(props.commit)} />
				<div class="inline-block px-1 select-none after:content-['·']" />
				<time datetime={when()} title={formatFullTime(when())}>
					{formatRelativeTime(when())}
				</time>
			</div>
		</div>
	);
};

const CommitListSkeleton: Component = () => (
	<>
		<div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700">
			<div class="grid grid-cols-14 gap-4">
				<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Author</div>
				<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Commit</div>
				<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-6">Message</div>
				<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2 justify-self-end">Date</div>
			</div>
			<For each={commitListSkeletonRows}>
				{(_, index) => (
					<div class="grid grid-cols-14 gap-4 py-3">
						<div class="col-span-3 flex min-w-0 items-center gap-2">
							<SkeletonBlock class="size-6 rounded-full" />
							<SkeletonBlock class={index() % 3 === 0 ? 'h-4 w-24' : 'h-4 w-32'} />
						</div>
						<div class="col-span-3 flex items-center gap-2">
							<SkeletonBlock class="h-8 w-24" />
							<SkeletonBlock class="size-6" />
							<SkeletonBlock class="size-6" />
						</div>
						<div class="col-span-6 flex min-w-0 items-center gap-2">
							<SkeletonBlock class={index() % 2 === 0 ? 'h-5 w-3/4' : 'h-5 w-1/2'} />
							<Show when={index() % 4 === 0}>
								<SkeletonBlock class="h-5 w-14" />
							</Show>
						</div>
						<div class="col-span-2 justify-self-end">
							<SkeletonBlock class="h-4 w-16" />
						</div>
					</div>
				)}
			</For>
		</div>

		<div class="md:hidden">
			<For each={commitListSkeletonRows}>
				{(_, index) => (
					<div class={clsx('relative p-2 mb-2', index() < commitListSkeletonRows.length - 1 && 'border-b border-gray-200 dark:border-gray-700')}>
						<div class="flex items-center justify-between gap-2">
							<SkeletonBlock class={index() % 2 === 0 ? 'h-5 w-2/3' : 'h-5 w-1/2'} />
							<SkeletonBlock class="size-6" />
						</div>
						<div class="mt-3 flex flex-wrap items-center gap-2">
							<SkeletonBlock class="h-7 w-24" />
							<SkeletonBlock class="size-6 rounded-full" />
							<SkeletonBlock class="h-4 w-24" />
							<SkeletonBlock class="h-4 w-16" />
						</div>
					</div>
				)}
			</For>
		</div>
	</>
);

const CommitListContent: Component<{
	repo: RepoContext;
	commits: CommitEntry[];
	labelsByHash: Map<string, string[]>;
}> = (props) => (
	<Show
		when={props.commits.length > 0}
		fallback={<p class="text-sm text-gray-500 dark:text-gray-400">No commits found.</p>}
	>
		<div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700">
			<div class="grid grid-cols-14 gap-4">
				<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Author</div>
				<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Commit</div>
				<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-6">Message</div>
				<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2 justify-self-end">Date</div>
			</div>
			<For each={props.commits}>
				{(commit) => (
					<CommitDesktopRow
						repo={props.repo}
						commit={commit}
						labels={props.labelsByHash.get(commitHash(commit)) ?? []}
					/>
				)}
			</For>
		</div>

		<div class="md:hidden">
			<For each={props.commits}>
				{(commit, index) => (
					<CommitMobileRow
						repo={props.repo}
						commit={commit}
						labels={props.labelsByHash.get(commitHash(commit)) ?? []}
						last={index() === props.commits.length - 1}
					/>
				)}
			</For>
		</div>
	</Show>
);

export const CommitsPage: Component = () => {
	const params = useParams();
	const [searchParams] = useSearchParams();
	const navigate = useNavigate();
	const repoQuery = useRepoQuery();
	const ref = createMemo(() => safeDecode(params.ref ?? 'HEAD'));
	const page = createMemo(() => parseIntegerSearchParam(searchParams.page, 1, 1));
	const pageOffset = createMemo(() => (page() - 1) * COMMITS_PAGE_LIMIT);
	const cursor = createMemo(() => String(pageOffset()));

	const branchesQuery = createQuery(() => {
		const repo = repoQuery.data;
		return {
			queryKey: ['repo-branches', repo?.repoDid],
			enabled: Boolean(repo),
			queryFn: async () => getRepoBranches(repo!),
		};
	});
	const logQuery = createQuery(() => {
		const repo = repoQuery.data;
		const hasBranches = branchesQuery.data && branchesQuery.data.branches.length > 0;
		return {
			queryKey: ['repo-commits', repo?.repoDid, ref(), page()],
			enabled: Boolean(repo && hasBranches),
			queryFn: async () => getRepoLog(repo!, ref(), {
				limit: COMMITS_PAGE_LIMIT,
				cursor: page() > 1 ? cursor() : undefined,
			}),
		};
	});
	const tagsQuery = createQuery(() => {
		const repo = repoQuery.data;
		const hasBranches = branchesQuery.data && branchesQuery.data.branches.length > 0;
		return {
			queryKey: ['repo-tags', repo?.repoDid],
			enabled: Boolean(repo && hasBranches),
			queryFn: async () => getRepoTags(repo!),
		};
	});
	const labelsByHash = createMemo(() => {
		const map = new Map<string, string[]>();
		for (const branch of branchesQuery.data?.branches ?? []) {
			addRefLabel(map, branch.reference.hash, branch.reference.name);
		}
		for (const tag of tagsQuery.data?.tags ?? []) {
			addRefLabel(map, tagTargetHash(tag), tag.name);
		}
		return map;
	});
	const commits = createMemo(() => logQuery.data?.commits ?? []);
	const totalCount = createMemo(() => {
		const total = logQuery.data?.total;
		return total && total > 0 ? total : undefined;
	});
	const hasNextPage = createMemo(() => {
		const data = logQuery.data;
		if (!data) return false;
		if (totalCount()) {
			return pageOffset() + COMMITS_PAGE_LIMIT < totalCount()!;
		}
		return data.commits.length === COMMITS_PAGE_LIMIT;
	});

	createEffect(() => {
		if (repoQuery.data && branchesQuery.data && branchesQuery.data.branches.length === 0) {
			navigate(`/${repoQuery.data.owner.handle}/${repoQuery.data.slug}`, { replace: true });
		}
	});

	return (
		<RepoFrame
			active="code"
			loadingFallback={
				<section id="commit-table" class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white overflow-x-auto">
					<h2 class="font-bold text-sm mb-4 uppercase dark:text-white">commits</h2>
					<CommitListSkeleton />
				</section>
			}
		>
			<section id="commit-table" class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white overflow-x-auto">
				<h2 class="font-bold text-sm mb-4 uppercase dark:text-white">commits</h2>
				<Switch>
					<Match when={logQuery.isLoading && !logQuery.data}>
						<CommitListSkeleton />
					</Match>
					<Match when={logQuery.error}>
						<ErrorState message={getErrorMessage(logQuery.error)} />
					</Match>
					<Match when={logQuery.data && repoQuery.data}>
						<CommitListContent repo={repoQuery.data!} commits={commits()} labelsByHash={labelsByHash()} />
					</Match>
				</Switch>
			</section>
			<Show when={repoQuery.data && logQuery.data}>
				<PaginationControls
					offset={pageOffset()}
					limit={COMMITS_PAGE_LIMIT}
					totalCount={totalCount()}
					hasNext={hasNextPage()}
					hrefForOffset={(offset) =>
						commitListPageHref(repoQuery.data!, ref(), Math.floor(Math.max(0, offset) / COMMITS_PAGE_LIMIT) + 1)
					}
				/>
			</Show>
		</RepoFrame>
	);
};

const CommitAttribution: Component<{ commit: CommitEntry }> = (props) => {
	const showCommitter = createMemo(
		() => props.commit.author?.Email && props.commit.committer?.Email && props.commit.author.Email !== props.commit.committer.Email,
	);

	return (
		<Show
			when={showCommitter()}
			fallback={<CommitIdentity person={commitAuthor(props.commit)} />}
		>
			<span class="inline-flex flex-wrap items-center gap-1">
				<span>authored by</span>
				<CommitIdentity person={props.commit.author} />
				<span>and committed by</span>
				<CommitIdentity person={props.commit.committer} />
			</span>
		</Show>
	);
};

const CommitDiffSkeleton: Component = () => (
	<section class="untangled-commit-diff mt-2 w-full">
		<div class="untangled-diff-view untangled-diff-view-commit">
			<div class="untangled-diff-panel">
				<div class="untangled-diff-toolbar">
					<div class="flex min-w-0 items-center gap-2">
						<SkeletonBlock class="h-8 w-8" />
						<SkeletonBlock class="h-7 w-20" />
						<SkeletonBlock class="h-4 w-28" />
					</div>
					<div class="flex items-center gap-2">
						<SkeletonBlock class="h-8 w-28" />
						<SkeletonBlock class="h-8 w-32" />
					</div>
				</div>
				<div class="untangled-diff-panel-body untangled-diff-panel-body-with-tree">
					<aside class="untangled-pr-file-tree" aria-label="changed files loading">
						<div class="untangled-pr-file-tree-list">
							<For each={commitDiffSkeletonTreeRows}>
								{(_, index) => (
									<div class="untangled-pr-file-tree-entry">
										<SkeletonBlock class="size-4 shrink-0 rounded-sm" />
										<SkeletonBlock class={index() % 3 === 0 ? 'h-4 w-20' : 'h-4 w-32'} />
									</div>
								)}
							</For>
						</div>
					</aside>
					<div class="untangled-pr-diff-files">
						<For each={commitDiffSkeletonFiles}>
							{(_, fileIndex) => (
								<div class="untangled-diff-file">
									<div class="untangled-diff-file-header">
										<div class="flex min-w-0 items-center gap-2">
											<SkeletonBlock class="size-4 shrink-0" />
											<SkeletonBlock class="h-7 w-20" />
											<SkeletonBlock class={fileIndex() === 0 ? 'h-5 w-2/3' : 'h-5 w-1/2'} />
										</div>
									</div>
									<div class="space-y-2 bg-white p-4 dark:bg-gray-900">
										<For each={commitDiffSkeletonLines}>
											{(_, index) => (
												<div class="untangled-skeleton-code-line">
													<SkeletonBlock class="h-4 w-8" />
													<SkeletonBlock class={index() % 4 === 0 ? 'h-4 w-1/2' : index() % 3 === 0 ? 'h-4 w-2/3' : 'h-4 w-11/12'} />
												</div>
											)}
										</For>
									</div>
								</div>
							)}
						</For>
					</div>
				</div>
			</div>
		</div>
	</section>
);

const CommitDetailSkeleton: Component = () => (
	<div class="untangled-commit-view w-full">
		<section class="untangled-commit-detail bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white">
			<SkeletonBlock class="h-6 w-2/3" />
			<div class="mt-3 space-y-2">
				<SkeletonBlock class="h-4 w-5/6" />
				<SkeletonBlock class="h-4 w-1/2" />
			</div>
			<div class="mt-4 flex flex-wrap items-center gap-2">
				<SkeletonBlock class="size-6 rounded-full" />
				<SkeletonBlock class="h-4 w-28" />
				<SkeletonBlock class="h-4 w-32" />
				<SkeletonBlock class="h-4 w-16" />
				<SkeletonBlock class="h-4 w-16" />
			</div>
		</section>
		<CommitDiffSkeleton />
	</div>
);

const CommitDetailContent: Component<{
	repo: RepoContext;
	commit: CommitEntry;
	hash: string;
	parentHash: string;
	message: ReturnType<typeof splitCommitMessage>;
	patch: string;
}> = (props) => {
	const when = createMemo(() => commitTime(props.commit));

	return (
		<div class="untangled-commit-view w-full">
			<section class="untangled-commit-detail bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white">
				<div id="commit-message">
					<p class="pb-2">{props.message.subject}</p>
					<Show when={props.message.body}>
						<p class="mt-1 cursor-text pb-2 text-sm whitespace-pre-wrap">{props.message.body}</p>
					</Show>
				</div>

				<div class="flex flex-wrap items-center gap-2 text-sm">
					<p class="flex flex-wrap items-center gap-1 text-gray-500 dark:text-gray-300">
						<CommitAttribution commit={props.commit} />
						<span class="px-1 select-none before:content-['·']" />
						<time datetime={when()} title={formatFullTime(when())}>
							{formatFullTime(when())}
						</time>
						<span class="px-1 select-none before:content-['·']" />
						<A
							href={commitHref(props.repo, props.hash)}
							class="no-underline hover:underline text-gray-500 dark:text-gray-300"
						>
							{props.hash.slice(0, 8)}
						</A>
						<Show when={props.parentHash}>
							{(parent) => (
								<>
									<ArrowLeft class="w-3 h-3 mx-1" />
									<A
										href={commitHref(props.repo, parent())}
										class="no-underline hover:underline text-gray-500 dark:text-gray-300"
									>
										{parent().slice(0, 8)}
									</A>
								</>
							)}
						</Show>
					</p>
				</div>
			</section>

			<section class="untangled-commit-diff mt-2 w-full">
				<DiffView patch={props.patch} wrap defaultExpanded showFileTree variant="commit" />
			</section>
		</div>
	);
};

export const CommitPage: Component = () => {
	const params = useParams();
	const repoQuery = useRepoQuery();
	const ref = createMemo(() => safeDecode(params.ref ?? ''));
	const commitQuery = createQuery(() => {
		const repo = repoQuery.data;
		return {
			queryKey: ['repo-commit', repo?.repoDid, ref()],
			enabled: Boolean(repo && ref()),
			queryFn: async () => getRepoCommit(repo!, ref()),
		};
	});
	const commit = createMemo(() => commitQuery.data?.diff?.commit);
	const hash = createMemo(() => {
		const current = commit();
		return current ? commitHash(current) : ref();
	});
	const parentHash = createMemo(() => {
		const current = commit();
		return current ? primaryParentHash(current) : '';
	});
	const message = createMemo(() => splitCommitMessage(commit()?.message ?? ''));
	const patch = createMemo(() => renderUnifiedDiff(commitQuery.data?.diff));

	return (
		<RepoFrame active="code" fullWidth loadingFallback={<CommitDetailSkeleton />}>
			<Switch>
				<Match when={commitQuery.isLoading}>
					<CommitDetailSkeleton />
				</Match>
				<Match when={commitQuery.error}>
					<div class={cardStyles('p-6')}>
						<ErrorState message={getErrorMessage(commitQuery.error)} />
					</div>
				</Match>
				<Match when={commit() && repoQuery.data}>
					<CommitDetailContent
						repo={repoQuery.data!}
						commit={commit()!}
						hash={hash()}
						parentHash={parentHash()}
						message={message()}
						patch={patch()}
					/>
				</Match>
			</Switch>
		</RepoFrame>
	);
};
