<script lang="ts">
	import { Synced } from "$lib/spells/synced.svelte";
	import { TextareaAutosize } from "$lib/spells/textarea-autosize.svelte";
	import type { ConversationClass } from "$lib/state/conversations.svelte.js";
	import { safeParse } from "$lib/utils/json.js";
	import { keys } from "$lib/utils/object.svelte";
	import { onchange, oninput } from "$lib/utils/template.js";
	import { RadioGroup } from "melt/builders";
	import { codeToHtml } from "shiki";
	import typia from "typia";
	import IconX from "~icons/carbon/close";
	import Dialog from "../dialog.svelte";
	import { isDark } from "$lib/spells/is-dark.svelte";

	interface Props {
		conversation: ConversationClass;
		open: boolean;
	}

	let { conversation, open = $bindable(false) }: Props = $props();

	let tempSchema = $derived(conversation.data.structuredOutput?.schema ?? "");

	const modes = ["form", "code"] as const;
	const radioGroup = new RadioGroup({
		value: modes[0],
	});

	type Schema = {
		name?: string;
		description?: string;
		schema?: {
			type?: string;
			properties?: { [key: string]: { type: string; description?: string } };
			required?: string[];
			additionalProperties?: boolean;
		};
		strict?: boolean;
	};

	export function parseJsonSchema(): Schema {
		const parsed = safeParse(conversation.data.structuredOutput?.schema ?? "");
		const baseSchema = {
			schema: {
				type: "object",
				properties: {},
				required: [],
				additionalProperties: true,
			},
			strict: false,
		} satisfies Schema;

		if (typia.is<Schema>(parsed)) {
			return {
				...baseSchema,
				...parsed,
				schema: {
					...baseSchema.schema,
					...parsed.schema,
					properties: {
						...baseSchema.schema.properties,
						...parsed.schema?.properties,
					},
					required: Array.from(new Set([...(baseSchema.schema.required || []), ...(parsed.schema?.required || [])])),
				},
			};
		}

		return baseSchema;
	}

	const schemaObj = new Synced<Schema>({
		value: parseJsonSchema,
		onChange(v) {
			const required = Array.from(new Set(v.schema?.required)).filter(name =>
				keys(v.schema?.properties ?? {}).includes(name)
			);
			const validated: Schema = {
				name: v.name,
				description: v.description,
				schema: {
					...v.schema,
					required,
				},
				strict: v.strict,
			};
			conversation.update({
				structuredOutput: { ...conversation.data.structuredOutput, schema: JSON.stringify(validated, null, 2) },
			});
		},
	});

	function updateSchema(obj: Partial<Schema>) {
		schemaObj.current = { ...schemaObj.current, ...obj };
	}

	function updateSchemaNested(nestedObj: Partial<Schema["schema"]>) {
		updateSchema({
			schema: {
				...schemaObj.current.schema,
				...nestedObj,
			},
		});
	}

	let textarea = $state<HTMLTextAreaElement>();
	new TextareaAutosize({
		element: () => textarea,
		input: () => tempSchema,
	});
</script>

