<script lang="ts">
	import { goto } from '$app/navigation';
	import { page } from '$app/stores';
	import * as api from '$lib/api';
	import type { LogScanRule, LogScanRuleInput, LogScanTestResult } from '$lib/api';
	import { logScanRuleDetailCache } from '$lib/stores/caches';
	import ForgeHero from '$lib/components/ForgeHero.svelte';
	import ToggleSwitch from '$lib/components/ToggleSwitch.svelte';
	import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
	import RegexTestBox from '$lib/components/RegexTestBox.svelte';
	import { t } from '$lib/i18n';

	// SvelteKit gives a string id; a non-numeric path silently maps
	// to NaN. Guard explicitly so the rest of the page doesn't make
	// bogus API calls.
	const id = $derived.by(() => {
		const n = Number($page.params.id);
		return Number.isFinite(n) && n > 0 ? n : null;
	});

	let rule = $state<LogScanRule | null>(null);
	let loading = $state(true);
	let saving = $state(false);
	let testing = $state(false);
	let deleting = $state(false);
	let confirmDelete = $state(false);
	let error = $state('');
	let serverTestResult = $state<LogScanTestResult | null>(null);
	// Cached workload name for the scope label. We fetch by id
	// rather than listing every workload because the rule already
	// tells us exactly which one to look up. A failed lookup falls
	// back to the truncated id so the page still renders.
	let scopedWorkloadName = $state('');

	let name = $state('');
	let pattern = $state('');
	let severity = $state<'info' | 'warn' | 'error'>('warn');
	let streams = $state<'all' | 'stdout' | 'stderr'>('all');
	let cooldownSeconds = $state(60);
	let enabled = $state(true);
	let sampleLine = $state('');

	const regexValid = $derived.by(() => {
		if (!pattern) return true;
		try {
			new RegExp(pattern);
			return true;
		} catch {
			return false;
		}
	});

	function seedForm(r: LogScanRule): void {
		name = r.name;
		pattern = r.pattern;
		severity = r.severity;
		streams = r.streams;
		cooldownSeconds = r.cooldown_seconds;
		enabled = r.enabled;
	}

	// Best-effort scope-label resolution (fire-and-forget); failure falls back
	// to the truncated id. Runs once per id alongside the form seed.
	async function resolveScopeName(r: LogScanRule, k: string): Promise<void> {
		if (!r.workload_id) {
			scopedWorkloadName = '';
			return;
		}
		try {
			const w = await api.getWorkload(r.workload_id);
			// Ignore a late response if we've since navigated to another id.
			if (String(id) === k) scopedWorkloadName = w.name;
		} catch {
			if (String(id) === k) scopedWorkloadName = '';
		}
	}

	// Keyed warm-seed: render the cached rule instantly on revisit and seed the
	// editable form ONCE per id (a background revalidation never re-seeds, so
	// unsaved edits survive). Plain (non-reactive) so the $effect tracks only id.
	let seededKey: string | null = null;

	async function load(): Promise<void> {
		if (id === null) {
			error = 'Invalid rule id';
			loading = false;
			return;
		}
		const k = String(id);
		const cached = logScanRuleDetailCache.peek(k);
		if (cached.value) {
			rule = cached.value;
			if (seededKey !== k) { seedForm(cached.value); resolveScopeName(cached.value, k); seededKey = k; }
			loading = false;
		} else {
			loading = true;
		}
		error = '';
		await logScanRuleDetailCache.refresh(k);
		// Bail if the route id changed while we were fetching — a newer load()
		// now owns the page state; applying this stale result would clobber it.
		if (id === null || String(id) !== k) return;
		const entry = logScanRuleDetailCache.peek(k);
		if (entry.error && !cached.value) {
			error = entry.error || 'Failed to load rule';
			loading = false;
			return;
		}
		if (entry.value) {
			rule = entry.value;
			if (seededKey !== k) { seedForm(entry.value); resolveScopeName(entry.value, k); seededKey = k; }
		}
		loading = false;
	}

	async function save(e?: Event): Promise<void> {
		e?.preventDefault();
		if (!rule || id === null || saving) return;
		saving = true;
		error = '';
		try {
			const body: LogScanRuleInput = {
				name: name.trim(),
				pattern,
				severity,
				streams,
				cooldown_seconds: cooldownSeconds,
				enabled
			};
			rule = await api.updateLogScanRule(id, body);
			logScanRuleDetailCache.set(String(id), rule);
		} catch (e) {
			error = e instanceof Error ? e.message : 'Save failed';
		} finally {
			saving = false;
		}
	}

	async function runServerTest(): Promise<void> {
		if (id === null) return;
		testing = true;
		serverTestResult = null;
		error = '';
		try {
			serverTestResult = await api.testLogScanRule(id, sampleLine);
		} catch (e) {
			error = e instanceof Error ? e.message : 'Test failed';
		} finally {
			testing = false;
		}
	}

	async function doDelete(): Promise<void> {
		if (id === null) return;
		deleting = true;
		error = '';
		try {
			await api.deleteLogScanRule(id);
			goto('/log-scan-rules');
		} catch (e) {
			error = e instanceof Error ? e.message : 'Delete failed';
			deleting = false;
			confirmDelete = false;
		}
	}

	function scopeLabel(r: LogScanRule | null): string {
		if (!r) return '';
		if (r.overrides_id !== 0) {
			return $t('logscan.scope.override', { id: String(r.overrides_id) });
		}
		if (r.workload_id !== '') {
			// Prefer the human-readable name when the workload load
			// succeeded; fall back to the truncated id so the label
			// still resolves on missing/deleted workloads.
			const label = scopedWorkloadName || r.workload_id.slice(0, 8);
			return $t('logscan.scope.workload', { id: label });
		}
		return $t('logscan.scope.global');
	}

	// Reload when route id changes — SvelteKit reuses the component
	// instance across [id] transitions, so onMount alone would leave
	// stale data when navigating between sibling pages.
	$effect(() => {
		const _ = id;
		load();
	});
