<script lang="ts">
	import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
	import Search from '@lucide/svelte/icons/search';
	import { api } from '$lib/api/client';
	import { errorMessage } from '$lib/api/errors';
	import type { components } from '$lib/api/schema';
	import Badge from '$lib/components/Badge.svelte';
	import Button from '$lib/components/Button.svelte';
	import FormField from '$lib/components/FormField.svelte';
	import Input from '$lib/components/Input.svelte';
	import Select from '$lib/components/Select.svelte';
	import Tooltip from '$lib/components/Tooltip.svelte';
	import { m } from '$lib/paraglide/messages';

	type Setting = components['schemas']['LLMSettingOut'];
	type Role = Setting['role'];
	type Status = components['schemas']['LLMRoleStatus'];

	type Draft = { base_url: string; model: string; api_key: string };
	type Discovery = { models: string[]; supported: boolean; loading: boolean };

	let settings = $state<Setting[]>([]);
	// Per-role form state, seeded with the stored values so the fields show
	// what is actually configured rather than an empty box over a placeholder.
	let draft = $state<Record<string, Draft>>({});
	let tested = $state<Record<string, Status | undefined>>({});
	let discovered = $state<Record<string, Discovery | undefined>>({});
	let busy = $state<string | null>(null);
	let error = $state<string | null>(null);

	async function refresh() {
		const { data } = await api.GET('/api/admin/llm/settings');
		settings = data ?? [];
		for (const setting of settings) {
			draft[setting.role] = {
				base_url: setting.base_url,
				model: setting.model,
				// Never round-trips: the key is write-only, so the field stays
				// empty and an empty field means "keep the stored one".
				api_key: ''
			};
		}
	}

	$effect(() => void refresh());

	function candidate(role: Role) {
		const form = draft[role];
		return {
			role,
			base_url: form.base_url || undefined,
			model: form.model || undefined,
			api_key: form.api_key || undefined
		};
	}

	/** Validate before storing: a wrong endpoint should fail here, not on
	 *  the next user question. */
	async function test(role: Role) {
		busy = role;
		error = null;
		const { data } = await api.POST('/api/admin/llm/test', { body: candidate(role) });
		tested[role] = data?.roles?.[0];
		busy = null;
	}

	/** Ask the endpoint what it serves. Runs server-side, so the key never
	 *  leaves the backend. */
	async function discover(role: Role) {
		const form = draft[role];
		discovered[role] = { models: [], supported: true, loading: true };
		const { data } = await api.POST('/api/admin/llm/models/{role}', {
			params: { path: { role } },
			body: { base_url: form.base_url || undefined, api_key: form.api_key || undefined }
		});
		discovered[role] = {
			models: data?.models ?? [],
			supported: data?.supported ?? false,
			loading: false
		};
	}

	async function save(role: Role) {
		busy = role;
		error = null;
		const form = draft[role];
		const stored = settings.find((setting) => setting.role === role);
		// Only send what actually changed. Submitting every field would mark
		// the untouched ones as hand-edited, so fixing a URL would quietly
		// claim the model no longer comes from .env.
		const { error: failed } = await api.PUT('/api/admin/llm/settings/{role}', {
			params: { path: { role } },
			body: {
				base_url: form.base_url === stored?.base_url ? undefined : form.base_url,
				model: form.model === stored?.model ? undefined : form.model,
				// Empty means "keep the stored key", not "delete it".
				api_key: form.api_key || undefined
			}
		});
		busy = null;
		if (failed) {
			error = m.admin_llm_save_failed();
			return;
		}
		tested[role] = undefined;
		await refresh();
	}

	/** Put one field back to what .env says. Per field, because an admin who
	 *  fixed the model has no reason to lose their URL. */
	async function reset(role: Role, field: 'base_url' | 'model' | 'api_key') {
		busy = role;
		await api.PUT('/api/admin/llm/settings/{role}', {
			params: { path: { role } },
			body: { [`reset_${field}`]: true }
		});
		busy = null;
		tested[role] = undefined;
		await refresh();
	}

	function sourceLabel(fromEnv: boolean): string {
		return fromEnv ? m.admin_llm_source_env() : m.admin_llm_source_ui();
	}
</script>

