<script lang="ts">
	import { PenLine, Send, X } from '@lucide/svelte';
	import {
		Handle,
		useEdges,
		useNodes,
		useNodesData,
		Position,
		type NodeProps,
		type Edge,
		type Node,
		useSvelteFlow
	} from '@xyflow/svelte';
	import { onMount } from 'svelte';

	import ErrorMessage from '$lib/components/error/Error.svelte';
	import type { ChatMessage } from '$lib/helpers/types';
	import { Button } from '$lib/components/ui/button';
	import ComboBoxModels from '$lib/components/model/ComboBoxModels.svelte';
	import Spinner from '$lib/components/loading/Spinner.svelte';
	import Message from './Message.svelte';
	import { MAX_SUGGESTIONS } from '$lib';
	import { triggerAiCall } from '$lib/chat';
	import { SUGGESTIONS_PROMPT } from '$lib/consts';
	import { authState } from '$lib/state/auth.svelte';
	import { signinModalState } from '$lib/state/signin-modal.svelte';
	import Welcome from './Welcome.svelte';
	import ListModels from '$lib/components/model/ListModels.svelte';
	import { breakpointsState } from '$lib/state/breakpoints.svelte';

	let { id }: NodeProps = $props();

	// svelte-ignore state_referenced_locally
	const nodeData = useNodesData(id);
	const { update: updateNodes } = useNodes();
	const { update: updateEdges } = useEdges();
	const { updateNodeData, deleteElements } = useSvelteFlow();

	let selectedModels = $derived<string[]>(
		(nodeData.current?.data.selectedModels as string[]) ?? []
	);
	let messages = $derived((nodeData.current?.data.messages as ChatMessage[]) ?? []);
	let isFirstNode = $derived((nodeData.current?.data.isFirstNode as boolean) ?? false);
	let showWelcome = $derived((nodeData.current?.data.showWelcome as boolean) ?? true);
	let isParentNode = $derived((nodeData.current?.data.isParentNode as boolean) ?? false);
	let isFromEdit = $derived((nodeData.current?.data.isFromEdit as boolean) ?? false);

	let prompt = $state.raw<string>((nodeData.current?.data.prompt as string) ?? '');
	let loading = $state.raw<boolean>(false);
	let errorMessage = $state.raw<Set<string>>(new Set());

	const randomSuggestions = SUGGESTIONS_PROMPT.sort(() => Math.random() - 0.5).slice(
		0,
		breakpointsState.isMobile ? 1 : MAX_SUGGESTIONS
	);

	function toggleModel(modelId: string) {
		updateNodeData(
			id,
			{
				...nodeData.current?.data,
				selectedModels: selectedModels.includes(modelId)
					? selectedModels.filter((m) => m !== modelId)
					: [...selectedModels, modelId]
			},
			{ replace: true }
		);
	}

	function handleTriggerAction(models: string[] = selectedModels) {
		if (!authState.user) {
			signinModalState.open = true;
			return;
		}
		errorMessage = new Set();
		const newNodes: Node[] = [];
		const newEdges: Edge[] = [];
		const newMessages = [...messages, { role: 'user', content: prompt }] as ChatMessage[];
		updateNodeData(
			id,
			{
				...nodeData.current?.data,
				messages: newMessages,
				selectedModels: models
			},
			{ replace: true }
		);

		models.forEach((m) => {
			const newNodeId = `assistant-${crypto.randomUUID()}`;
			const newNode: Node = {
				id: newNodeId,
				type: 'assistant',
				position: {
					x: 0,
					y: 0
				},
				data: {
					role: 'assistant',
					selectedModel: m,
					content: '',
					loading: true,
					messages: newMessages
				}
			};
			const newEdge: Edge = {
				id: `edge-${crypto.randomUUID()}`,
				source: id,
				target: newNodeId
			};
			newNodes.push(newNode);
			newEdges.push(newEdge);
		});
		updateNodes((currentNodes) => [...currentNodes, ...newNodes]);
		updateEdges((currentEdges) => [...currentEdges, ...newEdges]);
		triggerAiCall({
			userId: id,
			newNodes,
			messages: newMessages,
			selectedModels: models,
			prompt,
			nodeData: nodeData.current?.data as Record<string, unknown>,
			authToken: authState.token ?? '',
			billingOption: authState.user?.billingOption ?? 'personal',
			updateNodeData: (nodeId, data, opts) =>
				updateNodeData(nodeId, data, { replace: opts?.replace ?? true }),
			updateNodes,
			updateEdges,
			onLoadingChange: (v) => (loading = v),
			onError: (msg) => {
				errorMessage = new Set([...errorMessage, msg]);
			}
		});
	}

	let lastMessage = $derived(
		messages?.length > 0 && messages[messages.length - 1].role === 'user'
			? messages[messages.length - 1]
			: null
	);

	function handlePromptInput(e: Event & { currentTarget: EventTarget & HTMLTextAreaElement }) {
		const value = (e.target as HTMLTextAreaElement).value;
		if (isFirstNode) {
			if (value.trim() === '') {
				updateNodeData(id, { ...nodeData.current?.data, showWelcome: true }, { replace: true });
			} else {
				updateNodeData(id, { ...nodeData.current?.data, showWelcome: false }, { replace: true });
			}
		}
	}

	function handleDeleteNode() {
		deleteElements({ nodes: [{ id: id }] });
	}

	function handleEditMessage(newContent: string) {
		const newNodeId = `user-${crypto.randomUUID()}`;
		const prevMessages = messages.slice(0, -1);
		const newNode: Node = {
			id: newNodeId,
			type: 'user',
			position: { x: 0, y: 0 },
			data: {
				role: 'user',
				selectedModels,
				messages: prevMessages,
				isFirstNode: false,
				isFromEdit: true,
				showWelcome: false,
				isParentNode: true,
				prompt: newContent
			}
		};
		const newEdge: Edge = {
			id: `edge-${crypto.randomUUID()}`,
			source: id,
			target: newNodeId
		};
		updateNodes((currentNodes) => [...currentNodes, newNode]);
		updateEdges((currentEdges) => [...currentEdges, newEdge]);
	}

	onMount(() => {
		if (prompt.trim() !== '' && prompt) {
			handleTriggerAction();
			prompt = '';
		}
	});
