<script lang="ts">
	import { goto } from '$app/navigation';
	import { base } from '$app/paths';
	import { page } from '$app/state';
	import {
		api,
		ApiError,
		type AppDomain,
		type AppRole,
		type ExecutionLog,
		type Route,
		type RouteInput,
		type Script,
		type VersionInfo
	} from '$lib/api';
	import { currentUser } from '$lib/auth';
	import { canAdminApp, canWriteApp } from '$lib/capabilities';
	import { logLevelColor, statusColor } from '$lib/styles';
	import {
		checkHostAgainstClaims,
		guessPathKind,
		hostSuggestions,
		parseHostInput,
		pathKindMismatchWarning
	} from '$lib/route-utils';
	import CodeEditor from '$lib/CodeEditor.svelte';
	import ConfirmModal from '$lib/ConfirmModal.svelte';
	import { format as formatRhai } from '$lib/rhai';

	/// Pretty-print a JSON string in place, leaving it untouched if the
	/// input doesn't parse. The error state is shown next to the button
	/// so users see why it didn't reformat.
	function formatJson(s: string): { ok: true; text: string } | { ok: false; error: string } {
		try {
			return { ok: true, text: JSON.stringify(JSON.parse(s), null, 2) };
		} catch (e) {
			return { ok: false, error: e instanceof Error ? e.message : String(e) };
		}
	}

	// Route is `/scripts/[id]` so `page.params.id` is always present.
	let id = $derived(page.params.id ?? '');

	let tab = $state<'edit' | 'routing' | 'settings' | 'executions'>('edit');

	// ---------------- shared script state ----------------
	let script = $state<Script | null>(null);
	let scriptError = $state<string | null>(null);
	let scriptLoading = $state(true);
	let info = $state<VersionInfo | null>(null);

	let appSlug = $state<string | null>(null);
	let appDomains = $state<AppDomain[]>([]);
	let appMyRole = $state<AppRole | null>(null);

	const me = $derived($currentUser);
	const canWrite = $derived(canWriteApp(me, appMyRole));
	const canAdmin = $derived(canAdminApp(me, appMyRole));

	async function loadScript() {
		scriptLoading = true;
		scriptError = null;
		try {
			script = await api.scripts.get(id);
			editableSource = script.source;
			editableName = script.name;
			editableDescription = script.description ?? '';
			editableTimeout = script.timeout_seconds;
			editableSandbox = { ...(script.sandbox ?? {}) };
			// Resolve the owning app for the breadcrumb (slug),
			// route-form host suggestions (domain claims), and UI
			// shadowing (my_role on this app). All non-fatal — the
			// page renders without them, just with reduced fidelity.
			const appId = script.app_id;
			void api.apps
				.get(appId)
				.then((a) => {
					appSlug = a.slug;
					appMyRole = a.my_role ?? null;
				})
				.catch(() => {});
			void api.domains
				.listForApp(appId)
				.then((d) => {
					appDomains = d;
				})
				.catch(() => {});
		} catch (e) {
			scriptError = e instanceof Error ? e.message : String(e);
			script = null;
		} finally {
			scriptLoading = false;
		}
	}

	async function loadInfo() {
		try {
			info = await api.version();
		} catch {
			info = null;
		}
	}

	// ---------------- edit tab ----------------
	let editableSource = $state('');
	let savingSource = $state(false);
	let saveSourceError = $state<string | null>(null);
	let rhaiFormatError = $state<string | null>(null);

	function formatRhaiSource() {
		const r = formatRhai(editableSource);
		if (r.ok) {
			editableSource = r.text;
			rhaiFormatError = null;
		} else {
			rhaiFormatError = `Parse error: ${r.error.message} (line ${r.error.line}, position ${r.error.column})`;
		}
	}

	async function saveSource() {
		if (!script) return;
		savingSource = true;
		saveSourceError = null;
		try {
			script = await api.scripts.update(id, { source: editableSource });
			editableSource = script.source;
		} catch (e) {
			saveSourceError = e instanceof Error ? e.message : String(e);
		} finally {
			savingSource = false;
		}
	}

	let testBody = $state('{}');
	let testHeaders = $state('{}');
	let testBodyFormatError = $state<string | null>(null);
	let testHeadersFormatError = $state<string | null>(null);
	let testInProgress = $state(false);

	function formatTestBody() {
		const r = formatJson(testBody);
		if (r.ok) {
			testBody = r.text;
			testBodyFormatError = null;
		} else {
			testBodyFormatError = r.error;
		}
	}
	function formatTestHeaders() {
		const r = formatJson(testHeaders);
		if (r.ok) {
			testHeaders = r.text;
			testHeadersFormatError = null;
		} else {
			testHeadersFormatError = r.error;
		}
	}
	let testResult = $state<{
		status: number;
		headers: Record<string, string>;
		body: unknown;
	} | null>(null);
	let testError = $state<string | null>(null);

	async function invoke() {
		testInProgress = true;
		testError = null;
		testResult = null;
		try {
			let parsedBody: unknown;
			try {
				parsedBody = JSON.parse(testBody);
			} catch (e) {
				testError = `Body is not valid JSON: ${e instanceof Error ? e.message : String(e)}`;
				return;
			}
			let parsedHeaders: Record<string, string> = {};
			try {
				const obj: unknown = testHeaders.trim() ? JSON.parse(testHeaders) : {};
				if (obj && typeof obj === 'object') {
					for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
						parsedHeaders[k] = String(v);
					}
				}
			} catch (e) {
				testError = `Headers JSON is invalid: ${e instanceof Error ? e.message : String(e)}`;
				return;
			}
			testResult = await api.execute(id, parsedBody, parsedHeaders);
			await loadLogs();
		} catch (e) {
			testError =
				e instanceof ApiError ? `${e.status}: ${e.message}` : e instanceof Error ? e.message : String(e);
		} finally {
			testInProgress = false;
		}
	}

	// ---------------- routing tab ----------------
	let routes = $state<Route[]>([]);
	let routesError = $state<string | null>(null);
	let routesLoading = $state(true);

	let showAddRoute = $state(false);
	let newRoutePath = $state('/');
	let newRoutePathKind = $state<'exact' | 'prefix' | 'param'>('exact');
	// Host input is free-form; the kind is derived from what the user
	// typed (see `parsedHost` below). Default `*` = Any, matching the
	// canonical display form for an unrestricted host.
	let newRouteHost = $state('*');
	let newRouteMethod = $state('');
	let newRouteDispatchMode = $state<'sync' | 'async'>('sync');
	let pathKindAutoUpdate = $state(true);
	let creatingRoute = $state(false);
	let createRouteError = $state<string | null>(null);

	let previewUrl = $state('http://localhost:8000/');
	let previewMethod = $state('GET');
	let previewResult = $state<string | null>(null);

	// Auto-update the path-kind selector as the user types.
	$effect(() => {
		if (pathKindAutoUpdate) {
			newRoutePathKind = guessPathKind(newRoutePath);
		}
	});

	let parsedHost = $derived(parseHostInput(newRouteHost));
	let hostCheck = $derived(checkHostAgainstClaims(parsedHost, appDomains));
	let hostDatalistId = 'route-host-suggestions';
	let suggestions = $derived(hostSuggestions(appDomains));

	let pathKindWarning = $derived(
		newRoutePath.trim() ? pathKindMismatchWarning(newRoutePath, newRoutePathKind) : null
	);

	async function loadRoutes() {
		routesLoading = true;
		routesError = null;
		try {
			routes = await api.routes.listForScript(id);
		} catch (e) {
			routesError = e instanceof Error ? e.message : String(e);
		} finally {
			routesLoading = false;
		}
	}

	async function submitRoute(event: Event) {
		event.preventDefault();
		creatingRoute = true;
		createRouteError = null;
		try {
			const input: RouteInput = {
				host_kind: parsedHost.kind,
				host: parsedHost.host,
				path_kind: newRoutePathKind,
				path: newRoutePath.trim(),
				method: newRouteMethod.trim() || null,
				dispatch_mode: newRouteDispatchMode
			};
			await api.routes.create(id, input);
			showAddRoute = false;
			newRoutePath = '/';
			newRouteHost = '*';
			newRouteMethod = '';
			newRouteDispatchMode = 'sync';
			pathKindAutoUpdate = true;
			await loadRoutes();
		} catch (e) {
			if (e instanceof ApiError && e.status === 409) {
				const body = e.body as { conflicting_route?: Route; reason?: string } | null;
				createRouteError = `Conflict with existing route ${
					body?.conflicting_route ? formatRoute(body.conflicting_route) : '(unknown)'
				}${body?.reason ? ` — ${body.reason}` : ''}`;
			} else {
				createRouteError = e instanceof Error ? e.message : String(e);
			}
		} finally {
			creatingRoute = false;
		}
	}

	// F-U-007: ConfirmModal replaces the native confirm/alert flow so
	// the route detail is in the body and any error gets an inline
	// region instead of an unstyled browser modal.
	let routeToRemove = $state<Route | null>(null);
	let removingRoute = $state(false);
	let removeRouteError = $state<string | null>(null);

	function requestRemoveRoute(route: Route) {
		routeToRemove = route;
		removeRouteError = null;
	}

	async function confirmRemoveRoute() {
		if (!routeToRemove) return;
		removingRoute = true;
		removeRouteError = null;
		try {
			await api.routes.remove(routeToRemove.id);
			routeToRemove = null;
			await loadRoutes();
		} catch (e) {
			removeRouteError = e instanceof Error ? e.message : String(e);
		} finally {
			removingRoute = false;
		}
	}

	async function runPreview() {
		previewResult = null;
		if (!script) return;
		try {
			const r = await api.routes.match(script.app_id, previewUrl, previewMethod);
			if (r.matched) {
				const ours = r.matched.script_id === id;
				const tag = ours ? '✓ matches THIS script' : '⚠ matches a DIFFERENT script';
				previewResult = `${tag}\nroute_id: ${r.matched.route_id}\nscript_id: ${r.matched.script_id}\nparams: ${JSON.stringify(r.matched.params)}\nrest: ${JSON.stringify(r.matched.rest)}`;
			} else {
				previewResult = 'no route matches';
			}
		} catch (e) {
			previewResult = `error: ${e instanceof Error ? e.message : String(e)}`;
		}
	}

	function formatRoute(r: Route): string {
		const host =
			r.host_kind === 'any'
				? '*'
				: r.host_kind === 'wildcard'
					? `*.${r.host}`
					: r.host;
		return `[${r.method ?? 'ANY'} ${host} ${r.path}]`;
	}

	function fullUrlForRoute(r: Route): string {
		const baseUrl = info?.public_base_url ?? '';
		const host =
			r.host_kind === 'any'
				? baseUrl
				: r.host_kind === 'wildcard'
					? baseUrl.replace(/\/\/[^/]+/, `//example.${r.host}`)
					: baseUrl.replace(/\/\/[^/]+/, `//${r.host}`);
		return `${host}${r.path.replace(/\/\*$/, '/...')}`;
	}

	// ---------------- settings tab ----------------
	let editableName = $state('');
	let editableDescription = $state('');
	let editableTimeout = $state(30);
	let editableSandbox = $state<Record<string, number | undefined>>({});
	let savingSettings = $state(false);
	let saveSettingsError = $state<string | null>(null);

	async function saveSettings() {
		if (!script) return;
		savingSettings = true;
		saveSettingsError = null;
		try {
			const cleanedSandbox: Record<string, number> = {};
			for (const [k, v] of Object.entries(editableSandbox)) {
				if (typeof v === 'number' && v > 0) cleanedSandbox[k] = v;
			}
			script = await api.scripts.update(id, {
				name: editableName.trim(),
				description: editableDescription.trim() === '' ? null : editableDescription.trim(),
				timeout_seconds: editableTimeout,
				sandbox: cleanedSandbox
			});
			editableSandbox = { ...(script.sandbox ?? {}) };
		} catch (e) {
			saveSettingsError = e instanceof Error ? e.message : String(e);
		} finally {
			savingSettings = false;
		}
	}

	// ---------------- executions tab ----------------
	let logs = $state<ExecutionLog[] | null>(null);
	let logsError = $state<string | null>(null);
	let logsLoading = $state(true);

	async function loadLogs() {
		logsLoading = true;
		logsError = null;
		try {
			logs = await api.scripts.logs(id, { limit: 50 });
		} catch (e) {
			logsError = e instanceof Error ? e.message : String(e);
			logs = null;
		} finally {
			logsLoading = false;
		}
	}

	// ---------------- deletion ----------------
	let confirmingDelete = $state(false);
	let deleting = $state(false);
	let deleteError = $state<string | null>(null);

	function askDelete() {
		deleteError = null;
		confirmingDelete = true;
	}

	async function confirmDelete() {
		if (!script) return;
		deleting = true;
		deleteError = null;
		try {
			await api.scripts.remove(id);
			await goto(base + '/');
		} catch (e) {
			deleteError = e instanceof Error ? e.message : String(e);
		} finally {
			deleting = false;
		}
	}

	$effect(() => {
		void loadScript();
		void loadInfo();
		void loadRoutes();
		void loadLogs();
	});

	// Defense-in-depth: anyone non-admin who lands on the Settings
	// tab via a stale link gets bounced back to Edit. The tab button
	// itself is also hidden.
	$effect(() => {
		if (!canAdmin && tab === 'settings') {
			tab = 'edit';
		}
	});
