import clsx from 'clsx';
import DOMPurify from 'dompurify';
import hljs from 'highlight.js/lib/common';
import {
	Check,
	Copy,
	Download,
	ExternalLink,
	FileText,
	File,
	Square,
	SquareCheckBig,
	BookMarked,
	GitFork,
	Star,
	CircleDot,
	GitPullRequest,
	LoaderCircle,
	MessageSquarePlus,
	Reply,
	X,
} from 'lucide-solid';
import { useAuth } from '../lib/auth';
import { resolveActor } from '../lib/api/identity';
import { marked } from 'marked';
import { A, useNavigate, useLocation } from '@solidjs/router';
import { For, Show, createMemo, createSignal, createEffect, onCleanup, onMount, type Component, type JSX } from 'solid-js';
import type { RepoContext, TreeEntry } from '../lib/api/repos';
import type { Did } from '@atcute/lexicons/syntax';
import { createQuery } from '../lib/atproto-query';
import { getRepo, getRepoLanguages, getRepoForkCount, getRepoByDid } from '../lib/api/repos';
import { getRepoStarCount } from '../lib/api/stars';
import { getRepoIssueCount } from '../lib/api/issues';
import { getRepoPullCount } from '../lib/api/pulls';
import { formatRelativeTime, languageColor, encodePath, formatLanguagePercent, blobHref, treeHref, getErrorMessage } from '../lib/repo-utils';
import { getTangledAppviewService } from '../lib/settings';
import { Avatar, SkeletonBlock, StateBadge, PlaceholderAvatar, buttonStyles, cardStyles, textareaStyles } from './common';
import { useRepoPreloader } from '../lib/preloading';


marked.setOptions({
	gfm: true,
	breaks: false,
});

marked.use({
	renderer: {
		code({ text, lang }) {
			const validLanguage = lang && hljs.getLanguage(lang) ? lang : 'plaintext';
			const highlighted = hljs.highlight(text, { language: validLanguage }).value;
			return `<pre><code class="hljs language-${validLanguage}">${highlighted}</code></pre>`;
		},
	},
});

DOMPurify.addHook('uponSanitizeAttribute', (_node, data) => {
	if (data.attrName === 'class') {
		const classes = data.attrValue.split(/\s+/);
		const filtered = classes.filter(
			(cls) => cls === 'hljs' || cls.startsWith('hljs-') || cls.startsWith('language-'),
		);
		if (filtered.length > 0) {
			data.attrValue = filtered.join(' ');
		} else {
			return (data.keepAttr = false);
		}
	}
});

export const RepoTabLink: Component<{
	active: boolean;
	href: string;
	icon: JSX.Element;
	label: string;
	meta?: string;
	reserveMeta?: boolean;
}> = (props) => (
	<A href={props.href} class="relative -mr-px group no-underline hover:no-underline">
		<div
			class={clsx(
				'untangled-repo-tab px-4 py-1 mr-1 text-black dark:text-white text-center relative rounded-t whitespace-nowrap',
				props.active ? 'untangled-repo-tab-active -mb-px bg-white dark:bg-gray-800' : 'untangled-repo-tab-idle',
			)}
		>
			<span class="flex items-center justify-center gap-2">
				{props.icon}
				{props.label}
				<Show when={props.meta !== undefined || props.reserveMeta}>
					<span
						aria-hidden={props.meta === undefined ? 'true' : undefined}
						class={clsx(
							'untangled-repo-tab-meta rounded bg-gray-200 px-1 text-sm text-gray-700 dark:bg-gray-700 dark:text-gray-200',
							props.meta === undefined && 'untangled-repo-tab-meta-placeholder',
						)}
					>
						{props.meta ?? '0'}
					</span>
				</Show>
			</span>
		</div>
	</A>
);

const skeletonRows = Array.from({ length: 7 });
const listSkeletonRows = Array.from({ length: 10 });
const skeletonCodeLines = Array.from({ length: 18 });
const prFileTreeSkeletonRows = Array.from({ length: 8 });
const prDiffFileSkeletons = Array.from({ length: 2 });
const prDiffCodeLines = Array.from({ length: 8 });

export const RepoLanguageBarSkeleton: Component = () => (
	<div class="group -my-4 -m-6 mb-4" aria-hidden="true">
		<div class="untangled-language-bar untangled-language-bar-loading">
			<SkeletonBlock class="untangled-language-bar-skeleton" />
		</div>
	</div>
);

export const TreeRowsSkeleton: Component = () => (
	<div class="space-y-3">
		<For each={skeletonRows}>
			{(_, index) => (
				<div class="untangled-file-row">
					<div class="flex min-w-0 items-center gap-2">
						<SkeletonBlock class="size-4 shrink-0 rounded-sm" />
						<SkeletonBlock class={index() % 3 === 0 ? 'h-5 w-1/3' : 'h-5 w-2/3'} />
					</div>
					<SkeletonBlock class="h-4 w-16 justify-self-end" />
				</div>
			)}
		</For>
	</div>
);

export const RepoOverviewSidebarSkeleton: Component = () => (
	<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">
		<div class="flex items-center gap-2 pb-3 font-bold">
			<SkeletonBlock class="size-4" />
			<SkeletonBlock class="h-5 w-20" />
			<SkeletonBlock class="h-5 w-8" />
		</div>
		<div class="space-y-6">
			<For each={Array.from({ length: 5 })}>
				{(_, index) => (
					<div>
						<SkeletonBlock class={index() % 2 === 0 ? 'h-5 w-5/6' : 'h-5 w-2/3'} />
						<div class="mt-2 flex flex-wrap items-center gap-2">
							<SkeletonBlock class="h-6 w-20" />
							<SkeletonBlock class="size-6 rounded-full" />
							<SkeletonBlock class="h-4 w-24" />
						</div>
					</div>
				)}
			</For>
		</div>
		<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">
				<SkeletonBlock class="size-4" />
				<SkeletonBlock class="h-5 w-20" />
				<SkeletonBlock class="h-5 w-8" />
			</div>
			<div class="space-y-2">
				<SkeletonBlock class="h-4 w-2/3" />
				<SkeletonBlock class="h-4 w-1/2" />
			</div>
		</div>
	</div>
);

export const RepoTreeSkeleton: Component<{ nested?: boolean }> = (props) => (
	<>
		<section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white">
			<RepoLanguageBarSkeleton />
			<div class="flex items-center justify-between gap-4 pb-5">
				<div class="flex items-center gap-2">
					<SkeletonBlock class="untangled-skeleton-branch-select" />
					<SkeletonBlock class="untangled-skeleton-compare-button" />
				</div>
				<SkeletonBlock class="untangled-skeleton-clone-button" />
			</div>

			<Show when={props.nested}>
				<div>
					<div class="pb-2 mb-3 border-b border-gray-200 dark:border-gray-700">
						<SkeletonBlock class="h-5 w-2/3" />
					</div>
					<div class="pb-2 mb-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between flex-wrap gap-2">
						<div class="flex min-w-0 flex-wrap items-center gap-2">
							<SkeletonBlock class="size-6 rounded-full" />
							<SkeletonBlock class="h-4 w-28" />
							<SkeletonBlock class="h-4 w-2/5" />
							<SkeletonBlock class="h-4 w-20" />
						</div>
						<SkeletonBlock class="h-6 w-20" />
					</div>
				</div>
			</Show>

			<div class={clsx('untangled-repo-overview', props.nested && 'untangled-repo-overview--path')}>
				<div class="untangled-file-tree-pane min-w-0 pr-0 md:pr-6">
					<TreeRowsSkeleton />
				</div>
				<Show when={!props.nested}>
					<RepoOverviewSidebarSkeleton />
				</Show>
			</div>
		</section>
		<div class="mt-4 rounded bg-white dark:bg-gray-800 shadow-sm w-full mx-auto overflow-hidden">
			<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex items-center gap-2">
				<SkeletonBlock class="size-4" />
				<SkeletonBlock class="h-4 w-24" />
			</div>
			<section class="px-6 py-6 overflow-auto">
				<div class="space-y-3">
					<SkeletonBlock class="h-5 w-3/4" />
					<SkeletonBlock class="h-5 w-11/12" />
					<SkeletonBlock class="h-5 w-2/3" />
					<SkeletonBlock class="h-5 w-5/6" />
				</div>
			</section>
		</div>
	</>
);

