<script lang="ts">
	import { onMount, tick } from 'svelte';
	import { enumerate, match, parse, RecordPathParseError, type PathInfo } from 'recordpath';
	import JsonDisplay from '$lib/JsonDisplay.svelte';
	import { fetchRecord, isAtUri } from '$lib/slingshot';

	const SAMPLE = {
		$type: 'app.bsky.feed.post',
		text: 'Check out this project by @alice.bsky.social! Really impressive work on atproto tooling.',
		langs: ['en'],
		createdAt: '2026-04-15T12:38:49.982Z',
		reply: {
			root: {
				cid: 'bafyreieac34fnjyhuuzvgdnsyeeueyn45se5kuk6yppesn25gydjf5m5hy',
				uri: 'at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/app.bsky.feed.post/3mjjvykdfo22r'
			},
			parent: {
				cid: 'bafyreibm3lim4hfn4fggpbtxkuoyyjokbonyetkyqfmr6qegqrgatxkhue',
				uri: 'at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/app.bsky.feed.post/3mjkj5zd7lc2b'
			}
		},
		facets: [
			{
				index: { byteStart: 32, byteEnd: 52 },
				features: [
					{
						$type: 'app.bsky.richtext.facet#mention',
						did: 'did:plc:ewvi7nxzyoun6zhxrhs64oiz'
					}
				]
			},
			{
				index: { byteStart: 0, byteEnd: 31 },
				features: [
					{
						$type: 'app.bsky.richtext.facet#link',
						uri: 'https://example.com/atproto-tooling'
					}
				]
			}
		],
		embed: {
			$type: 'app.bsky.embed.external',
			external: {
				uri: 'https://example.com/atproto-tooling',
				title: 'ATProto Tooling Project',
				description: 'A collection of tools for building on the atmosphere.'
			}
		}
	};

	let jsonText = $state('');
	let pathInput = $state('');
	let editing = $state(false);
	let uriInput = $state('');
	let loading = $state(false);
	let loadError = $state('');
	let jsonTextarea: HTMLTextAreaElement | undefined = $state();

	// -- Hash routing --
	// Hash is either an AT-URI ("at://...") or URL-encoded JSON ("%7B...").
	// Encoding is always applied; decoding distinguishes the two by first char.

	function pushHash(content: string) {
		const hash = content ? `#${encodeURIComponent(content)}` : '';
		if (decodeURIComponent(location.hash) !== decodeURIComponent(hash)) {
			history.pushState(null, '', hash || location.pathname + location.search);
		}
	}

	function readHash(): string {
		return decodeURIComponent(location.hash.slice(1));
	}

	// Load state from decoded hash content — no pushState (called on back/forward)
	function loadFromHash(content: string) {
		if (!content) {
			jsonText = '';
			pathInput = '';
			uriInput = '';
			editing = false;
			return;
		}
		if (content.startsWith('at://')) {
			loadUri(content, false);
		} else if (content.startsWith('{')) {
			try {
				const parsed = JSON.parse(content);
				if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
					jsonText = JSON.stringify(parsed, null, 2);
					editing = false;
					pathInput = '';
					uriInput = '';
				}
			} catch {
				// bad JSON in hash, ignore
			}
		}
	}

	// -- Paste handling --

	function tryLoadJson(text: string): boolean {
		try {
			const parsed = JSON.parse(text);
			if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
				jsonText = JSON.stringify(parsed, null, 2);
				editing = false;
				pathInput = '';
				uriInput = '';
				pushHash(jsonText);
				return true;
			}
		} catch {
			// not valid JSON
		}
		return false;
	}

	function handleGlobalPaste(e: ClipboardEvent) {
		const active = document.activeElement;
		if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) return;
		const text = e.clipboardData?.getData('text') ?? '';
		if (tryLoadJson(text)) {
			e.preventDefault();
		}
	}

	function handleTextareaPaste(e: ClipboardEvent) {
		const ta = e.target as HTMLTextAreaElement;
		const replacesAll = ta.selectionStart === 0 && ta.selectionEnd === ta.value.length;
		const isEmpty = ta.value.trim() === '';
		if (!replacesAll && !isEmpty) return;
		const text = e.clipboardData?.getData('text') ?? '';
		if (tryLoadJson(text)) {
			e.preventDefault();
		}
	}

	onMount(() => {
		const initial = readHash();
		if (initial) loadFromHash(initial);

		const onHashChange = () => loadFromHash(readHash());
		window.addEventListener('hashchange', onHashChange);
		document.addEventListener('paste', handleGlobalPaste);
		return () => {
			window.removeEventListener('hashchange', onHashChange);
			document.removeEventListener('paste', handleGlobalPaste);
		};
	});

	let parseResult = $derived.by(() => {
		const text = jsonText.trim();
		if (!text) return { record: null, error: '' };
		try {
			const parsed = JSON.parse(text);
			if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
				return { record: null, error: 'Expected a JSON object' };
			}
			return { record: parsed as Record<string, unknown>, error: '' };
		} catch (e) {
			return { record: null, error: (e as Error).message };
		}
	});

	let record = $derived(parseResult.record);
	let jsonError = $derived(parseResult.error);
	let paths: PathInfo[] = $derived(
		record ? Array.from(enumerate(record), ([p]) => p) : []
	);

	let pathResult = $derived.by(() => {
		const trimmed = pathInput.trim();
		if (!trimmed || !record) {
			return { matches: [] as unknown[], parseError: null as RecordPathParseError | null };
		}
		try {
			parse(trimmed);
			return { matches: match(record, trimmed), parseError: null };
		} catch (e) {
			if (e instanceof RecordPathParseError) {
				return { matches: [], parseError: e };
			}
			return { matches: [], parseError: null };
		}
	});

	let matches = $derived(pathResult.matches);
	let parseError = $derived(pathResult.parseError);
	let hasInput = $derived(pathInput.trim().length > 0);
	let noMatch = $derived(hasInput && record !== null && !parseError && matches.length === 0);

	function selectPath(path: string) {
		pathInput = path;
	}

	function loadSample() {
		jsonText = JSON.stringify(SAMPLE, null, 2);
		pathInput = '';
		editing = false;
		loadError = '';
		uriInput = '';
		pushHash(jsonText);
	}

	function formatJson() {
		if (!record) return;
		jsonText = JSON.stringify(record, null, 2);
	}

	async function loadUri(uri?: string, push = true) {
		const target = uri ?? uriInput.trim();
		if (!target) return;
		loading = true;
		loadError = '';
		try {
			const result = await fetchRecord(target);
			jsonText = JSON.stringify(result.value, null, 2);
			uriInput = target;
			pathInput = '';
			editing = false;
			if (push) pushHash(target);
		} catch (e) {
			loadError = (e as Error).message;
		} finally {
			loading = false;
		}
	}

	function formatValue(val: unknown): string {
		if (val === null) return 'null';
		if (val === undefined) return 'undefined';
		if (typeof val === 'string') return JSON.stringify(val);
		if (typeof val !== 'object') return String(val);
		const str = JSON.stringify(val, null, 2);
		if (str.length > 500) return str.slice(0, 500) + '\n…';
		return str;
	}