</script>

<section>
	<a class="back" href={appSlug ? `${base}/apps/${appSlug}` : `${base}/apps`}>
		← {appSlug ?? 'Apps'}
	</a>

	{#if scriptLoading}
		<p class="muted">Loading…</p>
	{:else if scriptError}
		<div class="error">{scriptError}</div>
	{:else if script}
		<header class="page-header">
			<div>
				{#if appSlug}
					<div class="breadcrumb">
						<a href="{base}/apps">Apps</a> /
						<a href="{base}/apps/{appSlug}">{appSlug}</a> / Scripts /
						<code>{script.name}</code>
					</div>
				{/if}
				<h1>
					{script.name}
					{#if script.kind === 'module'}
						<span class="kind-badge kind-module" title="Library imported by other scripts">module</span>
					{:else}
						<span class="kind-badge kind-endpoint" title="Handles HTTP routes and trigger events">endpoint</span>
					{/if}
				</h1>
				<p class="muted">
					v{script.version} · timeout {script.timeout_seconds}s · {script.description ?? 'no description'}
				</p>
			</div>
			{#if canAdmin}
				<button type="button" class="danger" onclick={askDelete} disabled={deleting}>
					{deleting ? 'Deleting…' : 'Delete'}
				</button>
			{/if}
		</header>

		<nav class="tabs">
			<button class:active={tab === 'edit'} onclick={() => (tab = 'edit')}>Edit</button>
			<button class:active={tab === 'routing'} onclick={() => (tab = 'routing')}>
				Routing
				{#if routes.length > 0}
					<span class="badge-count">{routes.length}</span>
				{/if}
			</button>
			{#if canAdmin}
				<button class:active={tab === 'settings'} onclick={() => (tab = 'settings')}>Settings</button>
			{/if}
			<button class:active={tab === 'executions'} onclick={() => (tab = 'executions')}>
				Executions
			</button>
		</nav>

		<!-- ============================================================ EDIT ===== -->
		{#if tab === 'edit'}
			<div class="grid">
				<section class="card">
					<header class="editor-header">
						<h2>Source</h2>
						{#if canWrite}
							<button type="button" class="ghost small" onclick={formatRhaiSource}>
								Format
							</button>
						{/if}
					</header>
					<CodeEditor
						bind:value={editableSource}
						language="rhai"
						minHeight="22rem"
						readOnly={!canWrite}
					/>
					{#if rhaiFormatError}
						<div class="error inline">{rhaiFormatError}</div>
					{/if}
					{#if saveSourceError}
						<div class="error inline">{saveSourceError}</div>
					{/if}
					{#if canWrite}
						<div class="actions">
							<button
								type="button"
								onclick={saveSource}
								disabled={savingSource || editableSource === script.source}
							>
								{savingSource ? 'Saving…' : 'Save'}
							</button>
						</div>
					{/if}
				</section>

				<section class="card">
					<h2>Test invoke</h2>
					<div class="json-block">
						<header class="json-header">
							<span>Request body (JSON)</span>
							<button type="button" class="ghost small" onclick={formatTestBody}>
								Format
							</button>
						</header>
						<CodeEditor bind:value={testBody} language="json" minHeight="9rem" />
						{#if testBodyFormatError}
							<div class="error inline">{testBodyFormatError}</div>
						{/if}
					</div>
					<div class="json-block">
						<header class="json-header">
							<span>Headers (JSON object)</span>
							<button type="button" class="ghost small" onclick={formatTestHeaders}>
								Format
							</button>
						</header>
						<CodeEditor bind:value={testHeaders} language="json" minHeight="6rem" />
						{#if testHeadersFormatError}
							<div class="error inline">{testHeadersFormatError}</div>
						{/if}
					</div>
					<div class="actions">
						<button type="button" onclick={invoke} disabled={testInProgress}>
							{testInProgress ? 'Running…' : 'Send'}
						</button>
					</div>
					{#if testError}
						<div class="error inline">{testError}</div>
					{/if}
					{#if testResult}
						<div class="result">
							<div class="status">HTTP {testResult.status}</div>
							<pre>{JSON.stringify(testResult.body, null, 2)}</pre>
						</div>
					{/if}
					<p class="muted hint">
						Test invoke uses the internal /api/v1/execute/{`{id}`} bypass — your custom routes
						aren't checked. To test routes, use the Match preview on the Routing tab or curl the
						URL directly.
					</p>
				</section>
			</div>

			<!-- ====================================================== ROUTING ===== -->
		{:else if tab === 'routing'}
			<section class="card wide">
				<header class="card-header">
					<h2>Routes</h2>
					{#if canWrite}
						<button type="button" onclick={() => (showAddRoute = !showAddRoute)}>
							{showAddRoute ? 'Cancel' : '+ Add route'}
						</button>
					{/if}
				</header>

				{#if showAddRoute && canWrite}
					<form class="route-form" onsubmit={submitRoute}>
						<label class="full">
							<span>Path</span>
							<input
								bind:value={newRoutePath}
								oninput={() => (pathKindAutoUpdate = true)}
								placeholder="/greet, /greet/:name, /webhooks/*"
								required
								autocomplete="off"
							/>
						</label>
						<div class="row">
							<label>
								<span>Path kind</span>
								<select
									bind:value={newRoutePathKind}
									onchange={() => (pathKindAutoUpdate = false)}
								>
									<option value="exact">exact</option>
									<option value="param">param</option>
									<option value="prefix">prefix</option>
								</select>
							</label>
							<label>
								<span>Method</span>
								<select bind:value={newRouteMethod}>
									<option value="">ANY</option>
									<option value="GET">GET</option>
									<option value="POST">POST</option>
									<option value="PUT">PUT</option>
									<option value="DELETE">DELETE</option>
									<option value="PATCH">PATCH</option>
								</select>
							</label>
							<label>
								<span>Dispatch</span>
								<select bind:value={newRouteDispatchMode}>
									<option value="sync">sync (wait for response)</option>
									<option value="async">async (202 + run later)</option>
								</select>
							</label>
						</div>
						<label class="full">
							<span class="host-label">
								Host
								<span class="kind-chip kind-{parsedHost.kind}">
									{parsedHost.kind}
								</span>
							</span>
							<input
								bind:value={newRouteHost}
								placeholder="*  ·  app.example.com  ·  *.example.com"
								list={hostDatalistId}
								autocomplete="off"
								autocapitalize="off"
								autocorrect="off"
								spellcheck="false"
							/>
							<datalist id={hostDatalistId}>
								{#each suggestions as s (s)}
									<option value={s}></option>
								{/each}
							</datalist>
							<small class="muted">
								<code>*</code> = any host claimed by this app ·
								<code>*.foo.com</code> = wildcard · <code>foo.com</code> =
								strict
							</small>
						</label>
						{#if !hostCheck.ok}
							<div class="warning inline">
								{hostCheck.reason}.
								{#if appDomains.length > 0}
									Claims:
									{#each appDomains as d, i (d.id)}<code>{d.pattern}</code>{#if i < appDomains.length - 1},
										{/if}{/each}
								{/if}
							</div>
						{/if}
						{#if pathKindWarning}
							<div class="warning inline">{pathKindWarning}</div>
						{/if}
						{#if createRouteError}
							<div class="error inline">{createRouteError}</div>
						{/if}
						<div class="actions">
							<button type="submit" disabled={creatingRoute}>
								{creatingRoute ? 'Creating…' : 'Create route'}
							</button>
						</div>
					</form>
				{/if}

				{#if routesLoading}
					<p class="muted">Loading routes…</p>
				{:else if routesError}
					<div class="error">{routesError}</div>
				{:else if routes.length === 0}
					<p class="muted">
						No routes yet. Scripts are still callable via
						<code>POST /api/v1/execute/{`{id}`}</code> while you set things up.
					</p>
				{:else}
					<ul class="route-list">
						{#each routes as r (r.id)}
							<li>
								<div class="route-row">
									<span class="kind kind-{r.path_kind}">{r.path_kind}</span>
									<span class="method">{r.method ?? 'ANY'}</span>
									<span class="host">
										{r.host_kind === 'any'
											? '*'
											: r.host_kind === 'wildcard'
												? `*.${r.host}`
												: r.host}
									</span>
									<span class="path">{r.path}</span>
									{#if r.dispatch_mode === 'async'}
										<span class="dispatch dispatch-async" title="Dispatch mode">async</span>
									{/if}
									{#if canWrite}
										<button type="button" class="link danger" onclick={() => requestRemoveRoute(r)}>
											remove
										</button>
									{/if}
								</div>
								{#if info}
									<div class="route-url muted">→ {fullUrlForRoute(r)}</div>
								{/if}
							</li>
						{/each}
					</ul>
				{/if}
			</section>

			<section class="card wide">
				<h2>Match preview</h2>
				<p class="muted hint">
					Synthetic check: does this URL+method match a route, and which? Useful for
					verifying your routing before exposing scripts publicly.
				</p>
				<div class="row">
					<label class="grow">
						<span>URL</span>
						<input bind:value={previewUrl} autocomplete="off" />
					</label>
					<label>
						<span>Method</span>
						<select bind:value={previewMethod}>
							<option>GET</option>
							<option>POST</option>
							<option>PUT</option>
							<option>DELETE</option>
							<option>PATCH</option>
						</select>
					</label>
				</div>
				<div class="actions">
					<button type="button" onclick={runPreview}>Match</button>
				</div>
				{#if previewResult}
					<pre class="preview">{previewResult}</pre>
				{/if}
			</section>

			<!-- ===================================================== SETTINGS ===== -->
		{:else if tab === 'settings' && canAdmin}
			<section class="card wide">
				<h2>General</h2>
				<label>
					<span>Name</span>
					<input bind:value={editableName} required />
				</label>
				<label>
					<span>Description</span>
					<input bind:value={editableDescription} placeholder="optional" />
				</label>
				<label>
					<span>Timeout (seconds)</span>
					<input type="number" bind:value={editableTimeout} min="1" max="300" />
				</label>

				<details class="advanced chevron">
					<summary>Advanced sandbox</summary>
					<p class="muted hint">
						Each value is admin-ceiling-capped at write time. Leave empty to use the
						platform default for that knob.
					</p>
					<div class="sandbox-grid">
						{#each ['max_operations', 'max_string_size', 'max_array_size', 'max_map_size', 'max_call_levels', 'max_expr_depth'] as key (key)}
							<label>
								<span>{key}</span>
								<input
									type="number"
									min="1"
									bind:value={editableSandbox[key]}
									placeholder="(default)"
								/>
							</label>
						{/each}
					</div>
				</details>

				{#if saveSettingsError}
					<div class="error inline">{saveSettingsError}</div>
				{/if}
				<div class="actions">
					<button type="button" onclick={saveSettings} disabled={savingSettings}>
						{savingSettings ? 'Saving…' : 'Save settings'}
					</button>
				</div>
			</section>

			<!-- =================================================== EXECUTIONS ===== -->
		{:else if tab === 'executions'}
			<section class="logs">
				<header class="logs-header">
					<h2>Recent executions</h2>
					<button type="button" class="ghost" onclick={loadLogs} disabled={logsLoading}>
						{logsLoading ? 'Refreshing…' : 'Refresh'}
					</button>
				</header>
				{#if logsError}
					<div class="error">{logsError}</div>
				{:else if logs && logs.length === 0}
					<p class="muted">
						No executions yet — try the Test invoke panel on the Edit tab, or curl
						one of your routes.
					</p>
				{:else if logs}
					<ul class="exec-list">
						{#each logs as log (log.id)}
							<li>
								<details>
									<summary>
										<span class="badge" style:background={statusColor[log.status]}>
											{log.status}
										</span>
										<span class="time">{new Date(log.created_at).toLocaleString()}</span>
										<span class="muted">
											{log.response_code ?? '—'} · {log.duration_ms}ms
										</span>
										<span class="muted path-snippet">{log.request_path}</span>
									</summary>
									<div class="exec-body">
										<div>
											<strong>Request body</strong>
											<pre>{JSON.stringify(log.request_body, null, 2)}</pre>
										</div>
										<div>
											<strong>Response body</strong>
											<pre>{JSON.stringify(log.response_body, null, 2)}</pre>
										</div>
										{#if log.script_logs && log.script_logs.length > 0}
											<div>
												<strong>Script logs</strong>
												<ul class="log-entries">
													{#each log.script_logs as entry}
														<li>
															<span
																class="level"
																style:color={logLevelColor[entry.level] ?? '#cbd5e1'}
															>
																{entry.level}
															</span>
															<span class="msg">{entry.message}</span>
															{#if entry.data !== null && entry.data !== undefined}
																<pre class="data">{JSON.stringify(entry.data)}</pre>
															{/if}
														</li>
													{/each}
												</ul>
											</div>
										{/if}
									</div>
								</details>
							</li>
						{/each}
					</ul>
				{/if}
			</section>
		{/if}

		{#if routeToRemove}
			<!-- F-U-007: route delete confirm modal, replacing native confirm/alert. -->
			<ConfirmModal
				title="Delete route"
				variant="danger"
				confirmLabel="Delete route"
				busyLabel="Deleting…"
				busy={removingRoute}
				onConfirm={confirmRemoveRoute}
				onCancel={() => (routeToRemove = null)}
			>
				<p>
					Delete route
					<code>{routeToRemove.method ?? 'ANY'} {routeToRemove.path}</code>
					{#if routeToRemove.host}on <code>{routeToRemove.host}</code>{/if}?
				</p>
				<p class="muted">Existing inbound requests will start 404'ing on this path immediately.</p>
				{#if removeRouteError}
					<p class="modal-error">{removeRouteError}</p>
				{/if}
			</ConfirmModal>
		{/if}

		{#if confirmingDelete && script}
			<ConfirmModal
				title="Delete script “{script.name}”"
				variant="danger"
				confirmLabel="Delete script"
				busyLabel="Deleting…"
				confirmPhrase={script.name}
				confirmPhrasePrompt="Type the script name to confirm:"
				busy={deleting}
				onConfirm={confirmDelete}
				onCancel={() => (confirmingDelete = false)}
			>
				<p>
					This will <strong>permanently delete</strong>
					<strong>{script.name}</strong>, all its routes, and all its
					execution logs. There is no undo.
				</p>
				{#if routes.length > 0}
					<p class="muted">
						{routes.length} route{routes.length === 1 ? '' : 's'} bound to
						this script will be removed.
					</p>
				{/if}
				{#if deleteError}
					<p class="modal-error">{deleteError}</p>
				{/if}
			</ConfirmModal>
		{/if}
	{/if}
</section>

<style>
	.back {
		color: #94a3b8;
		text-decoration: none;
		font-size: 0.875rem;
	}
	.back:hover {
		color: #e2e8f0;
	}

	.page-header {
		display: flex;
		justify-content: space-between;
		align-items: flex-start;
		margin: 1rem 0 1.5rem;
	}
	.breadcrumb {
		font-size: 0.875rem;
		color: #64748b;
		margin-bottom: 0.25rem;
	}
	.breadcrumb a {
		color: #94a3b8;
		text-decoration: none;
	}
	.breadcrumb a:hover {
		color: #e2e8f0;
	}
	.breadcrumb code {
		background: #1e293b;
		padding: 0.1rem 0.3rem;
		border-radius: 0.25rem;
	}
	h1 {
		margin: 0;
		font-size: 1.5rem;
	}
	h2 {
		margin: 0 0 0.75rem;
		font-size: 1rem;
		color: #cbd5e1;
		text-transform: uppercase;
		letter-spacing: 0.05em;
	}
	p.muted {
		margin: 0.25rem 0 0;
	}
	.muted {
		color: #64748b;
	}

	button {
		background: #38bdf8;
		color: #0b1220;
		border: none;
		padding: 0.5rem 1rem;
		border-radius: 0.375rem;
		font-weight: 600;
		cursor: pointer;
	}
	button:disabled {
		opacity: 0.5;
		cursor: not-allowed;
	}
	button.danger {
		background: #ef4444;
		color: #fff;
	}
	button.ghost {
		background: transparent;
		color: #94a3b8;
		border: 1px solid #334155;
	}
	button.small {
		padding: 0.2rem 0.6rem;
		font-size: 0.75rem;
		font-weight: 500;
	}

	.json-block {
		display: flex;
		flex-direction: column;
		gap: 0.4rem;
	}
	.json-header {
		display: flex;
		justify-content: space-between;
		align-items: center;
		font-size: 0.85rem;
		color: #cbd5e1;
	}
	.editor-header {
		display: flex;
		justify-content: space-between;
		align-items: center;
	}
	.editor-header h2 {
		margin: 0;
	}
	button.link {
		background: transparent;
		color: #94a3b8;
		padding: 0.25rem 0.5rem;
		font-weight: 500;
		text-decoration: underline;
	}
	button.link.danger {
		color: #fca5a5;
	}

	.tabs {
		display: flex;
		gap: 0.5rem;
		border-bottom: 1px solid #1e293b;
		margin-bottom: 1rem;
	}
	.tabs button {
		background: transparent;
		color: #94a3b8;
		padding: 0.75rem 1rem;
		border-radius: 0;
		font-weight: 500;
		border-bottom: 2px solid transparent;
		margin-bottom: -1px;
	}
	.tabs button:hover {
		color: #e2e8f0;
	}
	.tabs button.active {
		color: #38bdf8;
		border-bottom-color: #38bdf8;
	}
	.badge-count {
		background: #334155;
		color: #cbd5e1;
		font-size: 0.7rem;
		padding: 0.05rem 0.4rem;
		border-radius: 999px;
		margin-left: 0.4rem;
	}

	.grid {
		display: grid;
		grid-template-columns: 1fr 1fr;
		gap: 1rem;
	}
	@media (max-width: 720px) {
		.grid {
			grid-template-columns: 1fr;
		}
	}

	.card {
		background: #1e293b;
		border-radius: 0.5rem;
		padding: 1.25rem;
		display: flex;
		flex-direction: column;
		gap: 0.75rem;
		margin-bottom: 1rem;
	}
	.card.wide {
		grid-column: 1 / -1;
	}
	.card-header {
		display: flex;
		justify-content: space-between;
		align-items: center;
	}

	input,
	select {
		background: #0b1220;
		color: #e2e8f0;
		border: 1px solid #334155;
		border-radius: 0.375rem;
		padding: 0.5rem 0.75rem;
		font-family: ui-sans-serif, system-ui, sans-serif;
		font-size: 0.9rem;
		width: 100%;
		box-sizing: border-box;
	}
	input:disabled,
	select:disabled {
		opacity: 0.5;
	}
	label {
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
		font-size: 0.85rem;
		color: #cbd5e1;
	}
	label.full {
		width: 100%;
	}
	label.grow {
		flex: 1;
	}

	.row {
		display: grid;
		grid-template-columns: 1fr 2fr;
		gap: 0.75rem;
	}

	.actions {
		display: flex;
		justify-content: flex-end;
	}

	.error,
	.warning {
		border-radius: 0.375rem;
		padding: 0.75rem;
		font-size: 0.875rem;
	}
	.error {
		border: 1px solid #b91c1c;
		background: #450a0a;
		color: #fecaca;
	}
	.warning {
		border: 1px solid #ca8a04;
		background: #422006;
		color: #fde68a;
	}
	.inline {
		font-size: 0.875rem;
	}

	.result {
		background: #0b1220;
		border: 1px solid #334155;
		border-radius: 0.375rem;
		padding: 0.75rem;
	}
	.result .status {
		font-weight: 600;
		margin-bottom: 0.5rem;
	}
	.result pre,
	.preview {
		margin: 0;
		font-size: 0.85rem;
		white-space: pre-wrap;
		word-break: break-word;
	}
	.preview {
		background: #0b1220;
		border: 1px solid #334155;
		border-radius: 0.375rem;
		padding: 0.75rem;
		font-family:
			ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
	}
	.hint {
		font-size: 0.8rem;
	}

	.route-form {
		background: #0f172a;
		border: 1px solid #334155;
		border-radius: 0.5rem;
		padding: 1rem;
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
	}

	.route-list {
		list-style: none;
		padding: 0;
		margin: 0;
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
	}
	.route-row {
		display: flex;
		align-items: center;
		gap: 0.5rem;
		padding: 0.6rem 0.75rem;
		background: #0f172a;
		border: 1px solid #334155;
		border-radius: 0.375rem;
		font-family:
			ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
		font-size: 0.85rem;
	}
	.route-row .kind {
		font-size: 0.7rem;
		font-weight: 700;
		text-transform: uppercase;
		padding: 0.15rem 0.45rem;
		border-radius: 0.25rem;
	}
	.kind-exact {
		background: #14532d;
		color: #bbf7d0;
	}
	.kind-prefix {
		background: #1e3a8a;
		color: #bfdbfe;
	}
	.kind-param {
		background: #581c87;
		color: #e9d5ff;
	}
	.host-label {
		display: flex;
		align-items: center;
		gap: 0.5rem;
	}
	.kind-chip {
		font-size: 0.65rem;
		font-weight: 700;
		text-transform: uppercase;
		padding: 0.1rem 0.4rem;
		border-radius: 0.25rem;
		letter-spacing: 0.03em;
	}
	.kind-chip.kind-any {
		background: #1e293b;
		color: #cbd5e1;
	}
	.kind-chip.kind-strict {
		background: #14532d;
		color: #bbf7d0;
	}
	.kind-chip.kind-wildcard {
		background: #1e3a8a;
		color: #bfdbfe;
	}
	.route-row .method {
		color: #fbbf24;
		font-weight: 700;
		min-width: 3rem;
	}
	.route-row .host {
		color: #94a3b8;
	}
	.route-row .path {
		color: #e2e8f0;
		flex: 1;
	}
	.route-row .dispatch {
		font-size: 0.6rem;
		font-weight: 700;
		text-transform: uppercase;
		padding: 0.1rem 0.4rem;
		border-radius: 0.25rem;
		letter-spacing: 0.03em;
	}
	.dispatch-async {
		background: #422006;
		color: #fcd34d;
	}
	.route-url {
		font-family:
			ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
		font-size: 0.75rem;
		padding: 0 0.75rem 0.4rem;
	}

	.advanced {
		background: #0f172a;
		border: 1px solid #334155;
		border-radius: 0.5rem;
		padding: 0.75rem 1rem;
	}
	.advanced summary {
		cursor: pointer;
		font-weight: 600;
		color: #cbd5e1;
	}
	.sandbox-grid {
		display: grid;
		grid-template-columns: repeat(2, 1fr);
		gap: 0.75rem;
		margin-top: 0.75rem;
	}
	@media (max-width: 720px) {
		.sandbox-grid {
			grid-template-columns: 1fr;
		}
	}

	.logs-header {
		display: flex;
		justify-content: space-between;
		align-items: center;
		margin-bottom: 0.75rem;
	}
	.exec-list {
		list-style: none;
		padding: 0;
		margin: 0;
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
	}
	.exec-list summary {
		display: flex;
		gap: 0.75rem;
		align-items: center;
		padding: 0.75rem 1rem;
		background: #1e293b;
		border-radius: 0.375rem;
		cursor: pointer;
		list-style: none;
		font-size: 0.875rem;
	}
	.exec-list summary::-webkit-details-marker {
		display: none;
	}
	.path-snippet {
		font-family:
			ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
		font-size: 0.8rem;
	}
	.badge {
		font-size: 0.7rem;
		padding: 0.125rem 0.5rem;
		border-radius: 999px;
		color: #0b1220;
		font-weight: 700;
		text-transform: uppercase;
		letter-spacing: 0.05em;
	}
	.time {
		font-size: 0.875rem;
		color: #cbd5e1;
	}
	.exec-body {
		padding: 0.75rem 1rem;
		background: #0b1220;
		border-radius: 0 0 0.375rem 0.375rem;
		display: flex;
		flex-direction: column;
		gap: 0.75rem;
	}
	.exec-body pre {
		background: #02101f;
		border: 1px solid #1e293b;
		border-radius: 0.25rem;
		padding: 0.5rem;
		margin: 0.25rem 0 0;
		font-size: 0.8rem;
		white-space: pre-wrap;
		word-break: break-word;
	}
	.log-entries {
		list-style: none;
		padding: 0;
		margin: 0.25rem 0 0;
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
		font-size: 0.85rem;
	}
	.log-entries .level {
		display: inline-block;
		width: 3.5rem;
		text-transform: uppercase;
		font-weight: 700;
		font-size: 0.7rem;
	}
	.log-entries .data {
		margin: 0.25rem 0 0 4rem;
		font-size: 0.75rem;
	}

	.kind-badge {
		display: inline-block;
		padding: 0 0.45rem;
		margin-left: 0.5rem;
		border-radius: 0.25rem;
		font-size: 0.65rem;
		font-weight: 600;
		text-transform: uppercase;
		letter-spacing: 0.05em;
		line-height: 1.5;
		vertical-align: middle;
	}

	.kind-endpoint {
		background: #1e3a5f;
		color: #93c5fd;
	}

	.kind-module {
		background: #3f2e7d;
		color: #c4b5fd;
	}
</style>