export const RepoBlobSkeleton: Component = () => (
	<div class={cardStyles('overflow-hidden')}>
		<div class="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 px-4 py-3 dark:border-gray-700">
			<div class="min-w-0 flex-1 space-y-2">
				<SkeletonBlock class="h-5 w-2/3" />
				<SkeletonBlock class="h-4 w-52" />
			</div>
			<SkeletonBlock class="h-8 w-24" />
		</div>
		<div class="flex items-center justify-between gap-4 bg-gray-900 px-4 py-3">
			<SkeletonBlock class="untangled-skeleton-on-dark h-5 w-2/3" />
			<SkeletonBlock class="untangled-skeleton-on-dark h-5 w-20" />
		</div>
		<div class="space-y-2 p-4">
			<For each={skeletonCodeLines}>
				{(_, index) => (
					<div class="untangled-skeleton-code-line">
						<SkeletonBlock class="h-4 w-8" />
						<SkeletonBlock class={index() % 5 === 0 ? 'h-4 w-1/2' : index() % 3 === 0 ? 'h-4 w-2/3' : 'h-4 w-11/12'} />
					</div>
				)}
			</For>
		</div>
	</div>
);

export const RepoListSkeleton: Component<{ kind: 'issues' | 'pulls' }> = (props) => (
	<div class="flex flex-col gap-3">
		<For each={listSkeletonRows}>
			{(_, index) => (
				<div class="rounded bg-white px-6 py-4 shadow-sm dark:bg-gray-800">
					<SkeletonBlock class={index() % 2 === 0 ? 'h-5 w-2/3' : 'h-5 w-1/2'} />
					<div class="mt-3 flex flex-wrap items-center gap-2">
						<SkeletonBlock class="h-7 w-20" />
						<SkeletonBlock class="size-5 rounded-full" />
						<SkeletonBlock class="h-4 w-24" />
						<SkeletonBlock class="h-4 w-16" />
					</div>
					<Show when={props.kind === 'pulls'}>
						<div class="mt-3 flex flex-wrap items-center gap-2">
							<SkeletonBlock class="h-4 w-14" />
							<SkeletonBlock class="h-5 w-20" />
							<SkeletonBlock class="h-4 w-10" />
							<SkeletonBlock class="h-5 w-24" />
						</div>
					</Show>
				</div>
			)}
		</For>
	</div>
);

export const RepoListPageSkeleton: Component<{
	kind: 'issues' | 'pulls';
	showCreate?: boolean;
}> = (props) => (
	<>
		<div class="rounded bg-white p-4 dark:bg-gray-800">
			<div class="untangled-filter-grid">
				<div class="flex items-stretch rounded border border-gray-300 dark:border-gray-700">
					<SkeletonBlock class="h-8 w-20 rounded-none" />
					<SkeletonBlock class="h-8 w-20 rounded-none" />
					<Show when={props.kind === 'pulls'}>
						<SkeletonBlock class="h-8 w-24 rounded-none" />
					</Show>
				</div>
				<div class="untangled-repo-filter-search">
					<SkeletonBlock class="h-8 w-full" />
				</div>
				<Show when={props.showCreate}>
					<SkeletonBlock class="h-8 w-28 justify-self-end" />
				</Show>
			</div>
		</div>
		<div class="mt-2">
			<RepoListSkeleton kind={props.kind} />
		</div>
	</>
);

export const RepoFormSkeleton: Component<{ titleWidth?: string }> = (props) => (
	<div class={cardStyles('p-6')}>
		<div class="flex flex-col gap-4">
			<SkeletonBlock class={clsx('h-8', props.titleWidth ?? 'w-40')} />
			<SkeletonBlock class="h-10 w-full" />
			<SkeletonBlock class="h-64 w-full" />
			<SkeletonBlock class="h-9 w-32 self-end" />
		</div>
	</div>
);

export const PullComposeSkeleton: Component = () => (
	<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">
		<div class="flex flex-col gap-6">
			<For each={Array.from({ length: 3 })}>
				{(_, index) => (
					<section class="untangled-new-pr-step">
						<div class="untangled-new-pr-heading">
							<SkeletonBlock class="size-6 rounded-full" />
							<SkeletonBlock class={index() === 0 ? 'h-4 w-16' : 'h-4 w-20'} />
						</div>
						<div class="ml-10 flex flex-col gap-3">
							<SkeletonBlock class="h-14 w-full" />
							<SkeletonBlock class={index() === 1 ? 'h-56 w-full' : 'h-10 w-2/3'} />
						</div>
					</section>
				)}
			</For>
		</div>
	</div>
);

const IssueThreadSkeleton: Component = () => (
	<div class="grid grid-cols-1 gap-4 w-full md:grid-cols-10">
		<div class="col-span-1 min-w-0 md:col-span-8">
			<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white">
				<SkeletonBlock class="h-7 w-3/4" />
				<div class="mt-3 flex flex-wrap items-center gap-2">
					<SkeletonBlock class="h-7 w-20" />
					<SkeletonBlock class="size-6 rounded-full" />
					<SkeletonBlock class="h-4 w-28" />
				</div>
				<div class="mt-5 space-y-3">
					<SkeletonBlock class="h-5 w-11/12" />
					<SkeletonBlock class="h-5 w-4/5" />
					<SkeletonBlock class="h-5 w-2/3" />
				</div>
				<SkeletonBlock class="mt-5 size-8" />
			</section>
			<div class="mt-4 flex flex-col gap-4">
				<div class="overflow-hidden rounded border border-gray-200 bg-gray-50 shadow-sm dark:border-gray-700 dark:bg-gray-800/50">
					<div class="flex items-center gap-2 bg-white px-6 py-4 text-sm dark:bg-gray-800">
						<SkeletonBlock class="size-8 rounded-full" />
						<SkeletonBlock class="h-5 w-32" />
					</div>
					<div class="space-y-3 p-6">
						<SkeletonBlock class="h-5 w-full" />
						<SkeletonBlock class="h-5 w-5/6" />
					</div>
				</div>
				<div class="bg-white dark:bg-gray-800 rounded shadow-sm py-4 px-4 relative flex min-w-0 flex-col gap-3">
					<SkeletonBlock class="h-4 w-24" />
					<SkeletonBlock class="h-28 w-full" />
					<SkeletonBlock class="h-8 w-24 self-end" />
				</div>
			</div>
		</div>
		<aside class="col-span-1 flex min-w-0 flex-col gap-6 text-sm md:col-span-2">
			<section>
				<SkeletonBlock class="h-4 w-16" />
				<SkeletonBlock class="mt-2 h-4 w-20" />
			</section>
			<section>
				<SkeletonBlock class="h-4 w-20" />
				<SkeletonBlock class="mt-2 h-4 w-20" />
			</section>
			<section>
				<SkeletonBlock class="h-4 w-28" />
				<div class="untangled-overlap-avatars mt-2 flex">
					<SkeletonBlock class="size-8 rounded-full" />
					<SkeletonBlock class="size-8 rounded-full" />
				</div>
			</section>
			<section>
				<SkeletonBlock class="h-4 w-16" />
				<SkeletonBlock class="mt-2 h-12 w-full" />
			</section>
		</aside>
	</div>
);

const PullHeaderSkeleton: Component = () => (
	<div class="untangled-pr-header-grid">
		<section class="untangled-pr-summary-card">
			<SkeletonBlock class="h-8 w-3/4" />
			<div class="mt-3 flex flex-wrap items-center gap-2">
				<SkeletonBlock class="h-7 w-20" />
				<SkeletonBlock class="h-4 w-16" />
				<SkeletonBlock class="size-6 rounded-full" />
				<SkeletonBlock class="h-4 w-28" />
				<SkeletonBlock class="h-4 w-16" />
				<SkeletonBlock class="h-4 w-14" />
				<SkeletonBlock class="h-5 w-24" />
			</div>
			<div class="mt-8 space-y-3">
				<SkeletonBlock class="h-5 w-11/12" />
				<SkeletonBlock class="h-5 w-4/5" />
				<SkeletonBlock class="h-5 w-2/3" />
			</div>
			<div class="mt-4 flex">
				<SkeletonBlock class="ml-auto h-9 w-24" />
			</div>
		</section>

		<aside class="untangled-pr-meta">
			<section class="untangled-pr-meta-section">
				<SkeletonBlock class="h-4 w-16" />
				<SkeletonBlock class="mt-2 h-4 w-20" />
			</section>
			<section class="untangled-pr-meta-section">
				<SkeletonBlock class="h-4 w-20" />
				<SkeletonBlock class="mt-2 h-4 w-20" />
			</section>
			<section class="untangled-pr-meta-section">
				<SkeletonBlock class="h-4 w-32" />
				<div class="mt-3 flex flex-wrap gap-2">
					<SkeletonBlock class="size-7 rounded-full" />
					<SkeletonBlock class="size-7 rounded-full" />
					<SkeletonBlock class="size-7 rounded-full" />
				</div>
			</section>
			<section class="untangled-pr-meta-section">
				<SkeletonBlock class="h-4 w-16" />
				<div class="mt-2 flex min-w-0 items-center gap-2">
					<SkeletonBlock class="h-5 w-full" />
					<SkeletonBlock class="size-5" />
				</div>
			</section>
		</aside>
	</div>
);