{#snippet source(setting: Setting, field: 'base_url' | 'model' | 'api_key', fromEnv: boolean)}
	<span class="flex items-center gap-1 text-xs font-normal text-ink-muted">
		{sourceLabel(fromEnv)}
		{#if !fromEnv}
			<!-- Long sentence as the tooltip, short name as the accessible
			     label: a screen reader announcing a whole sentence for a small
			     icon button is noise. -->
			<Tooltip
				text={m.admin_llm_reset_field()}
				label={m.admin_llm_reset_field_short()}
				onclick={() => reset(setting.role, field)}
			>
				<span class="text-ink-muted transition-colors hover:text-ink">
					<RotateCcw size={12} />
				</span>
			</Tooltip>
		{/if}
	</span>
{/snippet}

<div class="flex flex-col gap-4" data-testid="llm-settings">
	<p class="text-xs text-ink-muted">
		{m.admin_llm_intro()}
	</p>

	{#each settings as setting (setting.role)}
		<div class="rounded-xl border border-border p-4">
			<div class="mb-3 flex flex-wrap items-center gap-2">
				<span class="font-medium">{setting.role}</span>
				{#if tested[setting.role]}
					<Badge variant={tested[setting.role]?.ok ? 'success' : 'danger'}>
						{tested[setting.role]?.ok
							? `ok · ${tested[setting.role]?.latency_ms} ms`
							: m.admin_llm_endpoint_failed()}
					</Badge>
					{#if !tested[setting.role]?.ok}
						<!-- Why it failed, in words, plus the sanitized technical detail
						     the admin needs to fix it. -->
						<span class="text-xs text-ink-muted">
							{errorMessage(tested[setting.role]?.code)}
							{#if tested[setting.role]?.error}
								<code class="ml-1">{tested[setting.role]?.error}</code>
							{/if}
						</span>
					{/if}
				{/if}
			</div>

			<div class="grid gap-3 md:grid-cols-2">
				<div class="min-w-0">
					<FormField label={m.admin_llm_base_url()} for="{setting.role}-url">
						{#snippet hint()}{@render source(
								setting,
								'base_url',
								setting.base_url_from_env
							)}{/snippet}
						<Input
							id="{setting.role}-url"
							placeholder={m.admin_llm_base_url_placeholder()}
							bind:value={draft[setting.role].base_url}
						/>
					</FormField>
				</div>
				<div class="min-w-0">
					<FormField label={m.admin_llm_model()} for="{setting.role}-model">
						{#snippet hint()}{@render source(setting, 'model', setting.model_from_env)}{/snippet}
						<div class="flex items-center gap-1">
							{#if discovered[setting.role]?.models?.length}
								<!-- A dropdown once we know what the endpoint serves; the
								     free-text field stays reachable via "Enter manually". -->
								<Select
									id="{setting.role}-model"
									class="min-w-0 flex-1"
									bind:value={draft[setting.role].model}
									data-testid="model-select-{setting.role}"
								>
									{#each discovered[setting.role]?.models ?? [] as name (name)}
										<option value={name}>{name}</option>
									{/each}
								</Select>
								<Button
									variant="ghost"
									size="sm"
									class="shrink-0 whitespace-nowrap"
									onclick={() => (discovered[setting.role] = undefined)}
								>
									{m.admin_llm_enter_manually()}
								</Button>
							{:else}
								<div class="min-w-0 flex-1">
									<Input
										id="{setting.role}-model"
										placeholder={m.admin_llm_model_placeholder()}
										bind:value={draft[setting.role].model}
									/>
								</div>
								<Button
									variant="ghost"
									size="sm"
									class="shrink-0 whitespace-nowrap"
									disabled={discovered[setting.role]?.loading}
									onclick={() => discover(setting.role)}
									data-testid="discover-{setting.role}"
								>
									<Search size={14} />
									{discovered[setting.role]?.loading
										? m.admin_llm_checking()
										: m.admin_llm_check_models()}
								</Button>
							{/if}
						</div>
					</FormField>
					{#if discovered[setting.role] && !discovered[setting.role]?.loading && !discovered[setting.role]?.supported}
						<p class="mt-1 text-xs text-ink-muted" data-testid="no-model-list-{setting.role}">
							{m.admin_llm_no_model_list()}
						</p>
					{/if}
				</div>
				<div class="min-w-0 md:col-span-2">
					<FormField label={m.admin_llm_api_key()} for="{setting.role}-key">
						{#snippet hint()}{@render source(
								setting,
								'api_key',
								setting.api_key_from_env
							)}{/snippet}
						<Input
							id="{setting.role}-key"
							type="password"
							placeholder={setting.api_key_set ? '••••••••' : m.admin_llm_api_key_unset()}
							bind:value={draft[setting.role].api_key}
						/>
					</FormField>
				</div>
			</div>

			{#if tested[setting.role]?.error}
				<p class="mt-2 text-xs text-danger">{tested[setting.role]?.error}</p>
			{/if}

			<div class="mt-3 flex flex-wrap gap-2">
				<Button
					variant="ghost"
					size="sm"
					disabled={busy !== null}
					onclick={() => test(setting.role)}
					data-testid="test-{setting.role}"
				>
					{busy === setting.role ? m.admin_llm_testing_role() : m.admin_llm_test_role()}
				</Button>
				<Button
					size="sm"
					disabled={busy !== null || !tested[setting.role]?.ok}
					onclick={() => save(setting.role)}
					data-testid="save-{setting.role}"
				>
					{m.admin_llm_save()}
				</Button>
			</div>
			<p class="mt-2 text-xs text-ink-muted">{m.admin_llm_api_key_note()}</p>
		</div>
	{/each}

	{#if error}
		<p role="alert" class="text-sm text-danger">{error}</p>
	{/if}
</div>
