import clsx from 'clsx';
import { File, Folder, GitBranch, GitCommitHorizontal, List } from 'lucide-solid';
import { A, useNavigate, useParams } from '@solidjs/router';
import { createQuery, keepPreviousData, useQueryClient } from '../../lib/atproto-query';
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, type Accessor, type Component } from 'solid-js';
import { buildBlobDataUrl, decodeBlobBytes, decodeBlobText, getRepoBlob, getRepoBranches, getRepoDefaultBranch, getRepoLanguages, getRepoLog, getRepoTags, getRepoTree, type CommitHeadline, type RepoContext, type BranchEntry } from '../../lib/api/repos';
import { Avatar, ErrorState, PlaceholderAvatar, buttonStyles, cardStyles } from '../../components/common';
import { CloneDropdown, CodeView, FileRow, MarkdownBlock, OverviewFileRow, ReadmeCard, RepoBlobSkeleton, RepoTreeSkeleton, PathBreadcrumbs, TreeLatestCommitPanel, RepoLanguageBar, LoadingFilePath, TreeRowsSkeleton, RepoOverviewSidebarSkeleton } from '../../components/repo';
import { RepoFrame, useRepoQuery } from './shared';
import { blobHref, commitHref, commitsHref, countLines, decodeRoutePath, formatBytes, formatRelativeTime, getErrorMessage, getParentPath, imageLike, isDirectory, joinPath, markdownLike, safeDecode, sortedTreeEntries, svgLike, treeHref, updateRecentRepoLanguage, videoLike } from '../../lib/repo-utils';
import { LOADING_DELAY_MS, hasNamedRef, normalizeLanguages, resolveDefaultBranchName, resolveRouteRefAndPath, shouldAnimateNavigation, useDelayedLoading } from './code-helpers';
import { useAuth } from '../../lib/auth';

const readmeIsAvailable = (
	readme: { filename: string; contents: string } | undefined,
	entries: Array<{ name: string; mode: string }>,
) => {
	if (!readme) return false;
	if (readme.contents.trim().length > 0) return true;

	const filename = readme.filename.trim().toLowerCase();
	return filename.length > 0 && entries.some((entry) => !entry.mode.startsWith('004') && entry.name.toLowerCase() === filename);
};