</script>

<svelte:head>
	<title>{rule?.name ?? $t('logscan.titleSingular')} · Tinyforge</title>
</svelte:head>

<div class="forge" aria-busy={loading}>
	{#snippet detailLede()}
		{#if rule}
			<span class="lede-meta">
				{$t('logscan.list.scope')} <code>{scopeLabel(rule)}</code> ·
				{$t('logscan.list.severity')} <code>{rule.severity}</code> ·
				{$t('logscan.list.streams')} <code>{rule.streams}</code>
			</span>
		{/if}
	{/snippet}

	<ForgeHero
		backHref="/log-scan-rules"
		backLabel={$t('logscan.toolbar.backToList')}
		eyebrowSuffix={$t('logscan.titleSingular').toUpperCase()}
		title={rule?.name ?? $t('observability.loading')}
		size="lg"
		lede_html={rule ? detailLede : undefined}
	/>

	{#if error}
		<div class="alert" role="alert">
			<span class="alert-tag">ERR</span><span>{error}</span>
		</div>
	{/if}

	{#if loading || !rule}
		<div class="skeleton-rows" aria-busy="true" aria-live="polite" aria-label={$t('observability.loading')}>
			{#each Array(3) as _, i}
				<div class="skeleton-row" style:--i={i}></div>
			{/each}
		</div>
	{:else}
		<form class="panel" onsubmit={save} aria-busy={saving}>
			<header class="panel-head">
				<h2 class="panel-title">{$t('logscan.detail.config')}<span class="title-accent">.</span></h2>
				<span class="panel-sub">
					{$t('logscan.detail.configSub', { id: String(rule.id), scope: scopeLabel(rule) })}
				</span>
			</header>

			<div class="field">
				<label for="r-name" class="sub-label">{$t('logscan.form.name')}</label>
				<input id="r-name" type="text" class="input" bind:value={name} required />
			</div>

			<div class="field">
				<label for="r-pattern" class="sub-label">{$t('logscan.form.pattern')}</label>
				<input
					id="r-pattern"
					type="text"
					class="input mono"
					class:bad={!regexValid}
					bind:value={pattern}
					spellcheck="false"
					aria-invalid={!regexValid}
					aria-describedby={!regexValid ? 'r-pattern-err' : undefined}
					required
				/>
				{#if !regexValid}
					<span id="r-pattern-err" class="hint danger" role="alert">
						{$t('logscan.form.invalidRegex')}
					</span>
				{/if}
			</div>

			<div class="row three">
				<label class="sub" for="r-severity">
					<span class="sub-label">{$t('logscan.form.severity')}</span>
					<select id="r-severity" class="input" bind:value={severity}>
						<option value="info">info</option>
						<option value="warn">warn</option>
						<option value="error">error</option>
					</select>
				</label>
				<label class="sub" for="r-streams">
					<span class="sub-label">{$t('logscan.form.streams')}</span>
					<select id="r-streams" class="input" bind:value={streams}>
						<option value="all">all</option>
						<option value="stdout">stdout</option>
						<option value="stderr">stderr</option>
					</select>
				</label>
				<label class="sub" for="r-cooldown">
					<span class="sub-label">{$t('logscan.form.cooldown')}</span>
					<input
						id="r-cooldown"
						type="number"
						min="0"
						class="input"
						bind:value={cooldownSeconds}
					/>
				</label>
			</div>

			<div class="row-toggle">
				<div class="toggle-copy">
					<span class="lbl" aria-hidden="true">{$t('logscan.form.enabled')}</span>
					<p class="hint">{$t('logscan.form.enabledHint')}</p>
				</div>
				<ToggleSwitch bind:checked={enabled} label={$t('logscan.form.enabled')} />
			</div>

			<div class="actions">
				<button
					type="submit"
					class="forge-btn"
					disabled={saving || !name.trim() || !pattern || !regexValid}
					aria-busy={saving}
				>
					{saving ? $t('observability.saving') : $t('observability.save')}
				</button>
			</div>
		</form>

		<section class="panel" aria-labelledby="test-heading">
			<header class="panel-head">
				<h2 class="panel-title" id="test-heading">{$t('logscan.detail.regexTest')}<span class="title-accent">.</span></h2>
				<span class="panel-sub">{$t('logscan.detail.regexTestSub')}</span>
			</header>
			<RegexTestBox {pattern} bind:sample={sampleLine} />
			<div class="server-actions">
				<button
					type="button"
					class="forge-btn-ghost"
					onclick={runServerTest}
					disabled={testing || !sampleLine}
					aria-busy={testing}
					title={!sampleLine
						? $t('logscan.detail.serverTestHint')
						: $t('logscan.detail.serverTestSendHint')}
				>
					{testing ? $t('logscan.detail.testing') : $t('logscan.detail.runServerTest')}
				</button>
			</div>
			<div class="server-slot" aria-live="polite" aria-atomic="true">
				{#if serverTestResult}
					<div
						class="server-test"
						class:ok={serverTestResult.matched && !serverTestResult.error}
						class:fail={!!serverTestResult.error}
					>
						{#if serverTestResult.error}
							<span class="tag fail">{$t('logscan.detail.serverError')}</span>
							<pre class="server-pre muted">{serverTestResult.error}</pre>
						{:else if serverTestResult.matched}
							<span class="tag ok">{$t('logscan.detail.serverMatch')}</span>
							{#if serverTestResult.captures}
								<dl class="captures">
									{#each Object.entries(serverTestResult.captures) as [k, v]}
										<div class="cap">
											<dt>{k}</dt>
											<dd class="mono">{v || '∅'}</dd>
										</div>
									{/each}
								</dl>
							{/if}
						{:else}
							<span class="tag off">{$t('logscan.detail.serverNoMatch')}</span>
							<span class="muted">{$t('logscan.detail.serverNoMatchHint')}</span>
						{/if}
					</div>
				{/if}
			</div>
		</section>

		<section class="panel danger-panel" aria-labelledby="danger-heading">
			<header class="panel-head">
				<h2 class="panel-title" id="danger-heading">{$t('logscan.detail.dangerZone')}<span class="title-accent">.</span></h2>
				<span class="panel-sub">{$t('logscan.detail.dangerZoneSub')}</span>
			</header>
			<div class="danger-actions">
				<button
					type="button"
					class="forge-btn-ghost forge-btn-danger"
					onclick={() => (confirmDelete = true)}
				>
					{$t('logscan.detail.deleteButton')}
				</button>
			</div>
		</section>

		<ConfirmDialog
			open={confirmDelete}
			title={$t('logscan.detail.deleteTitle')}
			message={$t('logscan.detail.deleteMessage', { name: name.trim() || rule.name })}
			confirmLabel={deleting ? $t('observability.deleting') : $t('observability.delete')}
			confirmVariant="danger"
			onconfirm={doDelete}
			oncancel={() => (confirmDelete = false)}
		/>
	{/if}
</div>

<style>
	.forge {
		display: flex;
		flex-direction: column;
		gap: 1.25rem;
		max-width: 820px;
		margin: 0 auto;
	}

	/* ── Alert ─────────────────────────────────────── */
	.alert {
		display: flex;
		gap: 0.7rem;
		align-items: center;
		padding: 0.7rem 0.9rem;
		background: var(--color-danger-light);
		color: var(--color-danger-dark);
		border: 1px solid var(--color-danger);
		border-left-width: 4px;
		border-radius: var(--radius-lg);
		font-size: 0.875rem;
	}
	.alert-tag {
		font-family: var(--forge-mono);
		font-weight: 700;
		font-size: 0.65rem;
		letter-spacing: 0.16em;
		padding: 0.15rem 0.4rem;
		background: var(--color-danger);
		color: var(--surface-card);
		border-radius: var(--radius-sm);
	}
	:global([data-theme='dark']) .alert {
		background: color-mix(in srgb, var(--color-danger) 14%, transparent);
		color: color-mix(in srgb, var(--color-danger) 60%, var(--text-primary));
	}

	/* ── Skeleton ──────────────────────────────────── */
	.skeleton-rows {
		display: flex;
		flex-direction: column;
		gap: 0.55rem;
	}
	.skeleton-row {
		height: 64px;
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-2xl);
		background: linear-gradient(
			110deg,
			var(--surface-card) 20%,
			var(--surface-card-hover) 50%,
			var(--surface-card) 80%
		);
		background-size: 200% 100%;
		animation: shimmer 1.6s linear infinite;
		animation-delay: calc(var(--i) * 120ms);
	}
	@keyframes shimmer {
		0% {
			background-position: 200% 0;
		}
		100% {
			background-position: -200% 0;
		}
	}

	/* ── Panel ─────────────────────────────────────── */
	.panel {
		display: flex;
		flex-direction: column;
		gap: 0.9rem;
		background: var(--surface-card);
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-2xl);
		padding: 1.5rem;
	}
	@media (max-width: 600px) {
		.panel {
			padding: 1.1rem;
			gap: 1rem;
		}
	}
	.panel.danger-panel {
		border-color: color-mix(in srgb, var(--color-danger) 35%, var(--border-primary));
	}
	.panel-head {
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
		margin-bottom: 0.2rem;
	}
	.panel-title {
		margin: 0;
		font-size: 1.1rem;
		font-weight: 700;
		letter-spacing: -0.01em;
		color: var(--text-primary);
	}
	.title-accent {
		color: var(--forge-accent);
	}
	.panel-sub {
		font-family: var(--forge-mono);
		font-size: 0.7rem;
		color: var(--text-tertiary);
		line-height: 1.5;
	}

	/* ── Fields ────────────────────────────────────── */
	.field {
		display: flex;
		flex-direction: column;
		gap: 0.35rem;
	}
	.sub-label {
		font-family: var(--forge-mono);
		font-size: 0.62rem;
		font-weight: 600;
		letter-spacing: 0.14em;
		text-transform: uppercase;
		color: var(--text-secondary);
	}
	.input {
		width: 100%;
		background: var(--surface-input);
		border: 1px solid var(--border-input);
		border-radius: var(--radius-lg);
		padding: 0.55rem 0.75rem;
		font-size: 0.9rem;
		color: var(--text-primary);
		font-family: inherit;
		outline: none;
		transition: border-color 120ms ease, box-shadow 120ms ease;
	}
	.input:focus {
		border-color: var(--border-focus);
		box-shadow: 0 0 0 3px var(--forge-accent-soft);
	}
	.input.mono {
		font-family: var(--forge-mono);
		font-size: 0.85rem;
	}
	.input.bad {
		border-color: var(--color-danger);
	}
	.input.bad:focus {
		box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-danger) 22%, transparent);
	}

	.row.three {
		display: grid;
		grid-template-columns: 1fr 1fr 1fr;
		gap: 0.9rem;
	}
	@media (max-width: 720px) {
		.row.three {
			grid-template-columns: 1fr 1fr;
		}
	}
	@media (max-width: 480px) {
		.row.three {
			grid-template-columns: 1fr;
		}
	}
	.sub {
		display: flex;
		flex-direction: column;
		gap: 0.35rem;
		min-width: 0;
	}

	/* ── Hints ──────────────────────────────────────── */
	.hint {
		font-size: 0.78rem;
		color: var(--text-tertiary);
		line-height: 1.5;
		margin: 0;
	}
	.hint.danger {
		color: var(--color-danger);
	}

	/* ── Toggle row ─────────────────────────────────── */
	.row-toggle {
		display: flex;
		align-items: flex-start;
		justify-content: space-between;
		gap: 1rem;
		padding-top: 0.5rem;
		border-top: 1px dashed var(--border-primary);
		margin-top: 0.2rem;
	}
	.toggle-copy {
		flex: 1;
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
	}
	.lbl {
		font-weight: 600;
		font-size: 0.95rem;
		color: var(--text-primary);
	}

	/* ── Action footers ─────────────────────────────── */
	.actions {
		display: flex;
		justify-content: flex-end;
		gap: 0.55rem;
		padding-top: 0.4rem;
		flex-wrap: wrap;
	}
	.danger-actions {
		display: flex;
		justify-content: flex-end;
	}
	@media (max-width: 480px) {
		.actions {
			flex-direction: column-reverse;
			align-items: stretch;
		}
		.actions :global(.forge-btn),
		.actions :global(.forge-btn-ghost) {
			justify-content: center;
		}
		.danger-actions :global(.forge-btn-ghost) {
			width: 100%;
			justify-content: center;
		}
	}

	.server-actions {
		display: flex;
		justify-content: flex-end;
	}
	.server-slot {
		min-height: 0;
	}
	.server-test {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: 0.55rem;
		padding: 0.6rem 0.75rem;
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-lg);
		background: var(--surface-card-hover);
	}
	.server-test.ok {
		border-color: var(--color-success);
		background: color-mix(in srgb, var(--color-success) 8%, var(--surface-card));
	}
	.server-test.fail {
		border-color: var(--color-danger);
		background: color-mix(in srgb, var(--color-danger) 8%, var(--surface-card));
	}

	/* ── Tags ───────────────────────────────────────── */
	.tag {
		font-family: var(--forge-mono);
		font-weight: 700;
		font-size: 0.6rem;
		letter-spacing: 0.16em;
		padding: 0.1rem 0.4rem;
		background: var(--text-primary);
		color: var(--surface-card);
		border-radius: var(--radius-sm);
		flex: 0 0 auto;
	}
	.tag.ok {
		background: var(--color-success);
		color: var(--surface-card);
	}
	.tag.fail {
		background: var(--color-danger);
		color: var(--surface-card);
	}
	.tag.off {
		background: var(--text-tertiary);
		color: var(--surface-card);
	}

	/* ── Captures ───────────────────────────────────── */
	.captures {
		display: flex;
		flex-wrap: wrap;
		gap: 0.4rem;
		margin: 0;
		flex: 1 1 100%;
	}
	.cap {
		display: inline-flex;
		align-items: center;
		gap: 0.35rem;
		padding: 0.2rem 0.5rem;
		background: var(--surface-card);
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-md);
	}
	.cap dt {
		font-family: var(--forge-mono);
		font-size: 0.62rem;
		letter-spacing: 0.1em;
		color: var(--text-secondary);
		margin: 0;
	}
	.cap dd {
		margin: 0;
		font-size: 0.78rem;
		color: var(--text-primary);
	}

	.muted {
		color: var(--text-tertiary);
	}
	.mono {
		font-family: var(--forge-mono);
	}
	.server-pre {
		margin: 0;
		font-family: var(--forge-mono);
		font-size: 0.8rem;
		white-space: pre-wrap;
		word-break: break-word;
		max-width: 100%;
		flex: 1 1 100%;
	}
</style>