<Dialog title="Edit Structured Output" {open} onClose={() => (open = false)}>
	<div class="flex justify-end">
		<div
			class="flex items-center gap-0.5 rounded-md border border-gray-300 bg-white p-0.5 text-sm dark:border-gray-600 dark:bg-gray-800"
			{...radioGroup.root}
		>
			{#each modes as mode}
				{@const item = radioGroup.getItem(mode)}
				<div
					class={[
						"rounded px-2 py-0.5 capitalize select-none",
						item.checked ? "bg-gray-200 dark:bg-gray-700" : "hover:bg-gray-100 dark:hover:bg-gray-700/70",
					]}
					{...item.attrs}
				>
					{mode}
				</div>
			{/each}
		</div>
	</div>

	{#if radioGroup.value === "form"}
		<div class="fade-y -mx-2 mt-2 -mb-4 max-h-200 space-y-4 overflow-auto px-2 py-4 text-left">
			<!-- Top-level properties -->
			<div>
				<label for="schema-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
				<input
					type="text"
					id="schema-name"
					class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-2 py-1 text-gray-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
					value={schemaObj.current.name}
					{...onchange(value => updateSchema({ name: value }))}
				/>
			</div>

			<div>
				<label for="schema-description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
					Description
				</label>
				<textarea
					id="schema-description"
					rows="3"
					class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-2 py-1 text-gray-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
					value={schemaObj.current.description}
					{...onchange(value => updateSchema({ description: value }))}
				></textarea>
			</div>

			<!-- Properties Section -->
			<div class="border-t border-gray-200 pt-4 dark:border-gray-700">
				<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Properties</h3>
				{#if schemaObj.current.schema?.properties}
					<div class="mt-3 space-y-3">
						{#each Object.entries(schemaObj.current.schema.properties) as [propertyName, propertyDefinition], index (index)}
							<div
								class="relative space-y-2 rounded-md border border-gray-300 bg-white p-3 dark:border-gray-700 dark:bg-gray-900"
							>
								<div>
									<label for="{propertyName}-name" class="block text-xs font-medium text-gray-500 dark:text-gray-400">
										Name
									</label>
									<input
										type="text"
										id="{propertyName}-name"
										class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-2 py-1 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
										value={propertyName}
										{...onchange(value => {
											const updatedProperties = { ...schemaObj.current.schema?.properties };
											if (!updatedProperties || !updatedProperties[propertyName]) return;
											updatedProperties[value] = updatedProperties[propertyName];
											delete updatedProperties[propertyName];
											updateSchemaNested({ properties: updatedProperties });
										})}
									/>
								</div>

								<button
									type="button"
									class="absolute top-2 right-2 text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-500"
									onclick={() => {
										const updatedProperties = { ...schemaObj.current.schema?.properties };
										if (!updatedProperties || !updatedProperties[propertyName]) return;
										delete updatedProperties[propertyName];
										updateSchemaNested({ properties: updatedProperties });
									}}
									aria-label="delete"
								>
									<IconX />
								</button>

								<div>
									<label for="{propertyName}-type" class="block text-xs font-medium text-gray-500 dark:text-gray-400">
										Type
									</label>
									<select
										id="{propertyName}-type"
										class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-2 py-1 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
										bind:value={
											() => propertyDefinition.type,
											value => {
												const updatedProperties = { ...schemaObj.current.schema?.properties };
												if (updatedProperties && updatedProperties[propertyName]) {
													updatedProperties[propertyName].type = value;
													updateSchemaNested({ properties: updatedProperties });
												}
											}
										}
									>
										<option value="string">string</option>
										<option value="integer">integer</option>
										<option value="number">number</option>
										<option value="boolean">boolean</option>
										<option value="array">array</option>
										<option value="object">object</option>
										<option value="enum">enum</option>
										<option value="null">null</option>
									</select>
								</div>

								<div>
									<label
										for="{propertyName}-description"
										class="block text-xs font-medium text-gray-500 dark:text-gray-400">Description</label
									>
									<input
										type="text"
										id="{propertyName}-description"
										class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-2 py-1 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
										value={propertyDefinition.description}
										{...onchange(value => {
											const updatedProperties = { ...schemaObj.current.schema?.properties };
											if (!updatedProperties || !updatedProperties[propertyName]) return;
											updatedProperties[propertyName].description = value;
											updateSchemaNested({ properties: updatedProperties });
										})}
									/>
								</div>

								<div class="flex items-start">
									<div class="flex h-5 items-center">
										<input
											id="required-{propertyName}"
											aria-describedby="required-{propertyName}-description"
											name="required-{propertyName}"
											type="checkbox"
											class="h-4 w-4 rounded border border-gray-300 bg-white text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
											checked={schemaObj.current.schema?.required?.includes(propertyName)}
											onchange={e => {
												let updatedRequired = [...(schemaObj.current.schema?.required || [])];
												if (e.currentTarget.checked) {
													if (!updatedRequired.includes(propertyName)) {
														updatedRequired.push(propertyName);
													}
												} else {
													updatedRequired = updatedRequired.filter(name => name !== propertyName);
												}
												updateSchemaNested({ required: updatedRequired });
											}}
										/>
									</div>
									<div class="ml-3 text-sm">
										<label for="required-{propertyName}" class="font-medium text-gray-700 dark:text-gray-300">
											Required
										</label>
									</div>
								</div>
							</div>
						{:else}
							<p class="mt-3 text-sm text-gray-500">No properties defined yet.</p>
						{/each}
					</div>
				{:else}
					<p class="mt-3 text-sm text-gray-500">No properties defined yet.</p>
				{/if}

				<button
					type="button"
					class="btn-sm mt-4 flex w-full items-center justify-center rounded-md"
					onclick={() => {
						const newPropertyName = `newProperty${Object.keys(schemaObj.current.schema?.properties || {}).length + 1}`;
						const updatedProperties = {
							...(schemaObj.current.schema?.properties || {}),
							[newPropertyName]: { type: "string", description: "" },
						};
						updateSchemaNested({ properties: updatedProperties });
					}}
				>
					Add Property
				</button>
			</div>

			<!-- Strict and Additional Properties -->
			<div class="border-t border-gray-200 pt-4 dark:border-gray-700">
				<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Options</h3>
				<div class="mt-3 space-y-2">
					<div class="relative flex items-start">
						<div class="flex h-5 items-center">
							<input
								id="additionalProperties"
								name="additionalProperties"
								type="checkbox"
								class="h-4 w-4 rounded border border-gray-300 bg-white text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
								checked={schemaObj.current.schema?.additionalProperties !== undefined
									? schemaObj.current.schema.additionalProperties
									: true}
								onchange={e => updateSchemaNested({ additionalProperties: e.currentTarget.checked })}
							/>
						</div>
						<div class="ml-3 text-sm">
							<label for="additionalProperties" class="font-medium text-gray-700 dark:text-gray-300">
								Allow additional properties
							</label>
							<p id="additionalProperties-description" class="text-gray-500">
								If unchecked, only properties defined in the schema are allowed.
							</p>
						</div>
					</div>

					<div class="relative flex items-start">
						<div class="flex h-5 items-center">
							<input
								id="strict"
								name="strict"
								type="checkbox"
								class="h-4 w-4 rounded border border-gray-300 bg-white text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
								checked={schemaObj.current.strict !== undefined ? schemaObj.current.strict : false}
								onchange={e => updateSchema({ strict: e.currentTarget.checked })}
							/>
						</div>
						<div class="ml-3 text-sm">
							<label for="strict" class="font-medium text-gray-700 dark:text-gray-300">Strict mode</label>
							<p id="strict-description" class="text-gray-500">Enforces stricter validation rules.</p>
						</div>
					</div>
				</div>
			</div>
		</div>
	{:else}
		<!-- inside dialogs its a-ok -->
		<!-- svelte-ignore a11y_autofocus  -->
		<div
			class="relative mt-2 max-h-120 overflow-x-clip overflow-y-auto rounded-lg bg-gray-100 text-left ring-gray-100 focus-within:ring-3 dark:bg-gray-800 dark:ring-gray-600"
		>
			<div class="shiki-container pointer-events-none absolute inset-0" aria-hidden="true">
				{#await codeToHtml(tempSchema, { lang: "json", theme: isDark() ? "catppuccin-macchiato" : "catppuccin-latte" })}
					<!-- nothing -->
				{:then rendered}
					{@html rendered}
				{/await}
			</div>
			<textarea
				bind:this={textarea}
				autofocus
				value={conversation.data.structuredOutput?.schema ?? ""}
				{...onchange(v => {
					conversation.update({ structuredOutput: { ...conversation.data.structuredOutput, schema: v } });
				})}
				{...oninput(v => (tempSchema = v))}
				class="relative z-10 h-120 w-full resize-none overflow-hidden rounded-lg bg-transparent whitespace-pre-wrap text-transparent caret-black outline-none @2xl:px-3 dark:caret-white"
			></textarea>
		</div>
	{/if}

	{#snippet footer()}
		<button class="btn ml-auto" onclick={() => (open = false)}> Save </button>
	{/snippet}
</Dialog>

<style>
	.shiki-container > :global(pre),
	.shiki-container + textarea {
		padding-block: 10px;
		padding-inline: 8px;
		font-family: var(--font-mono) !important;
		font-size: 15px;
	}

	.shiki-container > :global(*) {
		background-color: transparent !important;
		font-family: var(--font-mono) !important;
	}

	.shiki-container > :global(pre) {
		border-radius: 8px;
		height: 100%;
		white-space: pre-wrap;
	}
</style>