const RepoCodePageLegacy: Component = () => {
	const params = useParams();
	const navigate = useNavigate();
	const repoQuery = useRepoQuery();
	const ref = createMemo(() => safeDecode(params.ref ?? 'HEAD'));
	const path = createMemo(() => decodeRoutePath(params.path));

	const codeQuery = createQuery(() => {
		const repo = repoQuery.data;
		return {
			queryKey: ['repo-code', repo?.repoDid, ref(), path()],
			enabled: Boolean(repo),
			queryFn: async () => {
				const currentRepo = repo!;
				const [tree, branches, tags, log] = await Promise.all([
					getRepoTree(currentRepo, ref(), path()),
					getRepoBranches(currentRepo),
					getRepoTags(currentRepo),
					getRepoLog(currentRepo, ref()),
				]);
				const defaultBranchName = branches.branches.find((branch) => branch.is_default)?.reference.name;
				const languageRef = ref() === 'HEAD' ? (defaultBranchName ?? ref()) : ref();
				const languages = await getRepoLanguages(currentRepo, languageRef);

				return { tree, branches, tags, languages, log };
			},
		};
	});

	return (
		<RepoFrame active="code" loadingFallback={<RepoTreeSkeleton nested={Boolean(path())} />}>
			<Switch>
				<Match when={codeQuery.isLoading}>
					<RepoTreeSkeleton nested={Boolean(path())} />
				</Match>
				<Match when={codeQuery.error}>
					<div class={cardStyles('p-6')}>
						<ErrorState message={getErrorMessage(codeQuery.error)} />
					</div>
				</Match>
				<Match when={Boolean(codeQuery.data && repoQuery.data)}>
					{(() => {
						const repo = repoQuery.data!;
						const data = codeQuery.data!;
						const languages = normalizeLanguages(data.languages?.languages ?? [], data.languages?.totalSize);
						const branches = data.branches?.branches ?? [];
						const tags = data.tags?.tags ?? [];
						const commits = data.log?.commits ?? [];
						const segments = path() ? path().split('/') : [];
						const defaultBranch =
							branches.find((branch) => branch.is_default)?.reference.name || ref();
						const parentPath = data.tree.dotdot ?? getParentPath(path());
						const sortedFiles = sortedTreeEntries(data.tree.files ?? []);
						const latestCommit =
							data.tree.lastCommit ??
							(commits[0]
							? {
									hash: commits[0].this,
									message: commits[0].message,
									when: commits[0].committer?.When || commits[0].author?.When || '',
									author: commits[0].author
										? {
												email: commits[0].author.Email,
												name: commits[0].author.Name,
												when: commits[0].author.When,
											}
										: undefined,
								}
							: undefined);

							return (
								<>
									<div class="tree">
									<RepoLanguageBar languages={languages} />

									<div class="flex items-center justify-between pb-5 gap-3 flex-wrap">
										<div class="flex gap-2 items-center">
											<select
												class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
												value={ref()}
												onChange={(event) =>
													navigate(treeHref(repo, event.currentTarget.value, path()))
												}
											>
												<optgroup label={`branches (${branches.length})`}>
													<For each={branches}>
														{(branch) => <option value={branch.reference.name}>{branch.reference.name}</option>}
													</For>
												</optgroup>
												<optgroup label={`tags (${tags.length})`}>
													<Show
														when={tags.length > 0}
														fallback={<option disabled>no tags found</option>}
													>
														<For each={tags.slice(0, 40)}>
															{(tag) => <option value={tag.name}>{tag.name}</option>}
														</For>
													</Show>
												</optgroup>
											</select>
											<Show when={ref() !== defaultBranch}>
												<button
													type="button"
													class={buttonStyles()}
													onClick={() => navigate(treeHref(repo, defaultBranch))}
												>
													default
												</button>
											</Show>
										</div>

											<div class="flex items-center gap-4 text-sm font-medium">
												<span class="inline-flex items-center gap-1">
													<GitCommitHorizontal class="size-4" />
													{data.log.total ?? commits.length}
												</span>
												<span class="inline-flex items-center gap-1">
													<GitBranch class="size-4" />
													{branches.length}
												</span>
												<span class="inline-flex items-center gap-1">
													<span>#</span>
													{tags.length}
												</span>
											</div>
										</div>

									<Show when={segments.length > 0}>
										<div class="pb-3 mb-3 border-b border-gray-200 dark:border-gray-700 text-sm text-gray-500 dark:text-gray-400 overflow-auto whitespace-nowrap">
											<A href={treeHref(repo, ref())} class="hover:underline">
												{repo.slug}
											</A>
											<For each={segments}>
												{(segment, index) => (
													<>
														<span class="px-1">/</span>
														<Show
															when={index() < segments.length - 1}
															fallback={<span class="text-gray-900 dark:text-gray-100">{segment}</span>}
														>
															<A
																href={treeHref(repo, ref(), segments.slice(0, index() + 1).join('/'))}
																class="hover:underline"
															>
																{segment}
															</A>
														</Show>
													</>
												)}
											</For>
											</div>
										</Show>

										<Show when={latestCommit}>
											{(commit) => (
												<div class="pb-2 mb-3 border-b border-gray-200 dark:border-gray-700 flex items-center flex-wrap gap-2">
													<div class="flex flex-wrap gap-1">
														<span class="text-sm text-gray-700 dark:text-gray-300">
															{commit().message.trim().split('\n')[0]}
														</span>
													</div>
													<span class="text-xs text-gray-500 dark:text-gray-400">
														{commit().hash.slice(0, 8)} · {formatRelativeTime(commit().when)}
													</span>
												</div>
											)}
										</Show>

										<div class="flex flex-col divide-y divide-gray-100 dark:divide-gray-700/60">
											<Show when={parentPath !== null}>
												<FileRow
													name=".."
													href={treeHref(repo, ref(), parentPath ?? '')}
													icon={<Folder class="size-4 flex-shrink-0" />}
												/>
											</Show>
												<For each={sortedFiles}>
												{(entry) => (
													<FileRow
														name={entry.name}
														href={
															isDirectory(entry)
																? treeHref(repo, ref(), joinPath(path(), entry.name))
																: blobHref(repo, ref(), joinPath(path(), entry.name))
														}
														icon={
															isDirectory(entry) ? (
																<Folder class="size-4 flex-shrink-0" />
															) : (
																<File class="size-4 flex-shrink-0" />
															)
														}
														lastCommit={entry.last_commit}
														lastCommitHref={entry.last_commit ? commitHref(repo, entry.last_commit.hash) : undefined}
														commitMessage={entry.last_commit?.message}
													/>
												)}
											</For>
										</div>
									</div>

								<Show when={readmeIsAvailable(data.tree.readme, sortedFiles) && data.tree.readme}>
									{(readme) => <ReadmeCard filename={readme().filename} markdown={readme().contents} />}
								</Show>
								</>
							);
						})()}
				</Match>
			</Switch>
		</RepoFrame>
	);
};

void RepoCodePageLegacy;

const getKnotHost = (knotUrl: string): string => {
	try {
		const url = new URL(knotUrl);
		let host = url.hostname;
		if (host === 'knot1.tangled.sh') {
			host = 'tangled.org';
		}
		return host;
	} catch {
		return knotUrl;
	}
};

interface EmptyRepoViewProps {
	repo: RepoContext;
	branches: BranchEntry[];
}