export const PullDiffSkeleton: Component = () => (
	<section class="untangled-pr-diff-breakout">
		<div class="untangled-pr-diff-toolbar">
			<div class="untangled-pr-diff-toolbar-main">
				<SkeletonBlock class="h-8 w-8" />
				<SkeletonBlock class="h-7 w-20" />
				<SkeletonBlock class="h-4 w-28" />
				<SkeletonBlock class="hidden h-5 w-px sm:inline-block" />
				<SkeletonBlock class="h-4 w-12" />
				<SkeletonBlock class="h-6 w-16" />
			</div>
			<div class="untangled-pr-diff-toolbar-actions">
				<SkeletonBlock class="h-8 w-28" />
			</div>
			<div class="untangled-pr-diff-toolbar-history">
				<SkeletonBlock class="h-8 w-8" />
			</div>
		</div>

		<div class="untangled-pr-diff-grid">
			<aside class="untangled-pr-file-tree" aria-label="changed files loading">
				<div class="untangled-pr-file-tree-list">
					<For each={prFileTreeSkeletonRows}>
						{(_, index) => (
							<div class="untangled-pr-file-tree-entry">
								<SkeletonBlock class="size-4 shrink-0 rounded-sm" />
								<SkeletonBlock class={index() % 3 === 0 ? 'h-4 w-24' : 'h-4 w-32'} />
							</div>
						)}
					</For>
				</div>
			</aside>

			<div class="untangled-pr-diff-files">
				<For each={prDiffFileSkeletons}>
					{(_, fileIndex) => (
						<div class="untangled-pr-diff-file">
							<div class="untangled-pr-diff-file-header">
								<div class="flex min-w-0 items-center gap-2">
									<SkeletonBlock class="untangled-skeleton-on-dark size-4 shrink-0" />
									<SkeletonBlock class="untangled-skeleton-on-dark h-7 w-20" />
									<SkeletonBlock class={fileIndex() === 0 ? 'untangled-skeleton-on-dark h-5 w-2/3' : 'untangled-skeleton-on-dark h-5 w-1/2'} />
									<SkeletonBlock class="untangled-skeleton-on-dark size-4 shrink-0 rounded-full" />
								</div>
								<SkeletonBlock class="untangled-skeleton-on-dark h-4 w-20 shrink-0" />
							</div>
							<div class="untangled-pr-diff-ellipsis">
								<SkeletonBlock class="untangled-skeleton-on-dark mx-auto h-4 w-10" />
							</div>
							<div class="space-y-2 bg-white p-4 dark:bg-gray-900">
								<For each={prDiffCodeLines}>
									{(_, 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>

			<aside class="untangled-pr-history">
				<div class="untangled-pr-history-card">
					<header>
						<SkeletonBlock class="h-5 w-20" />
						<div>
							<SkeletonBlock class="h-4 w-16" />
							<SkeletonBlock class="h-4 w-20" />
						</div>
					</header>
					<div class="untangled-pr-history-submission">
						<SkeletonBlock class="size-8 rounded-full" />
						<div class="min-w-0 flex-1 space-y-2">
							<SkeletonBlock class="h-4 w-2/3" />
							<SkeletonBlock class="h-4 w-1/2" />
						</div>
					</div>
					<div class="px-4">
						<SkeletonBlock class="h-11 w-full" />
					</div>
					<div class="p-4">
						<div class="flex items-center gap-2">
							<SkeletonBlock class="size-8 rounded-full" />
							<SkeletonBlock class="h-5 w-32" />
						</div>
						<div class="mt-4 space-y-3">
							<SkeletonBlock class="h-5 w-full" />
							<SkeletonBlock class="h-5 w-5/6" />
						</div>
					</div>
					<div class="untangled-pr-history-comment">
						<SkeletonBlock class="h-28 w-full" />
						<SkeletonBlock class="h-9 w-24" />
					</div>
				</div>
			</aside>
		</div>
	</section>
);

const PullThreadSkeleton: Component = () => (
	<div class="untangled-pr-detail">
		<PullHeaderSkeleton />
		<PullDiffSkeleton />
	</div>
);

export const RepoThreadSkeleton: Component<{ kind: 'issue' | 'pull' }> = (props) => (
	<Show when={props.kind === 'pull'} fallback={<IssueThreadSkeleton />}>
		<PullThreadSkeleton />
	</Show>
);

const defaultRepoFrameSkeletonContent = (active: 'code' | 'issues' | 'pulls') =>
	active === 'code' ? <RepoTreeSkeleton /> : <RepoListPageSkeleton kind={active === 'issues' ? 'issues' : 'pulls'} />;

export const RepoFrameSkeleton: Component<{
	active: 'code' | 'issues' | 'pulls';
	children?: JSX.Element;
}> = (props) => (
	<>
		<section id="repo-header" class="mb-2 py-2 px-4">
			<div class="flex flex-col sm:flex-row items-start gap-4 justify-between mb-2">
				<div class="flex gap-2">
					<div class="flex items-center gap-2 flex-wrap text-lg">
						<SkeletonBlock class="size-8 rounded-full" />
						<SkeletonBlock class="h-6 w-40" />
					</div>
				</div>
				<div class="hidden sm:block sm:flex-shrink-0">
					<div class="untangled-repo-actions">
						<SkeletonBlock class="h-8 w-32" />
						<SkeletonBlock class="h-8 w-20" />
					</div>
				</div>
			</div>
			<div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
				<SkeletonBlock class="h-5 w-72" />
				<SkeletonBlock class="h-5 w-24" />
			</div>
		</section>
		<section class="w-full flex flex-col">
			<nav class="w-full pl-4 overflow-auto">
				<div class="flex z-60">
					<RepoTabLink active={props.active === 'code'} href="#" icon={<SkeletonBlock class="size-4" />} label="overview" />
					<RepoTabLink active={props.active === 'issues'} href="#" icon={<SkeletonBlock class="size-4" />} label="issues" reserveMeta />
					<RepoTabLink active={props.active === 'pulls'} href="#" icon={<SkeletonBlock class="size-4" />} label="pulls" reserveMeta />
				</div>
			</nav>
			{props.children ?? defaultRepoFrameSkeletonContent(props.active)}
		</section>
	</>
);

export const OverviewFileRow: Component<{
	name: string;
	href: string;
	icon: JSX.Element;
	lastCommit?: TreeEntry['last_commit'];
	lastCommitHref?: string;
	class?: string;
	onMouseEnter?: JSX.EventHandlerUnion<HTMLAnchorElement, MouseEvent>;
	onMouseLeave?: JSX.EventHandlerUnion<HTMLAnchorElement, MouseEvent>;
	onClick?: JSX.EventHandlerUnion<HTMLAnchorElement, MouseEvent>;
}> = (props) => (
	<div class={clsx('untangled-file-row', props.class)}>
		<A href={props.href} class="flex min-w-0 items-center gap-2 no-underline hover:underline dark:text-white" onClick={props.onClick} onMouseEnter={props.onMouseEnter} onMouseLeave={props.onMouseLeave}>
			{props.icon}
			<span class="truncate">{props.name}</span>
		</A>
		<Show when={props.lastCommit?.when}>
			{(when) => (
				<Show
					when={props.lastCommitHref}
					fallback={<span class="untangled-file-row-time text-sm text-gray-500 dark:text-gray-400">{formatRelativeTime(when())}</span>}
				>
					{(href) => (
						<A href={href()} class="untangled-file-row-time text-sm text-gray-500 no-underline hover:underline dark:text-gray-400">
							{formatRelativeTime(when())}
						</A>
					)}
				</Show>
			)}
		</Show>
	</div>
);

export const FileRow: Component<{
	name: string;
	href: string;
	icon: JSX.Element;
	lastCommit?: TreeEntry['last_commit'];
	lastCommitHref?: string;
	commitMessage?: string;
}> = (props) => (
	<div class="grid grid-cols-12 gap-4 items-center py-1">
		<div class="col-span-8 md:col-span-4">
			<A href={props.href} class="no-underline hover:underline dark:text-white">
				<div class="flex items-center gap-2">
					{props.icon}
					<span class="truncate">{props.name}</span>
				</div>
			</A>
		</div>
		<div class="col-span-0 md:col-span-6 hidden md:block overflow-hidden">
			<Show when={props.commitMessage}>
				<div class="text-gray-500 dark:text-gray-400 block truncate">
					{props.commitMessage?.trim().split('\n')[0]}
				</div>
			</Show>
		</div>
		<div class="col-span-4 md:col-span-2 text-sm text-right text-gray-500 dark:text-gray-400">
			<Show when={props.lastCommit?.when}>
				{(when) => (
					<Show when={props.lastCommitHref} fallback={<span>{formatRelativeTime(when())}</span>}>
						{(href) => (
							<A href={href()} class="text-gray-500 no-underline hover:underline dark:text-gray-400">
								{formatRelativeTime(when())}
							</A>
						)}
					</Show>
				)}
			</Show>
		</div>
	</div>
);

export const ReadmeCard: Component<{
	filename?: string;
	markdown: string;
	repo?: RepoContext;
	refName?: string;
	path?: string;
}> = (props) => (
	<div class="mt-4 rounded bg-white dark:bg-gray-800 shadow-sm w-full mx-auto overflow-hidden">
		<Show when={props.filename}>
			<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex items-center gap-2">
				<FileText class="w-4 h-4 text-gray-600 dark:text-gray-400" />
				<span class="font-mono text-sm text-gray-800 dark:text-gray-200">{props.filename}</span>
			</div>
		</Show>
		<section class="px-6 py-6 overflow-auto">
			<MarkdownBlock
				markdown={props.markdown}
				variant="readme"
				repo={props.repo}
				refName={props.refName}
				path={props.path}
			/>
		</section>
	</div>
);

const cloneSshHost = (repo: RepoContext): string => {
	try {
		const host = new URL(repo.knot).hostname;
		return host === 'knot1.tangled.sh' ? 'tangled.org' : host;
	} catch {
		const host = repo.knot.replace(/^https?:\/\//, '').split('/')[0].replace(/:\d+$/, '');
		return host === 'knot1.tangled.sh' ? 'tangled.org' : host;
	}
};

const appviewArchiveUrl = (repo: RepoContext, refName: string): string => {
	const owner = encodeURIComponent(repo.owner.handle);
	const slug = encodeURIComponent(repo.slug);
	const ref = encodeURIComponent(refName);
	return new URL(`/${owner}/${slug}/archive/${ref}`, getTangledAppviewService()).toString();
};

const selectContents = (element: HTMLElement) => {
	const selection = window.getSelection();
	if (!selection) return;

	const range = document.createRange();
	range.selectNodeContents(element);
	selection.removeAllRanges();
	selection.addRange(range);
};

const CloneUrlItem: Component<{
	label: string;
	handleUrl: string;
	permaUrl: string;
	permalink: boolean;
}> = (props) => {
	const [copied, setCopied] = createSignal(false);
	const visibleUrl = createMemo(() => (props.permalink ? props.permaUrl : props.handleUrl));
	let copyTimer: ReturnType<typeof setTimeout> | undefined;

	onCleanup(() => clearTimeout(copyTimer));

	const copy = () => {
		clearTimeout(copyTimer);
		void navigator.clipboard?.writeText(visibleUrl());
		setCopied(true);
		copyTimer = setTimeout(() => setCopied(false), 2000);
	};

	return (
		<div class="mt-4">
			<label class="mb-1 block text-xs font-medium text-gray-700 normal-case dark:text-gray-300">
				{props.label}
			</label>
			<div class="untangled-clone-url-row">
				<span
					class="untangled-clone-url-text"
					onClick={(event) => selectContents(event.currentTarget)}
					title={visibleUrl()}
				>
					{visibleUrl()}
				</span>
				<button
					type="button"
					class="untangled-clone-copy"
					title="Copy to clipboard"
					aria-label={`Copy ${props.label} clone URL`}
					onClick={copy}
				>
					<Show when={copied()} fallback={<Copy class="size-4" />}>
						<Check class="size-4" />
					</Show>
				</button>
			</div>
		</div>
	);
};

export const CloneDropdown: Component<{ repo: RepoContext; refName: string }> = (props) => {
	const [open, setOpen] = createSignal(false);
	const [permalink, setPermalink] = createSignal(false);
	const sshHost = createMemo(() => cloneSshHost(props.repo));
	let root: HTMLDivElement | undefined;

	onMount(() => {
		const closeOnOutsideClick = (event: MouseEvent) => {
			if (!open() || !root || root.contains(event.target as Node)) return;
			setOpen(false);
		};
		const closeOnEscape = (event: KeyboardEvent) => {
			if (event.key === 'Escape') setOpen(false);
		};

		document.addEventListener('mousedown', closeOnOutsideClick);
		document.addEventListener('keydown', closeOnEscape);
		onCleanup(() => {
			document.removeEventListener('mousedown', closeOnOutsideClick);
			document.removeEventListener('keydown', closeOnEscape);
		});
	});

	return (
		<div class="untangled-clone-dropdown" ref={root}>
			<button
				type="button"
				class={clsx(buttonStyles('primary'), 'cursor-pointer px-4')}
				aria-expanded={open()}
				aria-haspopup="dialog"
				onClick={() => setOpen(!open())}
			>
				<Download class="size-4" />
				<span class="hidden md:inline">code</span>
			</button>
			<Show when={open()}>
				<div class="untangled-clone-popover" role="dialog" aria-label="Clone this repository">
					<div class="flex items-center justify-between gap-3">
						<h3 class="m-0 text-sm font-semibold text-gray-900 dark:text-white">
							clone this repository
						</h3>
						<label class="flex cursor-pointer items-center gap-1 text-xs font-normal normal-case text-gray-700 dark:text-gray-300">
							<input
								type="checkbox"
								class="sr-only"
								checked={permalink()}
								onChange={(event) => setPermalink(event.currentTarget.checked)}
							/>
							<Show when={permalink()} fallback={<Square class="size-4" />}>
								<SquareCheckBig class="size-4" />
							</Show>
							<span>use permalink</span>
						</label>
					</div>

					<CloneUrlItem
						label="HTTPS"
						handleUrl={`https://tangled.org/${props.repo.owner.handle}/${props.repo.slug}`}
						permaUrl={`https://tangled.org/${props.repo.repoDid}`}
						permalink={permalink()}
					/>
					<CloneUrlItem
						label="SSH"
						handleUrl={`git@${sshHost()}:${props.repo.owner.handle}/${props.repo.slug}`}
						permaUrl={`git@${sshHost()}:${props.repo.repoDid}`}
						permalink={permalink()}
					/>

					<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
						for self-hosted knots, clone URLs may differ based on your setup.
					</p>

					<a
						href={appviewArchiveUrl(props.repo, props.refName)}
						class={clsx(buttonStyles('primary'), 'mt-4 flex w-full justify-center text-sm hover:text-gray-100')}
					>
						<Download class="size-4" />
						download tar.gz
					</a>
				</div>
			</Show>
		</div>
	);
};

export const CommentCard: Component<{
	author: string;
	did: string;
	createdAt: string;
	markdown: string;
	replyDid?: string | null;
	variant?: 'default' | 'issue';
}> = (props) => (
	<Show
		when={props.variant === 'issue'}
		fallback={
			<div class="overflow-hidden rounded border border-gray-200 bg-gray-50 shadow-sm dark:border-gray-700 dark:bg-gray-800/50">
				<div class="flex items-center gap-2 bg-white px-6 py-4 text-sm text-gray-500 dark:bg-gray-800 dark:text-gray-400">
					<Avatar did={props.did} size="size-8" />
					<span class="text-gray-700 dark:text-gray-200">{props.author}</span>
					<span class="select-none">·</span>
					<span>{formatRelativeTime(props.createdAt)}</span>
				</div>
				<div class="min-w-0 border-t border-gray-200 px-6 py-4 dark:border-gray-700">
					<MarkdownBlock markdown={props.markdown} />
				</div>
			</div>
		}
	>
	<div class="overflow-hidden rounded border border-gray-200 bg-gray-50 shadow-sm dark:border-gray-700 dark:bg-gray-800/50">
		<div class="flex gap-2 rounded bg-white px-6 py-4 dark:bg-gray-800">
			<div class="shrink-0">
				<Avatar did={props.did} size="size-8 mr-1" />
			</div>
			<div class="min-w-0 flex-1">
				<div class="text-sm text-gray-500 dark:text-gray-400">
					<span class="text-gray-700 dark:text-gray-200">{props.author}</span>
					<span class="px-1 select-none">·</span>
					<span>{formatRelativeTime(props.createdAt)}</span>
				</div>
				<MarkdownBlock markdown={props.markdown} />
			</div>
		</div>
		<Show when={props.replyDid}>
			{(did) => (
				<div class="flex items-center gap-2 border-t border-gray-200 px-6 py-3 text-sm text-gray-500 dark:border-gray-700 dark:text-gray-400">
					<Avatar did={did()} size="size-6" />
					<span>Leave a reply...</span>
				</div>
			)}
		</Show>
	</div>
	</Show>
);

export const resolveRelativePath = (currentPath: string, relativePath: string): string => {
	let normalized = relativePath;
	if (normalized.startsWith('/')) {
		normalized = normalized.slice(1);
		currentPath = '';
	}

	if (normalized.startsWith('./')) {
		normalized = normalized.slice(2);
	}

	if (!currentPath) {
		return normalized;
	}

	const currentSegments = currentPath.split('/').filter(Boolean);
	const relativeSegments = normalized.split('/');

	for (const segment of relativeSegments) {
		if (segment === '..') {
			currentSegments.pop();
		} else if (segment !== '.' && segment !== '') {
			currentSegments.push(segment);
		}
	}

	return currentSegments.join('/');
};

export const MarkdownBlock: Component<{
	markdown: string;
	variant?: 'default' | 'readme' | 'code';
	repo?: RepoContext;
	refName?: string;
	path?: string;
}> = (props) => {
	const navigate = useNavigate();

	const html = createMemo(() => {
		const options: any = { async: false };

		if (props.repo && props.refName) {
			const repo = props.repo;
			const refName = props.refName;
			options.walkTokens = (token: any) => {
				if (token.type === 'link' || token.type === 'image') {
					const href = token.href;
					if (href && !href.startsWith('#') && !/^[a-z][a-z0-9+.-]*:/i.test(href)) {
						const [pathAndQuery, fragment] = href.split('#');
						const [pathPart, queryPart] = pathAndQuery.split('?');

						const resolvedPath = resolveRelativePath(props.path ?? '', pathPart);
						const typeSegment = token.type === 'image' ? 'raw' : 'tree';
						const newHref = `/${repo.owner.handle}/${repo.slug}/${typeSegment}/${encodeURIComponent(refName)}${resolvedPath ? `/${encodePath(resolvedPath)}` : ''}${queryPart ? `?${queryPart}` : ''}${fragment ? `#${fragment}` : ''}`;
						token.href = newHref;
					}
				}
			};
		}

		return DOMPurify.sanitize(marked.parse(props.markdown, options) as unknown as string, {
			ADD_ATTR: ['class'],
		});
	});

	const classes = createMemo(() =>
		props.variant === 'readme'
			? 'untangled-readme-prose prose prose-sm sm:prose-base min-w-0 max-w-none dark:prose-invert prose-p:text-gray-800 dark:prose-p:text-gray-200 prose-li:text-gray-800 dark:prose-li:text-gray-200 prose-pre:max-w-full prose-pre:overflow-x-auto prose-pre:rounded-md prose-pre:bg-gray-950 dark:prose-pre:bg-gray-950 prose-code:break-words'
			: props.variant === 'code'
				? 'font-mono text-sm whitespace-pre-wrap break-words'
				: 'prose prose-sm sm:prose-base min-w-0 max-w-none break-words untangled-anywhere dark:prose-invert prose-p:text-gray-800 dark:prose-p:text-gray-200 prose-li:text-gray-800 dark:prose-li:text-gray-200 prose-pre:max-w-full prose-pre:overflow-x-auto prose-pre:rounded-md prose-pre:bg-gray-950 dark:prose-pre:bg-gray-950 prose-code:break-words prose-a:break-all',
	);

	const onMarkdownClick = (event: MouseEvent) => {
		const anchor = (event.target as HTMLElement).closest('a');
		if (!anchor || event.defaultPrevented) {
			return;
		}

		if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
			return;
		}

		const href = anchor.getAttribute('href');
		if (!href) {
			return;
		}

		if (href.startsWith('/') && !href.startsWith('//')) {
			event.preventDefault();
			navigate(href);
			return;
		}

		try {
			const url = new URL(href, window.location.origin);
			if (url.hostname === 'tangled.org' || url.hostname === 'tangled.sh') {
				event.preventDefault();
				navigate(url.pathname + url.search + url.hash);
			}
		} catch {
			// ignore
		}
	};

	return <div class={classes()} innerHTML={html()} onClick={onMarkdownClick} />;
};



export const AsideCard: Component<{ title: string; children: JSX.Element }> = (props) => (
	<div class={cardStyles('p-4')}>
		<div class="mb-3 font-medium">{props.title}</div>
		{props.children}
	</div>
);

export const BranchPill: Component<{ name: string }> = (props) => (
	<span class="inline-flex items-center rounded bg-gray-200 px-1 py-0.5 font-mono text-xs text-gray-700 dark:bg-gray-700 dark:text-gray-200">
		{props.name}
	</span>
);

export const AtUriPanel: Component<{ uri: string }> = (props) => {
	const selectUri: JSX.EventHandlerUnion<HTMLSpanElement, MouseEvent> = (event) => {
		const selection = window.getSelection();
		if (!selection) return;

		const range = document.createRange();
		range.selectNodeContents(event.currentTarget);
		selection.removeAllRanges();
		selection.addRange(range);
	};

	return (
		<section class="untangled-at-uri-panel">
			<div class="untangled-at-uri-header">
				<span>AT URI</span>
				<div class="untangled-at-uri-actions">
					<button
						type="button"
						class="untangled-at-uri-action"
						aria-label="copy AT URI"
						title="Copy AT URI"
						onClick={() => void navigator.clipboard?.writeText(props.uri)}
					>
						<Copy class="size-4" />
					</button>
					<a
						href={`https://pdsls.dev/${props.uri}`}
						target="_blank"
						rel="noreferrer"
						class="untangled-at-uri-action"
						aria-label="view AT URI in PDSls"
						title="View in PDSls"
					>
						<ExternalLink class="size-4" />
					</a>
				</div>
			</div>
			<span class="untangled-at-uri" title={props.uri} onClick={selectUri}>
				{props.uri}
			</span>
		</section>
	);
};

export interface RepoCardProps {
	owner: string;
	name: string;
	description?: string;
	isFork?: boolean;
	language?: string;
	stars?: number;
	forks?: number;
	openIssues?: number;
	openPRs?: number;
	compact?: boolean;
	href?: string;
	noBorder?: boolean;
	showOwner?: boolean;
	repoDid?: string;
}

export interface RepoStatsListProps {
	repoDid?: string;
	owner?: string;
	name?: string;
	language?: string;
	stars?: number;
	forks?: number;
	openIssues?: number;
	openPRs?: number;
}

export const RepoStatsList: Component<RepoStatsListProps> = (props) => {
	const repoContextQuery = createQuery(() => ({
		queryKey: props.repoDid ? ['repo-by-did', props.repoDid] : ['repo-context', props.owner, props.name],
		queryFn: async () => {
			if (props.repoDid) {
				return getRepoByDid(props.repoDid as Did);
			} else if (props.owner && props.name) {
				return getRepo(props.owner, props.name);
			}
			return null;
		},
		enabled: Boolean(props.repoDid || (props.owner && props.name)),
		staleTime: 300_000,
	}));

	const repoContext = () => repoContextQuery.data;

	const starCountQuery = createQuery(() => ({
		queryKey: ['repo-stars-count', repoContext()?.repoDid],
		queryFn: () => getRepoStarCount(repoContext()!),
		enabled: !!repoContext() && props.stars === undefined,
		staleTime: 300_000,
	}));

	const forkCountQuery = createQuery(() => ({
		queryKey: ['repo-fork-count', repoContext()?.repoDid],
		queryFn: () => getRepoForkCount(repoContext()!),
		enabled: !!repoContext() && props.forks === undefined,
		staleTime: 300_000,
	}));

	const issueCountQuery = createQuery(() => ({
		queryKey: ['repo-issues-count', repoContext()?.repoDid],
		queryFn: () => getRepoIssueCount(repoContext()!),
		enabled: !!repoContext() && props.openIssues === undefined,
		staleTime: 300_000,
	}));

	const pullCountQuery = createQuery(() => ({
		queryKey: ['repo-pulls-count', repoContext()?.repoDid],
		queryFn: () => getRepoPullCount(repoContext()!),
		enabled: !!repoContext() && props.openPRs === undefined,
		staleTime: 300_000,
	}));

	const languagesQuery = createQuery(() => ({
		queryKey: ['repo-languages', repoContext()?.repoDid],
		queryFn: () => getRepoLanguages(repoContext()!, 'HEAD'),
		enabled: !!repoContext() && props.language === undefined,
		staleTime: 300_000,
	}));

	const resolvedLanguage = () => {
		if (props.language !== undefined) return props.language;
		const list = languagesQuery.data?.languages;
		if (!list || list.length === 0) return undefined;
		const sorted = [...list].sort((a, b) => b.percentage - a.percentage);
		return sorted[0].name;
	};

	const langColor = () => {
		const lang = resolvedLanguage();
		return lang ? languageColor(lang) : undefined;
	};

	const stars = () => props.stars !== undefined ? props.stars : starCountQuery.data;
	const forks = () => props.forks !== undefined ? props.forks : forkCountQuery.data;
	const openIssues = () => props.openIssues !== undefined ? props.openIssues : issueCountQuery.data;
	const openPRs = () => props.openPRs !== undefined ? props.openPRs : pullCountQuery.data;

	const isMetricsLoading = () => {
		const contextLoading = repoContextQuery.isLoading && (props.repoDid !== undefined || (props.owner !== undefined && props.name !== undefined));
		const langLoading = props.language === undefined && (languagesQuery.isLoading || contextLoading);
		const starsLoading = props.stars === undefined && (starCountQuery.isLoading || contextLoading);
		const forksLoading = props.forks === undefined && (forkCountQuery.isLoading || contextLoading);
		const issuesLoading = props.openIssues === undefined && (issueCountQuery.isLoading || contextLoading);
		const prsLoading = props.openPRs === undefined && (pullCountQuery.isLoading || contextLoading);
		return langLoading || starsLoading || forksLoading || issuesLoading || prsLoading;
	};

	return (
		<Show
			when={!isMetricsLoading()}
			fallback={
				<div class="text-gray-400 text-sm font-mono inline-flex items-center gap-3 mt-auto h-5">
					<SkeletonBlock class="h-4 w-16" />
					<SkeletonBlock class="h-4 w-4" />
					<SkeletonBlock class="h-4 w-8" />
					<SkeletonBlock class="h-4 w-6" />
				</div>
			}
		>
			<div class="text-gray-400 text-sm font-mono inline-flex flex-wrap gap-3 mt-auto">
				<Show when={resolvedLanguage()}>
					<div class="flex gap-2 items-center text-sm">
						<span
							class="rounded-full inline-block shrink-0"
							style={{ 'background-color': langColor(), width: '10px', height: '10px' }}
						/>
						<span>{resolvedLanguage()}</span>
					</div>
				</Show>
				<Show when={stars() !== undefined && stars()! > 0}>
					<div class="flex gap-1 items-center text-sm">
						<Star class="w-3 h-3 fill-current" />
						<span>{stars()}</span>
					</div>
				</Show>
				<Show when={forks() !== undefined && forks()! > 0}>
					<div class="flex gap-1 items-center text-sm">
						<GitFork class="w-3 h-3" />
						<span>{forks()}</span>
					</div>
				</Show>
				<Show when={openIssues() !== undefined && openIssues()! > 0}>
					<div class="flex gap-1 items-center text-sm">
						<CircleDot class="w-3 h-3" />
						<span>{openIssues()}</span>
					</div>
				</Show>
				<Show when={openPRs() !== undefined && openPRs()! > 0}>
					<div class="flex gap-1 items-center text-sm">
						<GitPullRequest class="w-3 h-3" />
						<span>{openPRs()}</span>
					</div>
				</Show>
			</div>
		</Show>
	);
};

export const RepoCard: Component<RepoCardProps> = (props) => {
	const linkHref = () => props.href || `/${props.owner}/${props.name}`;
	const preloadRepo = useRepoPreloader();

	return (
		<div
			class={clsx(
				props.compact ? 'focus-within:bg-gray-100 dark:focus-within:bg-gray-800/80' : 'min-h-32',
				'py-4 px-6 gap-1 flex flex-col drop-shadow-sm bg-white dark:bg-gray-800',
				!props.noBorder && 'border border-gray-200 dark:border-gray-700 rounded-sm'
			)}
		>
			<div class="font-medium dark:text-white flex items-center justify-between">
				<div class="flex items-center min-w-0 flex-1 mr-2">
					<Show
						when={props.isFork}
						fallback={<BookMarked class="w-4 h-4 mr-1.5 shrink-0" />}
					>
						<GitFork class="w-4 h-4 mr-1.5 shrink-0" />
					</Show>
					<A
						href={linkHref()}
						class={clsx(
							'truncate min-w-0 no-underline hover:underline text-black dark:text-white font-bold',
							props.compact && 'focus:outline-none'
						)}
						{...preloadRepo(props.owner, props.name)}
					>
						<Show when={props.showOwner !== false} fallback={props.name}>
							{props.owner}/{props.name}
						</Show>
					</A>
				</div>
			</div>

			<Show when={props.description}>
				<div class="text-gray-600 dark:text-gray-300 text-sm line-clamp-2">
					{props.description}
				</div>
			</Show>

			<RepoStatsList {...props} />
		</div>
	);
};

export const RepoCardSkeleton: Component<{ compact?: boolean; noBorder?: boolean }> = (props) => {
	return (
		<div
			class={clsx(
				props.compact ? 'focus-within:bg-gray-100 dark:focus-within:bg-gray-800/80' : 'min-h-32',
				'py-4 px-6 gap-1 flex flex-col drop-shadow-sm bg-white dark:bg-gray-800',
				!props.noBorder && 'border border-gray-200 dark:border-gray-700 rounded-sm'
			)}
		>
			<div class="font-medium dark:text-white flex items-center justify-between">
				<div class="flex items-center min-w-0 flex-1 mr-2 gap-2">
					<SkeletonBlock class="w-4 h-4 shrink-0 rounded-sm" />
					<SkeletonBlock class="h-5 w-1/3" />
				</div>
			</div>

			<div class="space-y-2 mt-2">
				<SkeletonBlock class="h-4 w-5/6" />
				<SkeletonBlock class="h-4 w-2/3" />
			</div>

			<div class="flex items-center gap-3 mt-auto h-5">
				<SkeletonBlock class="h-4 w-16" />
				<SkeletonBlock class="h-4 w-4" />
				<SkeletonBlock class="h-4 w-8" />
				<SkeletonBlock class="h-4 w-6" />
			</div>
		</div>
	);
};

export const StringCardSkeleton: Component = () => (
	<div class="border border-gray-200 dark:border-gray-700 rounded-sm">
		<div class="py-4 px-6 rounded bg-white dark:bg-gray-800 flex flex-col gap-2">
			<div class="flex items-center gap-2">
				<SkeletonBlock class="h-5 w-1/3" />
			</div>
			<div class="space-y-2 mt-1">
				<SkeletonBlock class="h-4 w-5/6" />
				<SkeletonBlock class="h-4 w-2/3" />
			</div>
			<div class="mt-2 flex gap-4">
				<SkeletonBlock class="h-3 w-16" />
				<SkeletonBlock class="h-3 w-16" />
			</div>
		</div>
	</div>
);

export const StringPageSkeleton: Component = () => (
	<div class="flex flex-col gap-4">
		<section class="mb-2 py-2 px-4">
			<div class="flex flex-col sm:flex-row items-start gap-4 justify-between">
				<div class="flex flex-col gap-2 w-full sm:w-1/2">
					<div class="flex items-center gap-2">
						<SkeletonBlock class="size-6 rounded-full shrink-0" />
						<SkeletonBlock class="h-5 w-1/4" />
						<span class="text-gray-400">/</span>
						<SkeletonBlock class="h-5 w-1/3" />
					</div>
					<div class="flex flex-col gap-1 mt-1">
						<SkeletonBlock class="h-4 w-1/3" />
					</div>
				</div>
				<div class="w-full sm:w-fit flex items-center gap-2">
					<SkeletonBlock class="h-8 w-24" />
					<SkeletonBlock class="h-8 w-16" />
				</div>
			</div>
		</section>

		<section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full shadow-sm">
			<div class="flex flex-col md:flex-row md:justify-between md:items-center pb-2 mb-3 border-b border-gray-200 dark:border-gray-700">
				<SkeletonBlock class="h-5 w-1/3 mb-2 md:mb-0" />
				<SkeletonBlock class="h-5 w-1/4" />
			</div>
			<div class="space-y-2 py-2 overflow-hidden">
				<For each={Array.from({ length: 36 })}>
					{(_, index) => (
						<div class="untangled-skeleton-code-line">
							<SkeletonBlock class="h-4 w-8" />
							<SkeletonBlock class={
								index() % 7 === 0 ? 'h-4 w-1/3' : 
								index() % 5 === 0 ? 'h-4 w-1/2' : 
								index() % 4 === 0 ? 'h-4 w-3/4' : 
								index() % 3 === 0 ? 'h-4 w-5/6' : 
								'h-4 w-11/12'
							} />
						</div>
					)}
				</For>
			</div>
		</section>
	</div>
);

export const IssueOrPullRow: Component<{
	title: string;
	number: number | string;
	state: 'open' | 'closed' | 'merged';
	kind: 'issue' | 'pull';
	author: { did: string; handle: string };
	createdAt: string;
	href: string;
	meta?: JSX.Element;
	children?: JSX.Element;
	onMouseEnter?: JSX.EventHandlerUnion<HTMLAnchorElement, MouseEvent>;
	onMouseLeave?: JSX.EventHandlerUnion<HTMLAnchorElement, MouseEvent>;
}> = (props) => {
	return (
		<A
			href={props.href}
			class="untangled-list-row"
			onMouseEnter={props.onMouseEnter}
			onMouseLeave={props.onMouseLeave}
		>
			<div class="min-w-0">
				<div class="text-base text-gray-900 dark:text-gray-100">
					{props.title}{' '}
					<span class="text-gray-500 dark:text-gray-400 font-normal">#{props.number}</span>
				</div>
				<div class="mt-3 flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
					<StateBadge state={props.state} kind={props.kind} />
					<Avatar did={props.author.did} size="size-5" />
					<span>{props.author.handle}</span>
					<span>·</span>
					<span>{formatRelativeTime(props.createdAt)}</span>
					{props.meta}
				</div>
				{props.children}
			</div>
		</A>
	);
};

export const ThreadCommentView: Component<{
	id?: string;
	author: { did: string; handle: string };
	createdAt: string;
	markdown: string;
	isAuthor?: boolean;
	isReply?: boolean;
	class?: string;
}> = (props) => (
	<div
		id={props.id}
		class={clsx(
			'flex gap-2 w-full mx-auto text-sm text-gray-900 dark:text-gray-100',
			props.isReply && 'pr-4',
			props.class || 'py-4'
		)}
	>
		<div class="shrink-0 h-fit relative z-10">
			<Avatar did={props.author.did} size="size-8 mr-1" />
		</div>
		<div class="min-w-0 flex-1">
			<div class="flex flex-wrap items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400 mb-0.5">
				<A
					class="text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white font-medium hover:underline"
					href={`/${props.author.did}`}
				>
					{props.author.handle}
				</A>
				<Show when={props.isAuthor}>
					<span class="text-xs text-gray-400 dark:text-gray-500">(author)</span>
				</Show>
				<span class="select-none">·</span>
				<Show when={props.id} fallback={<span>{formatRelativeTime(props.createdAt)}</span>}>
					<a href={`#${props.id}`} class="hover:underline">
						{formatRelativeTime(props.createdAt)}
					</a>
				</Show>
			</div>
			<div class="min-w-0 break-words text-gray-800 dark:text-gray-200">
				<MarkdownBlock markdown={props.markdown} />
			</div>
		</div>
	</div>
);

const commitSummary = (message: string) => message.trim().split('\n')[0] || 'no commit message';

export const PathBreadcrumbs: Component<{
	repo: Parameters<typeof treeHref>[0];
	refName: string;
	path: string;
	withDivider?: boolean;
}> = (props) => {
	const segments = createMemo(() => props.path.split('/').filter(Boolean));

	return (
		<div class={clsx('text-base', props.withDivider && 'pb-2 mb-3 border-b border-gray-200 dark:border-gray-700')}>
			<div class="overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500">
				<A href={treeHref(props.repo, props.refName)} class="text-gray-500 dark:text-gray-400 hover:underline">
					{props.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(props.repo, props.refName, segments().slice(0, index() + 1).join('/'))}
									class="text-gray-500 dark:text-gray-400 hover:underline"
								>
									{segment}
								</A>
							</Show>
						</>
					)}
				</For>
			</div>
		</div>
	);
};

export const TreeLatestCommitPanel: Component<{
	repo: Parameters<typeof treeHref>[0];
	commit: {
		hash: string;
		message: string;
		when: string;
		author?: {
			email: string;
			name: string;
		};
	};
}> = (props) => (
	<div class="pb-2 mb-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between flex-wrap text-sm gap-2">
		<div class="flex min-w-0 flex-wrap items-center gap-1">
			<Show
				when={props.commit.author?.email?.startsWith('did:') ? props.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">{props.commit.author?.name || 'unknown author'}</span>
			<span class="select-none text-gray-400 dark:text-gray-500">·</span>
			<A
				href={`/${props.repo.owner.handle}/${props.repo.slug}/commit/${props.commit.hash}`}
				class="min-w-0 truncate no-underline hover:underline text-gray-900 dark:text-white"
			>
				{commitSummary(props.commit.message)}
			</A>
			<span class="select-none text-gray-400 dark:text-gray-500">·</span>
			<span class="text-gray-500 dark:text-gray-400">{formatRelativeTime(props.commit.when)}</span>
		</div>
		<A
			href={`/${props.repo.owner.handle}/${props.repo.slug}/commit/${props.commit.hash}`}
			class="inline-flex w-fit items-center rounded bg-gray-100 px-2 py-1 font-mono text-xs text-gray-700 no-underline hover:underline dark:bg-gray-900 dark:text-gray-300"
		>
			{props.commit.hash.slice(0, 8)}
		</A>
	</div>
);

export const RepoLanguageBar: Component<{
	languages: Array<{ name: string; percentage: number; color?: string }>;
	loading?: boolean;
}> = (props) => (
	<Show when={props.languages.length > 0} fallback={<Show when={props.loading}><RepoLanguageBarSkeleton /></Show>}>
		<details class="group -my-4 -m-6 mb-4">
			<summary class="untangled-language-bar">
				<For each={props.languages}>
					{(language) => {
						const name = language.name || 'Other';
						const color = language.color || languageColor(name);
						return (
							<div
								class="untangled-language-segment"
								title={`${name} ${formatLanguagePercent(language.percentage)}`}
								style={{
									width: `${language.percentage}%`,
									'background-color': color,
								}}
							/>
						);
					}}
				</For>
			</summary>
			<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex items-center gap-4 flex-wrap">
				<For each={props.languages}>
					{(language) => {
						const name = language.name || 'Other';
						const color = language.color || languageColor(name);
						return (
							<div class="flex flex-grow items-center justify-center gap-2 text-xs">
								<div
									class="size-2 rounded-full"
									style={{
										background: `radial-gradient(circle at 35% 35%, color-mix(in srgb, ${color} 70%, white), ${color} 30%, color-mix(in srgb, ${color} 85%, black))`,
									}}
								/>
								<div>
									{name}{' '}
									<span class="text-gray-500 dark:text-gray-400">
										{formatLanguagePercent(language.percentage)}
									</span>
								</div>
							</div>
						);
					}}
				</For>
			</div>
		</details>
	</Show>
);

export const LoadingFilePath: Component<{
	repo: Parameters<typeof treeHref>[0];
	refName: string;
	path: string;
}> = (props) => {
	const segments = createMemo(() => props.path.split('/').filter(Boolean));
	const filename = createMemo(() => segments().at(-1) ?? props.path);

	return (
		<div class="untangled-blob-card">
			<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={props.repo} refName={props.refName} path={props.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(props.repo, props.refName)}>{props.refName}</A>
						</span>
					</div>
				</div>
			</div>
			<div class="space-y-3">
				<OverviewFileRow
					name={filename()}
					href={blobHref(props.repo, props.refName, props.path)}
					icon={<File class="size-4 flex-shrink-0" />}
					class="untangled-file-row-loading"
				/>
			</div>
		</div>
	);
};

export { CodeView, DiffView, PullDiffView } from './code-view';

export interface GenericComment {
	uri: string;
	cid?: string;
	rkey: string;
	author: { did: string; handle: string };
	value: {
		body: string;
		createdAt: string;
		replyTo?: any;
	};
}

export interface GenericCommentThread {
	item: GenericComment;
	replies: GenericComment[];
}

export const CommentThreadsSection: Component<{
	threads: GenericCommentThread[];
	authorDid?: string;
	title?: string;
	onReplySubmit?: (body: string, parentComment: GenericComment) => Promise<void>;
}> = (props) => {
	const auth = useAuth();
	const location = useLocation();
	const [replyingTo, setReplyingTo] = createSignal<GenericComment | null>(null);
	const [replyBody, setReplyBody] = createSignal('');
	const [replyWorking, setReplyWorking] = createSignal(false);
	const [replyError, setReplyError] = createSignal<string | null>(null);

	createEffect(() => {
		const hash = location.hash;
		const cleanHash = hash.startsWith('#') ? hash.slice(1) : hash;
		if (cleanHash && cleanHash.startsWith('comment-') && props.threads.length > 0) {
			setTimeout(() => {
				const element = document.getElementById(cleanHash);
				if (element) {
					// 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 viewerQuery = createQuery(() => ({
		queryKey: ['viewer', auth.currentDid()],
		enabled: Boolean(auth.currentDid()),
		queryFn: async () => resolveActor(auth.currentDid()!),
	}));

	const handleReplySubmit = async (e: SubmitEvent, parent: GenericComment) => {
		e.preventDefault();
		if (!props.onReplySubmit || !replyBody().trim() || replyWorking()) return;

		setReplyWorking(true);
		setReplyError(null);
		try {
			await props.onReplySubmit(replyBody(), parent);
			setReplyBody('');
			setReplyingTo(null);
		} catch (err) {
			setReplyError(getErrorMessage(err));
		} finally {
			setReplyWorking(false);
		}
	};

	return (
		<div class={clsx(props.title && 'mt-6', 'space-y-4')}>
			<Show when={props.title}>
				<h3 class="text-lg font-bold border-b border-gray-200 dark:border-gray-700 pb-2 dark:text-white mb-4">
					{props.title}
				</h3>
			</Show>
			<div class="space-y-4">
				<For each={props.threads}>
					{(thread) => (
						<div class="untangled-thread-group">
							<ThreadCommentView
								id={`comment-${thread.item.rkey}`}
								author={thread.item.author}
								createdAt={thread.item.value.createdAt}
								markdown={thread.item.value.body}
								isAuthor={props.authorDid ? thread.item.author.did === props.authorDid : false}
								class="untangled-thread-comment"
							/>
							<Show when={thread.replies.length > 0}>
								<div class="untangled-thread-replies-list">
									<For each={thread.replies}>
										{(reply) => (
											<ThreadCommentView
												id={`comment-${reply.rkey}`}
												author={reply.author}
												createdAt={reply.value.createdAt}
												markdown={reply.value.body}
												isAuthor={props.authorDid ? reply.author.did === props.authorDid : false}
												isReply={true}
												class="untangled-thread-reply"
											/>
										)}
									</For>
								</div>
							</Show>

							<Show when={props.onReplySubmit && auth.agent()}>
								<Show
									when={replyingTo()?.uri === thread.item.uri}
									fallback={
										<button
											type="button"
											class="untangled-thread-reply-trigger"
											onClick={() => {
												setReplyingTo(thread.item);
												setReplyBody('');
												setReplyError(null);
											}}
										>
											<Show when={auth.currentDid()}>
												{(did) => <Avatar did={did()} size="size-8 mr-1" />}
											</Show>
											<span class="w-full p-0">Leave a reply...</span>
										</button>
									}
								>
									<form
										onSubmit={(event) => void handleReplySubmit(event, thread.item)}
										class="untangled-thread-reply-form"
									>
										<Show when={viewerQuery.data}>
											{(viewer) => (
												<div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
													<Avatar did={viewer().did} size="size-6" />
													<span>{viewer().handle || viewer().did}</span>
												</div>
											)}
										</Show>
										<textarea
											value={replyBody()}
											onInput={(event) => setReplyBody(event.currentTarget.value)}
											class={textareaStyles()}
											rows="3"
											placeholder="Leave a reply..."
										/>
										<Show when={replyError()}>
											<div class="text-sm text-red-500 mt-1">{replyError()}</div>
										</Show>
										<div class="flex flex-wrap items-stretch justify-end gap-2 text-sm text-gray-500 dark:text-gray-400">
											<button
												type="button"
												class={clsx(buttonStyles(), 'text-red-500 dark:text-red-400')}
												onClick={() => setReplyingTo(null)}
											>
												<X class="size-4" />
												cancel
											</button>
											<button
												type="submit"
												class={buttonStyles('primary')}
												disabled={replyWorking() || !replyBody().trim()}
											>
												<Show when={replyWorking()} fallback={<Reply class="size-4" />}>
													<LoaderCircle class="size-4 animate-spin" />
												</Show>
												reply
											</button>
										</div>
									</form>
								</Show>
							</Show>
						</div>
					)}
				</For>
			</div>
		</div>
	);
};

export const CommentComposer: Component<{
	value: string;
	onValueChange: (val: string) => void;
	onSubmit: (event: SubmitEvent) => void;
	working?: boolean;
	submitLabel?: string;
	placeholder?: string;
	error?: string | null;
}> = (props) => {
	const auth = useAuth();
	const viewerQuery = createQuery(() => ({
		queryKey: ['viewer', auth.currentDid()],
		enabled: Boolean(auth.currentDid()),
		queryFn: async () => resolveActor(auth.currentDid()!),
	}));

	return (
		<form onSubmit={props.onSubmit} class="group/form w-full flex flex-col">
			<div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full border border-gray-200 dark:border-gray-700">
				<div class="text-sm pb-2 text-gray-500 dark:text-gray-400">
					<Show when={viewerQuery.data} fallback={<div class="h-6" />}>
						{(viewer) => (
							<A href={`/${viewer().handle || viewer().did}`} class="flex items-center gap-1 hover:underline text-gray-500 dark:text-gray-400 w-fit">
								<Avatar did={viewer().did} size="size-6" />
								<span>{viewer().handle || viewer().did}</span>
							</A>
						)}
					</Show>
				</div>
				<textarea
					rows={5}
					placeholder={props.placeholder ?? 'Add to the discussion. Markdown is supported.'}
					class={textareaStyles()}
					required
					value={props.value}
					onInput={(event) => props.onValueChange(event.currentTarget.value)}
				/>
				<Show when={props.error}>
					<div id="comment-error" class="error text-sm text-red-500 mt-1">{props.error}</div>
				</Show>
			</div>
			<div class="flex gap-2 mt-2">
				<button
					id="comment-button"
					type="submit"
					class="btn-create p-2 flex items-center gap-2 no-underline hover:no-underline"
					disabled={props.working || !props.value.trim()}
				>
					<Show when={props.working} fallback={<MessageSquarePlus class="w-4 h-4" />}>
						<LoaderCircle class="w-4 h-4 animate-spin" />
					</Show>
					<span>{props.submitLabel ?? 'Comment'}</span>
				</button>
			</div>
		</form>
	);
};