</script>

<article
	class="group/user relative z-10 w-[calc(100dvw-2rem)] rounded-3xl border border-border bg-background p-5 shadow-lg/5 lg:w-[600px]"
>
	<div class="nodrag pointer-events-auto cursor-auto">
		{#if isFromEdit}
			<span
				class="mb-2 inline-flex items-center justify-center gap-1 rounded-md bg-accent px-2 py-1 text-[11px] text-muted-foreground"
			>
				<PenLine class="size-2.5" />
				Edited message
			</span>
		{/if}
		<header class="mb-3 flex items-center justify-between">
			<div class="flex flex-wrap items-center gap-1">
				<ListModels {selectedModels} showSelector={!lastMessage} onToggleModel={toggleModel} />
				{#if !lastMessage && !loading}
					<ComboBoxModels onSelect={toggleModel} excludeIds={selectedModels} />
				{/if}
			</div>
		</header>
		<ErrorMessage bind:error={errorMessage} />
		{#if lastMessage}
			<Message nodeId={id} message={lastMessage} onEdit={handleEditMessage} />
		{:else}
			<footer class="flex flex-col items-end transition-all duration-300">
				<textarea
					name="message"
					id="message"
					placeholder="Ask me anything..."
					disabled={loading}
					class="w-full resize-none border-none bg-transparent text-base text-accent-foreground outline-none"
					bind:value={prompt}
					oninput={handlePromptInput}
					onkeydown={(e: KeyboardEvent) => {
						if (e.key === 'Enter' && !e.shiftKey) {
							e.preventDefault();
							prompt = prompt.trim();
							if (prompt) {
								handleTriggerAction();
							}
						}
					}}
				></textarea>
				<div class="flex w-full items-end justify-between gap-1">
					{#if isFirstNode && !loading && !lastMessage}
						<div class="items flex w-full gap-1">
							{#each randomSuggestions as suggestion}
								<Button
									variant="outline"
									size="2xs"
									class="rounded-full! shadow-none!"
									disabled={!selectedModels.length}
									onclick={() => {
										updateNodeData(
											id,
											{ ...nodeData.current?.data, showWelcome: false },
											{ replace: true }
										);
										prompt = suggestion;
										handleTriggerAction();
									}}
								>
									{suggestion}
								</Button>
							{/each}
						</div>
					{:else}
						<div></div>
					{/if}
					<Button
						variant={!selectedModels.length || !prompt ? 'outline' : 'default'}
						size="icon-sm"
						class=""
						disabled={!selectedModels.length || !prompt || loading}
						onclick={() => handleTriggerAction()}
					>
						{#if loading}
							<Spinner className="size-5" />
						{:else}
							<Send />
						{/if}
					</Button>
				</div>
			</footer>
		{/if}
	</div>
	{#if isFirstNode}
		<Welcome bind:showWelcome />
	{:else if isParentNode}
		<Button
			size="icon-lg"
			variant="outline"
			class="absolute -top-2 -right-2 opacity-0 transition-opacity duration-300 group-hover/user:opacity-100"
			onclick={handleDeleteNode}
		>
			<X />
		</Button>
	{/if}
</article>
<Handle type="target" id="t-top" position={Position.Top} class="opacity-0" />
<Handle type="target" id="t-left" position={Position.Left} class="opacity-0" />
<Handle type="target" id="t-right" position={Position.Right} class="opacity-0" />
<Handle type="source" id="s-bottom" position={Position.Bottom} class="opacity-0" />
<Handle type="source" id="s-left" position={Position.Left} class="opacity-0" />
<Handle type="source" id="s-right" position={Position.Right} class="opacity-0" />