const EmptyRepoView: Component<EmptyRepoViewProps> = (props) => {
	const auth = useAuth();
	const isOwner = createMemo(() => {
		const current = auth.currentDid();
		return Boolean(current && current === props.repo.owner.did);
	});

	return (
		<section class="bg-white dark:bg-gray-800 px-6 py-6 rounded relative w-full mx-auto dark:text-white min-h-[250px] flex items-center justify-center">
			<Switch>
				<Match when={props.branches.length > 0}>
					<div class="flex flex-col items-center w-full max-w-xl mx-auto">
						<p class="text-center text-gray-500 dark:text-gray-400">
							This branch is empty. Other branches in this repository are populated:
						</p>
						<div class="mt-4 grid grid-cols-1 divide-y divide-gray-200 dark:divide-gray-700 rounded border border-gray-200 dark:border-gray-700 w-full bg-gray-50 dark:bg-gray-900/50">
							<For each={props.branches}>
								{(branch) => (
									<A
										href={treeHref(props.repo, branch.reference.name)}
										class="no-underline hover:bg-gray-100 dark:hover:bg-gray-800 block p-3 transition-colors duration-150"
									>
										<div class="flex items-center justify-between">
											<span class="font-medium text-blue-600 dark:text-blue-400">{branch.reference.name}</span>
											<span class="text-xs text-gray-500 dark:text-gray-400">
												{branch.commit?.Committer?.When ? formatRelativeTime(branch.commit.Committer.When) : ''}
											</span>
										</div>
									</A>
								)}
							</For>
						</div>
					</div>
				</Match>
				<Match when={isOwner()}>
					<div class="w-full max-w-2xl mx-auto">
						<div class="py-4 flex flex-col gap-5 text-gray-700 dark:text-gray-300">
							<p class="font-semibold text-lg text-gray-900 dark:text-white">This is an empty repository. To get started:</p>
							<div class="space-y-4">
								<p class="flex items-start gap-3">
									<span class="text-xs bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full w-5 h-5 flex items-center justify-center font-mono flex-shrink-0 mt-0.5">1</span>
									<span>
										First, generate a new{' '}
										<a
											href="https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key"
											target="_blank"
											rel="noopener noreferrer"
											class="underline text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
										>
											SSH key pair
										</a>
										.
									</span>
								</p>
								<p class="flex items-start gap-3">
									<span class="text-xs bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full w-5 h-5 flex items-center justify-center font-mono flex-shrink-0 mt-0.5">2</span>
									<span>
										Then add the public key to your account from the{' '}
										<A
											href="/settings/keys"
											class="underline text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
										>
											keys page
										</A>{' '}
										in your settings.
									</span>
								</p>
								<p class="flex items-start gap-3">
									<span class="text-xs bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full w-5 h-5 flex items-center justify-center font-mono flex-shrink-0 mt-0.5">3</span>
									<span class="break-all">
										Configure your remote to:{' '}
										<code class="bg-gray-100 dark:bg-gray-900 px-2 py-1 rounded font-mono text-sm border border-gray-200 dark:border-gray-800 text-gray-900 dark:text-gray-100 select-all block mt-1.5 w-fit">
											git@{getKnotHost(props.repo.knot)}:{props.repo.repoDid}
										</code>
									</span>
								</p>
								<p class="flex items-start gap-3">
									<span class="text-xs bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full w-5 h-5 flex items-center justify-center font-mono flex-shrink-0 mt-0.5">4</span>
									<span>Push!</span>
								</p>
							</div>
						</div>
					</div>
				</Match>
				<Match when={true}>
					<p class="text-gray-400 dark:text-gray-500 py-12 text-center text-lg">
						This is an empty repository.
					</p>
				</Match>
			</Switch>
		</section>
	);
};