</script>

<div class="flex h-screen flex-col overflow-hidden bg-stone-50 text-stone-900">
	<!-- Header -->
	<header class="flex items-center gap-2 border-b border-stone-200 bg-white px-4 py-2">
		<h1 class="text-sm font-semibold">RecordPath Playground</h1>
		<a
			href="https://tangled.org/microcosm.blue/RecordPath/blob/main/spec.md"
			class="rounded-full bg-blue-50 px-2 py-0.5 text-[0.6875rem] font-medium text-blue-600 hover:bg-blue-100"
			>draft spec</a
		>
	</header>

	<!-- Main panels -->
	<main class="grid flex-1 grid-cols-2 overflow-hidden">
		<!-- Left: JSON editor / display -->
		<div class="flex flex-col flex-2 overflow-hidden border-r border-stone-200">
			{#if record || editing || jsonError}
				<!-- Panel header -->
				<div
					class="flex flex-wrap items-center gap-x-2 gap-y-1 border-b border-stone-200 bg-white px-3 py-1.5"
				>
					<span
						class="text-[0.6875rem] font-semibold tracking-wide text-stone-400 uppercase"
						>Record</span
					>
					<!-- AT-URI form -->
					<form
						class="flex items-center gap-1"
						onsubmit={(e) => {
							e.preventDefault();
							loadUri();
						}}
					>
						<input
							type="text"
							bind:value={uriInput}
							placeholder="at://..."
							spellcheck="false"
							class="w-52 rounded border border-stone-200 bg-white px-1.5 py-0.5 font-mono text-[0.625rem] outline-none
								focus:border-blue-400
								{loadError ? 'border-red-300' : ''}"
						/>
						<button
							type="submit"
							disabled={loading || !uriInput.trim()}
							class="cursor-pointer rounded border border-stone-200 px-1.5 py-0.5 text-[0.625rem] hover:bg-stone-50
								disabled:cursor-default disabled:opacity-40"
						>
							{loading ? '...' : 'Load'}
						</button>
					</form>
					{#if loadError}
						<span class="text-[0.625rem] text-red-500">{loadError}</span>
					{/if}
					<span class="flex-1"></span>
					<button
						onclick={loadSample}
						class="cursor-pointer rounded px-1.5 py-0.5 text-[0.625rem] text-stone-400 hover:text-stone-600"
						>Sample</button
					>
					<button
						onclick={formatJson}
						class="cursor-pointer rounded px-1.5 py-0.5 text-[0.625rem] text-stone-400 hover:text-stone-600"
						>Format</button
					>
					<span class="text-stone-200">|</span>
					<button
						class="cursor-pointer rounded px-1.5 py-0.5 text-[0.625rem]
							{editing
							? 'bg-sky-50 text-sky-600'
							: 'text-stone-400 hover:text-stone-600'}"
						onclick={() => (editing = true)}
					>
						Edit
					</button>
					<button
						class="cursor-pointer rounded px-1.5 py-0.5 text-[0.625rem]
							{!editing
							? 'bg-sky-50 text-sky-600'
							: 'text-stone-400 hover:text-stone-600'}"
						onclick={() => {
							editing = false;
							if (record && !uriInput) pushHash(jsonText);
						}}
					>
						Display
					</button>
				</div>
			{/if}

			{#if editing || jsonError}
				<textarea
					bind:this={jsonTextarea}
					bind:value={jsonText}
					spellcheck="false"
					placeholder="Paste a JSON record..."
					onpaste={handleTextareaPaste}
					class="flex-1 resize-none bg-white p-3 font-mono text-[0.8125rem] leading-relaxed outline-none"
					style="tab-size: 2"
				></textarea>
				{#if jsonError}
					<div class="border-t border-red-200 bg-red-50 px-3 py-1.5 text-xs text-red-600">
						{jsonError}
					</div>
				{/if}
			{:else if record}
				<JsonDisplay
					{record}
					activePath={pathInput}
					onSelectPath={(p) => (pathInput = p)}
					onLoadUri={(uri) => loadUri(uri)}
				/>
			{:else}
				<!-- Welcome state -->
				<div class="flex flex-1 flex-col items-center justify-center gap-4 bg-white px-8">
					<label class="text-sm text-stone-500">Paste an atproto record (JSON)</label>
					<textarea
						bind:value={jsonText}
						spellcheck="false"
						placeholder={'{"$type": "app.bsky.feed.post", ...}'}
						onpaste={handleTextareaPaste}
						onfocus={async () => {
							editing = true;
							await tick();
							jsonTextarea?.focus();
						}}
						class="h-24 w-full max-w-lg resize-none rounded border border-stone-200 p-3 font-mono text-xs leading-relaxed outline-none focus:border-blue-400"
						style="tab-size: 2"
					></textarea>
					<button
						onclick={loadSample}
						class="cursor-pointer rounded border border-stone-200 bg-white px-4 py-1.5 text-xs text-stone-600 hover:bg-stone-50"
					>
						Load sample record
					</button>
				</div>
			{/if}
		</div>

		<!-- Right: paths + matches -->
		<div class="flex flex-col flex-1 overflow-hidden">
			<!-- Path input -->
			<div class="border-b border-stone-200 bg-white p-2">
				<input
					type="text"
					bind:value={pathInput}
					placeholder="Type or click a RecordPath…"
					spellcheck="false"
					autocomplete="off"
					class="w-full rounded border-[1.5px] bg-white px-2.5 py-2 font-mono text-sm outline-none
						{parseError
						? 'border-red-400 shadow-[0_0_0_2px_theme(colors.red.100)]'
						: noMatch
							? 'border-amber-400 shadow-[0_0_0_2px_theme(colors.amber.100)]'
							: 'border-stone-200 focus:border-blue-500 focus:shadow-[0_0_0_2px_theme(colors.blue.100)]'}"
				/>
				{#if parseError}
					<pre
						class="mt-1.5 overflow-x-auto rounded bg-red-50 px-2.5 py-2 font-mono text-[0.8125rem]"
					><span class="text-red-300">{pathInput.trim()}</span>
<span class="text-red-500">{' '.repeat(parseError.position)}^ {parseError.message.split(' (position')[0]}</span>{#if parseError.suggestion}
<span class="text-emerald-500">hint: {parseError.suggestionHint}:</span> <button class="cursor-pointer text-emerald-600 underline decoration-emerald-300 hover:decoration-emerald-500" onclick={() => { pathInput = parseError.suggestion ?? ''; }}>{parseError.suggestion}</button>{/if}</pre>
				{/if}
			</div>

			<!-- Path list header -->
			<div
				class="flex items-center gap-2 border-b border-stone-200 bg-white px-3 py-1.5 text-[0.6875rem] font-semibold tracking-wide text-stone-400 uppercase"
			>
				Available paths
				{#if paths.length > 0}
					<span class="font-normal">({paths.length})</span>
				{/if}
			</div>

			<!-- Scrollable body -->
			<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
				<!-- Paths list -->
				<div class="flex-1 overflow-y-auto bg-white">
					{#each paths as { path, type }}
						<button
							onclick={() => selectPath(path)}
							class="flex w-full cursor-pointer items-center gap-2 border-l-[3px] px-3 py-[0.3125rem] text-left font-mono text-[0.8125rem]
								{pathInput === path
								? 'border-l-blue-500 bg-blue-50'
								: 'border-l-transparent hover:bg-stone-50'}"
						>
							<span class="min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
								>{path}</span
							>
							<span
								class="shrink-0 rounded px-1.5 py-px text-[0.625rem] font-medium uppercase tracking-wide
									{type === 'vector'
									? 'bg-emerald-50 text-emerald-700'
									: 'bg-sky-50 text-sky-700'}"
							>
								{type}
							</span>
						</button>
					{/each}
					{#if paths.length === 0 && !jsonError}
						<div class="px-3 py-6 text-center text-xs text-stone-400">
							Paste a JSON record to see available paths
						</div>
					{/if}
				</div>

				<!-- Matches -->
				{#if hasInput}
					<div class="flex max-h-[45%] flex-col overflow-hidden border-t border-stone-200">
						<div
							class="flex items-center gap-2 border-b border-stone-200 bg-white px-3 py-1.5 text-[0.6875rem] font-semibold tracking-wide text-stone-400 uppercase"
						>
							Matches
							<span class="font-normal">({matches.length})</span>
						</div>
						<div class="overflow-y-auto bg-white">
							{#if matches.length === 0}
								<div class="px-3 py-3 text-xs text-stone-400">No matches</div>
							{:else}
								{#each matches as val}
									<div
										class="whitespace-pre-wrap break-all border-b border-amber-100 bg-amber-50 px-3 py-1.5 font-mono text-[0.8125rem]"
									>
										{formatValue(val)}
									</div>
								{/each}
							{/if}
						</div>
					</div>
				{/if}
			</div>
		</div>
	</main>
</div>