export const RepoCodePage: Component = () => {
	const params = useParams();
	const navigate = useNavigate();
	const queryClient = useQueryClient();
	const repoQuery = useRepoQuery();
	const [pendingTreeHref, setPendingTreeHref] = createSignal<string>();
	let navigationAttempt = 0;
	const routeRef = createMemo(() => safeDecode(params.ref ?? 'HEAD'));
	const routePath = createMemo(() => decodeRoutePath(params.path));

	const branchesQuery = createQuery(() => {
		const repo = repoQuery.data;
		return {
			queryKey: ['repo-branches', repo?.repoDid],
			enabled: Boolean(repo),
			queryFn: async () => getRepoBranches(repo!),
		};
	});	const defaultBranchQuery = createQuery(() => {
		const repo = repoQuery.data;
		const hasBranches = branchesQuery.data && branchesQuery.data.branches.length > 0;
		return {
			queryKey: ['repo-default-branch', repo?.repoDid],
			enabled: Boolean(repo && hasBranches),
			queryFn: async () => getRepoDefaultBranch(repo!),
		};
	});
	const defaultBranch = createMemo(
		() =>
			defaultBranchQuery.data?.name ??
			defaultBranchQuery.data?.branch ??
			defaultBranchQuery.data?.reference?.name ??
			resolveDefaultBranchName(branchesQuery.data?.branches),
	);
	const defaultBranchReady = createMemo(() => Boolean(defaultBranchQuery.data || defaultBranchQuery.error || branchesQuery.data));
	const routeTarget = createMemo(() =>
		resolveRouteRefAndPath(
			routeRef(),
			routePath(),
			branchesQuery.data?.branches.map((branch) => branch.reference.name),
		),
	);
	const ref = createMemo(() => routeTarget().ref);
	const path = createMemo(() => routeTarget().path);
	const effectiveRef = createMemo(() => {
		const branches = branchesQuery.data?.branches;
		if (!branches || branches.length === 0) {
			return undefined;
		}
		return (ref() === 'HEAD' ? (defaultBranch() ?? (defaultBranchReady() ? 'HEAD' : undefined)) : ref());
	});
	const treeQuery = createQuery(() => {
		const repo = repoQuery.data;
		const currentRef = effectiveRef();
		return {
			queryKey: ['repo-tree', repo?.repoDid, currentRef, path()],
			enabled: Boolean(repo && currentRef),
			queryFn: async () => ({
				tree: await getRepoTree(repo!, currentRef!, path()),
				path: path(),
				ref: currentRef!,
			}),
			placeholderData: keepPreviousData,
		};
	});
	const selectedTreeRef = createMemo(() => {
		const currentTreeRef = treeQuery.data?.ref;
		if (currentTreeRef && currentTreeRef !== 'HEAD') return currentTreeRef;

		const currentRouteRef = ref();
		return currentRouteRef === 'HEAD' ? (defaultBranch() ?? currentRouteRef) : currentRouteRef;
	});
	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 logQuery = createQuery(() => {
		const repo = repoQuery.data;
		const currentRef = effectiveRef();
		return {
			queryKey: ['repo-log', repo?.repoDid, currentRef],
			enabled: Boolean(repo && currentRef),
			queryFn: async () => getRepoLog(repo!, currentRef!),
			placeholderData: keepPreviousData,
		};
	});
	const languageRef = effectiveRef;
	const languagesQuery = createQuery(() => {
		const repo = repoQuery.data;
		const currentLanguageRef = languageRef();
		return {
			queryKey: ['repo-languages', repo?.repoDid, currentLanguageRef],
			enabled: Boolean(repo && currentLanguageRef),
			queryFn: async () => getRepoLanguages(repo!, currentLanguageRef!),
			placeholderData: keepPreviousData,
		};
	});
	const overviewError = createMemo(
		() => branchesQuery.error || tagsQuery.error,
	);
	const overviewReady = createMemo(() =>
		Boolean(repoQuery.data && branchesQuery.data && tagsQuery.data),
	);
	const languagesLoading = createMemo(() => !languagesQuery.data && (languagesQuery.isLoading || languagesQuery.isFetching));
	const showOverviewSkeleton = useDelayedLoading(() => !overviewReady() && !overviewError());

	const isRefNotFoundError = (err: unknown): boolean => {
		if (!err) return false;
		if (typeof err === 'object') {
			const e = err as any;
			if (e.error === 'RefNotFound' || e.status === 404 || e.name === 'RefNotFound' || e.code === 'RefNotFound') {
				return true;
			}
		}
		const msg = getErrorMessage(err).toLowerCase();
		return (
			msg.includes('refnotfound') ||
			msg.includes('failed to access ref') ||
			msg.includes('reference not found') ||
			msg.includes('ref not found')
		);
	};

	const isEmptyRepo = createMemo(() => {
		return Boolean(branchesQuery.data && branchesQuery.data.branches.length === 0);
	});

	const isEmptyBranch = createMemo(() => {
		if (branchesQuery.data && branchesQuery.data.branches.length > 0) {
			if (treeQuery.error && isRefNotFoundError(treeQuery.error)) {
				return true;
			}
		}
		return false;
	});

	createEffect(() => {
		if (!treeQuery.isFetching) setPendingTreeHref(undefined);
	});

	createEffect(() => {
		const repo = repoQuery.data;
		const treeData = treeQuery.data;
		if (repo && treeData) {
			const tree = treeData.tree;
			const parent = tree?.parent ?? tree?.dotdot;
			if (tree && (!tree.files || tree.files.length === 0) && parent === treeData.path) {
				navigate(blobHref(repo, treeData.ref, treeData.path), { replace: true });
			}
		}
	});

	createEffect(() => {
		const repo = repoQuery.data;
		const langs = languagesQuery.data;
		if (repo && langs?.languages?.length) {
			const normalized = normalizeLanguages(langs.languages, langs.totalSize);
			const top = normalized[0]?.name;
			if (top) updateRecentRepoLanguage(repo.owner.handle, repo.slug, top);
		}
	});

	const handleFileClick = (
		event: MouseEvent,
		href: string,
		repo: Parameters<typeof getRepoBlob>[0],
		refName: string,
		filePath: string,
	) => {
		if (!shouldAnimateNavigation(event)) return;

		event.preventDefault();
		const attempt = ++navigationAttempt;
		setPendingTreeHref(href);

		const delay = new Promise<'delay'>((resolve) => {
			setTimeout(() => resolve('delay'), LOADING_DELAY_MS);
		});
		const load = queryClient
			.ensureQueryData({
				queryKey: ['blob', repo.repoDid, refName, filePath],
				queryFn: async () => getRepoBlob(repo, refName, filePath),
			})
			.then(
				() => 'loaded' as const,
				() => 'loaded' as const,
			);

		void Promise.race([delay, load]).then((result) => {
			if (attempt !== navigationAttempt) return;
			if (result === 'loaded') setPendingTreeHref(undefined);
			navigate(href);
		});
	};

	return (
		<RepoFrame active="code" loadingFallback={<RepoTreeSkeleton nested={Boolean(routePath())} />}>
			<Switch>
				<Match when={!overviewReady() && !overviewError() && !isEmptyRepo() && !isEmptyBranch() && showOverviewSkeleton()}>
					<RepoTreeSkeleton nested={Boolean(path())} />
				</Match>
				<Match when={isEmptyRepo()}>
					<EmptyRepoView repo={repoQuery.data!} branches={[]} />
				</Match>
				<Match when={isEmptyBranch()}>
					<EmptyRepoView repo={repoQuery.data!} branches={branchesQuery.data?.branches ?? []} />
				</Match>
				<Match when={overviewError()}>
					<div class={cardStyles('p-6')}>
						<ErrorState message={getErrorMessage(overviewError())} />
					</div>
				</Match>				<Match when={overviewReady()}>
					{(() => {
						const repo = repoQuery.data!;
						const treeData = () => treeQuery.data;
						const tree = () => treeData()?.tree;
						const languages = normalizeLanguages(languagesQuery.data?.languages ?? [], languagesQuery.data?.totalSize);
						const branches = branchesQuery.data?.branches ?? [];
						const tags = tagsQuery.data?.tags ?? [];
						const commits = () => logQuery.data?.commits ?? [];
						const activeRef = selectedTreeRef();
						const currentDefaultBranch = defaultBranch() ?? activeRef;
						const activeRefInOptions = hasNamedRef(activeRef, branches, tags);
						const treeRef = activeRef;
						const treePath = () => treeData()?.path ?? '';
						const parentPath = () => tree() ? (tree()!.dotdot ?? getParentPath(treePath())) : null;
						const sortedFiles = () => sortedTreeEntries(tree()?.files ?? []);
						const isNestedTreePath = () => treePath().length > 0;

							return (
								<>
								<section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white">
									<RepoLanguageBar languages={languages} loading={languagesLoading()} />

									<div class="flex items-center justify-between gap-4 pb-5">
										<div class="flex items-center gap-2">
												<select
													class="untangled-branch-select min-w-32 rounded border border-gray-300 bg-white px-3 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
													value={selectedTreeRef()}
													ref={(element) => {
														createEffect(() => {
															const nextValue = selectedTreeRef();
															branchesQuery.data?.branches.length;
															tagsQuery.data?.tags.length;
															queueMicrotask(() => {
																element.value = nextValue;
															});
														});
													}}
													onChange={(event) => navigate(treeHref(repo, event.currentTarget.value, path()))}
												>
													<Show when={!activeRefInOptions}>
														<option value={activeRef} selected>
															{activeRef}
														</option>
													</Show>
													<optgroup label={`branches (${branches.length})`}>
														<For each={branches}>
															{(branch) => (
																<option value={branch.reference.name} selected={branch.reference.name === activeRef}>
																	{branch.reference.name}
																</option>
															)}
														</For>
													</optgroup>
													<optgroup label={`tags (${tags.length})`}>
														<Show when={tags.length > 0} fallback={<option disabled>no tags found</option>}>
															<For each={tags.slice(0, 40)}>
																{(tag) => (
																	<option value={tag.name} selected={tag.name === activeRef}>
																		{tag.name}
																	</option>
																)}
															</For>
														</Show>
													</optgroup>
												</select>
											<button type="button" class={buttonStyles()} title="Compare branches or tags">
												<GitBranch class="size-4" />
											</button>
										</div>

											<CloneDropdown repo={repo} refName={activeRef} />
									</div>

									<Show when={isNestedTreePath()}>
										<div>
											<PathBreadcrumbs repo={repo} refName={treeRef} path={treePath()} withDivider />
											<Show when={tree()?.lastCommit}>
												{(commit) => <TreeLatestCommitPanel repo={repo} commit={commit()} />}
											</Show>
										</div>
									</Show>

									<div class={clsx('untangled-repo-overview', isNestedTreePath() && 'untangled-repo-overview--path')}>
										<div class="untangled-file-tree-pane min-w-0 pr-0 md:pr-6">
											<Show when={!treeQuery.isLoading} fallback={<TreeRowsSkeleton />}>
												<Switch fallback={
													<div class="space-y-3">
														<Show when={parentPath() !== null}>
															<OverviewFileRow
																name=".."
																href={treeHref(repo, treeRef, parentPath() ?? '')}
																icon={<Folder class="size-4 flex-shrink-0 fill-current" />}
																class={pendingTreeHref() === treeHref(repo, treeRef, parentPath() ?? '') ? 'untangled-file-row-loading' : undefined}
																onClick={(event) => {
																	if (shouldAnimateNavigation(event)) {
																		navigationAttempt += 1;
																		setPendingTreeHref(treeHref(repo, treeRef, parentPath() ?? ''));
																	}
																}}
															/>
														</Show>
														<For each={sortedFiles()}>
															{(entry) => {
																const entryPath = joinPath(treePath(), entry.name);
																const directory = isDirectory(entry);
																const href = directory ? treeHref(repo, treeRef, entryPath) : blobHref(repo, treeRef, entryPath);
																const currentRef = effectiveRef();

																let hoverTimer: ReturnType<typeof setTimeout> | undefined;

																const handleMouseEnter = () => {
																	hoverTimer = setTimeout(() => {
																		if (!directory) {
																			queryClient.prefetchQuery({
																				queryKey: ['blob', repo.repoDid, treeRef, entryPath],
																				queryFn: () => getRepoBlob(repo, treeRef, entryPath),
																			});
																			return;
																		}

																		queryClient.prefetchQuery({
																			queryKey: ['repo-tree', repo.repoDid, currentRef, entryPath],
																			queryFn: async () => ({
																				tree: await getRepoTree(repo, currentRef!, entryPath),
																				path: entryPath,
																				ref: currentRef!,
																			}),
																		});
																	}, 30);
																};
																const handleMouseLeave = () => {
																	clearTimeout(hoverTimer);
																};

																return (
																	<OverviewFileRow
																		name={entry.name}
																		href={href}
																		icon={
																			directory ? (
																				<Folder class="size-4 flex-shrink-0 fill-current" />
																			) : (
																				<File class="size-4 flex-shrink-0" />
																			)
																		}
																		lastCommit={entry.last_commit}
																		lastCommitHref={entry.last_commit ? commitHref(repo, entry.last_commit.hash) : undefined}
																		class={pendingTreeHref() === href ? 'untangled-file-row-loading' : undefined}
																		onMouseEnter={handleMouseEnter}
																		onMouseLeave={handleMouseLeave}
																		onClick={(event) => {
																			if (directory) {
																				if (shouldAnimateNavigation(event)) {
																					navigationAttempt += 1;
																					setPendingTreeHref(href);
																				}
																				return;
																			}

																			handleFileClick(event, href, repo, treeRef, entryPath);
																		}}
																	/>
																);
															}}
														</For>
													</div>
												}>
													<Match when={treeQuery.error}>
														<div class={cardStyles('p-4')}>
															<ErrorState message={getErrorMessage(treeQuery.error)} />
														</div>
													</Match>
												</Switch>
											</Show>
										</div>

										<Show when={!isNestedTreePath()}>
											<Show when={!logQuery.isLoading} fallback={<RepoOverviewSidebarSkeleton />}>
												<div class="min-w-0 md:pt-0 md:pl-6 border-gray-100 dark:border-gray-700 md:border-t-0 md:border-l">
													<A href={commitsHref(repo, activeRef)} class="flex items-center gap-2 pb-3 font-bold no-underline hover:underline dark:text-white">
														<List class="size-4" />
														<span>commits</span>
														<span class="rounded bg-gray-200 px-1 py-0.5 text-sm font-normal dark:bg-gray-700">
															{logQuery.data?.total ?? commits().length}
														</span>
													</A>
													<Switch fallback={
														<div class="space-y-6">
															<For each={commits().slice(0, 6)}>
																{(commit) => (
																	<div>
																		<div id="commit-message">
																			<div class="text-base cursor-pointer">
																				<div><div>
																					<a
																						href={`/${repo.owner.handle}/${repo.slug}/commit/${commit.this}`}
																						class="inline no-underline hover:underline dark:text-white"
																					>
																						{commit.message.trim().split('\n')[0]}
																					</a>
																				</div></div>
																			</div>
																		</div>
																		<div class="text-xs mt-2 text-gray-500 dark:text-gray-400 flex items-center flex-wrap">
																			<span class="font-mono">
																				<a
																					href={`/${repo.owner.handle}/${repo.slug}/commit/${commit.this}`}
																					class="no-underline hover:underline bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 px-2 py-1 rounded flex items-center gap-2"
																				>
																					{commit.this.slice(0, 8)}
																					<GitCommitHorizontal class="w-3 h-3" />
																				</a>
																			</span>
																			<span class="mx-1 before:content-['·'] before:select-none" />
																			<span class="flex items-center gap-1">
																				<Show
																					when={commit.author?.Email?.startsWith('did:') ? commit.author.Email : undefined}
																					fallback={<PlaceholderAvatar size="size-6" iconSize="size-4" />}
																				>
																					{(did) => <Avatar did={did()} size="size-6" />}
																				</Show>
																				<span>{commit.author?.Name || commit.committer?.Name || 'unknown author'}</span>
																			</span>
																			<div class="inline-block px-1 select-none after:content-['·']" />
																			<time datetime={commit.committer?.When || commit.author?.When || ''}>
																				{formatRelativeTime(commit.committer?.When || commit.author?.When || '')}
																			</time>
																			<Show when={activeRef === currentDefaultBranch}>
																				<div class="inline-block px-1 select-none after:content-['·']" />
																				<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-[2px] inline-flex items-center">
																					{currentDefaultBranch}
																				</span>
																			</Show>
																		</div>
																	</div>
																)}
															</For>
														</div>
													}>
														<Match when={logQuery.error}>
															<div class={cardStyles('p-4 mb-4')}>
																<ErrorState message={getErrorMessage(logQuery.error)} />
															</div>
														</Match>
													</Switch>

													<div class="mt-6 border-t border-gray-200 pt-4 dark:border-gray-700">
														<div class="flex items-center gap-2 pb-2 font-bold">
															<GitBranch class="size-4" />
															<span>branches</span>
															<span class="rounded bg-gray-200 px-1 py-0.5 text-sm font-normal dark:bg-gray-700">
																{branches.length}
															</span>
														</div>
														<div class="space-y-1 text-sm">
															<For each={branches.slice(0, 3)}>
																{(branch) => (
																	<div class="flex items-center justify-between gap-2">
																		<span>
																			{branch.reference.name}
																			<Show when={branch.is_default}>
																				<span class="ml-2 rounded bg-gray-200 px-1 py-0.5 font-mono text-xs dark:bg-gray-700">
																					default
																				</span>
																			</Show>
																		</span>
																		<span class="text-xs text-gray-500 dark:text-gray-400">
																			{branch.commit?.Committer?.When ? formatRelativeTime(branch.commit.Committer.When) : ''}
																		</span>
																	</div>
																)}
															</For>
														</div>
													</div>
												</div>
											</Show>
										</Show>
									</div>
								</section>

								<Show when={tree() && tree()!.readme && readmeIsAvailable(tree()!.readme, sortedFiles())}>
									<ReadmeCard
										filename={tree()!.readme!.filename}
										markdown={tree()!.readme!.contents}
										repo={repo}
										refName={activeRef}
										path={treePath()}
									/>
								</Show>
							</>
						);
					})()}
				</Match>
			</Switch>
		</RepoFrame>
	);
};

export const BlobPage: Component = () => {
	const params = useParams();
	const repoQuery = useRepoQuery();
	const [showRendered, setShowRendered] = createSignal(true);
	const [wrapContent, setWrapContent] = createSignal(false);
	const ref = createMemo(() => safeDecode(params.ref));
	const path = createMemo(() => decodeRoutePath(params.path));

	const blobQuery = createQuery(() => {
		const repo = repoQuery.data;
		return {
			queryKey: ['blob', repo?.repoDid, ref(), path()],
			enabled: Boolean(repo && path()),
			queryFn: async () => getRepoBlob(repo!, ref(), path()),
		};
	});

	createEffect(() => {
		const blob = blobQuery.data;
		setWrapContent(false);
		setShowRendered(Boolean(blob && (markdownLike(path(), blob.mimeType) || svgLike(path(), blob.mimeType))));
	});

	return (
		<RepoFrame active="code" loadingFallback={<RepoBlobSkeleton />}>
			<Switch>
				<Match when={blobQuery.isLoading && repoQuery.data}>
					<LoadingFilePath repo={repoQuery.data!} refName={ref()} path={path()} />
				</Match>
				<Match when={blobQuery.error}>
					<div class={cardStyles('p-6')}>
						<ErrorState message={getErrorMessage(blobQuery.error)} />
					</div>
				</Match>
				<Match when={Boolean(blobQuery.data && repoQuery.data)}>
					{(() => {
						const repo = repoQuery.data!;
						const blob = blobQuery.data!;
						const text = createMemo(() => decodeBlobText(blob));
						const imageUrl = createMemo(() => buildBlobDataUrl(blob));
						const rawUrl = createMemo(() => buildBlobDataUrl(blob));
						const isMarkdown = createMemo(() => markdownLike(path(), blob.mimeType));
						const isSvg = createMemo(() => svgLike(path(), blob.mimeType));
						const isVideo = createMemo(() => videoLike(path(), blob.mimeType));
						const isImage = createMemo(() => imageLike(path(), blob.mimeType));
						const showingText = createMemo(
							() => blob.content !== undefined && !blob.fileTooLarge && (!blob.isBinary || isSvg()),
						);
						const lineCount = createMemo(() => countLines(text()));
						const commitLink = (commit: Accessor<CommitHeadline>) => `/${repo.owner.handle}/${repo.slug}/commit/${commit().hash}`;

						const [blobUrl, setBlobUrl] = createSignal<string>();
						createEffect(() => {
							const bytes = decodeBlobBytes(blob);
							const webBlob = new Blob([bytes.buffer as any], { type: blob.mimeType });
							const url = URL.createObjectURL(webBlob);
							setBlobUrl(url);
							onCleanup(() => URL.revokeObjectURL(url));
						});

							return (
								<div class="untangled-blob-card peer">
									<div class="untangled-blob-header pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700">
									<div class="flex flex-col md:flex-row md:justify-between gap-2">
											<PathBreadcrumbs repo={repo} refName={ref()} path={path()} />

											<div class="untangled-blob-file-info text-gray-500 md:text-sm flex flex-wrap md:gap-0 items-center">
												<span>at <A href={treeHref(repo, ref())}>{ref()}</A></span>
												<Show when={showingText()}>
													<span class="select-none px-1 md:px-2">·</span>
													<span>{lineCount()} lines</span>
												</Show>
												<Show when={blob.size}>
													<span class="select-none px-1 md:px-2">·</span>
													<span>{formatBytes(blob.size!)}</span>
												</Show>
												<Show when={blobUrl()}>
													<span class="select-none px-1 md:px-2">·</span>
													<a href={blobUrl()!} target="_blank" rel="noreferrer" class="hover:underline">
														view raw
													</a>
												</Show>
												<Show when={isMarkdown() || isSvg()}>
													<span class="select-none px-1 md:px-2">·</span>
													<button type="button" onClick={() => setShowRendered((value) => !value)}>
														view {showRendered() ? 'code' : 'rendered'}
													</button>
												</Show>
												<Show when={showingText()}>
													<div class="hidden">
														<span class="select-none px-1 md:px-2">·</span>
														<label class="flex lowercase font-normal px-1 py-0 gap-1 text-xs md:text-sm">
															<input
																type="checkbox"
																checked={wrapContent()}
																onChange={(event) => setWrapContent(event.currentTarget.checked)}
															/>
															wrap content
														</label>
													</div>
												</Show>
											</div>
										</div>
									</div>

									<Show when={blob.lastCommit}>
										{(commit) => (
											<div class="untangled-blob-commit-panel">
												<div class="flex min-w-0 flex-wrap items-center gap-1">
													<Show
														when={commit().author?.email?.startsWith('did:') ? commit().author!.email : undefined}
														fallback={<PlaceholderAvatar size="size-6" iconSize="size-4" />}
													>
														{(did) => <Avatar did={did()} size="size-6" />}
													</Show>
													<span class="font-medium text-gray-700 dark:text-gray-200">
														{commit().author?.name || 'unknown author'}
													</span>
													<span class="select-none text-gray-400 dark:text-gray-500">·</span>
													<A href={commitLink(commit)} class="truncate text-gray-900 dark:text-gray-100">{commit().message.trim().split('\n')[0]}</A>
													<span class="select-none text-gray-400 dark:text-gray-500">·</span>
													<A href={commitLink(commit)} class="text-gray-400 dark:text-gray-500">{formatRelativeTime(commit().when)}</A>
												</div>
												<A
													href={commitLink(commit)}
													class="untangled-blob-commit-link"
												>
													{commit().hash.slice(0, 8)}
												</A>
											</div>
										)}
									</Show>

									<Switch>
									<Match when={blob.submodule}>
										<p class="untangled-blob-message text-center text-gray-400 dark:text-gray-500">
											This directory is a git submodule of{' '}
											<a href={blob.submodule!.url} class="underline">
												{blob.submodule!.url}
											</a>
											.
										</p>
									</Match>
									<Match when={blob.fileTooLarge}>
										<p class="untangled-blob-message text-center text-gray-400 dark:text-gray-500">
											This file is too large to render.
											</p>
										</Match>
										<Match when={isVideo() && rawUrl()}>
											<div class="untangled-blob-preview text-center">
												<video controls class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded">
													<source src={rawUrl()!} />
												</video>
											</div>
										</Match>
										<Match when={isImage() && imageUrl() && (!isSvg() || showRendered())}>
											<div class="untangled-blob-preview text-center">
												<img src={imageUrl()!} alt={path()} class="max-w-full h-auto mx-auto rounded border border-gray-200 dark:border-gray-700" />
											</div>
										</Match>
										<Match when={isMarkdown() && showRendered()}>
											<div class="untangled-blob-rendered overflow-auto relative">
												<MarkdownBlock
													markdown={text()}
													variant="readme"
													repo={repo}
													refName={ref()}
													path={getParentPath(path()) ?? ''}
												/>
											</div>
										</Match>
										<Match when={blob.content !== undefined}>
											<CodeView id="blob-contents" text={text()} wrap={wrapContent()} path={path()} />
										</Match>
										<Match when={true}>
											<p class="untangled-blob-message text-center text-gray-400 dark:text-gray-500">
												Previews are not supported for this file type.
										</p>
									</Match>
								</Switch>
								</div>
							);
						})()}
				</Match>
			</Switch>
		</RepoFrame>
	);
};
