import type { CSSProperties } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { v4 as uuidv4 } from "uuid";

import IconPaperclip from "virtual:icons/mdi/paperclip";
import IconSend from "virtual:icons/mdi/send";

import ChatFile from "./ChatFile";
import { useChat, useI18n, useOptions } from "../composables";
import { chatEventBus } from "../event-buses";
import type { ChatMessage } from "../types";
import { constructChatWebsocketUrl } from "../utils";

import "./Input.scss";

export interface ChatInputProps {
	placeholder?: string;
}

const defaultPlaceholderKey = "inputPlaceholder";
type CSSVariableProperties = CSSProperties & Record<string, string>;

export default function Input({
	placeholder = defaultPlaceholderKey,
}: ChatInputProps) {
	const { t } = useI18n();
	const { options } = useOptions();
	const chatStore = useChat();

	const [inputValue, setInputValue] = useState("");
	const [files, setFiles] = useState<File[]>([]);
	const [isSubmitting, setIsSubmitting] = useState(false);
	const [waitingForChatResponse, setWaitingForChatResponse] = useState(false);

	const textareaRef = useRef<HTMLTextAreaElement | null>(null);
	const fileInputRef = useRef<HTMLInputElement | null>(null);
	const resizeObserverRef = useRef<ResizeObserver | null>(null);

	const allowFileUploads = Boolean(options.allowFileUploads);
	const allowedFileTypes = options.allowedFilesMimeTypes ?? "";
	const complianceNotice = options.complianceNotice?.trim() ?? "";
	const isTopicSelectionPending = chatStore.isTopicSelectionPending;
	const isDisabled = Boolean(options.disabled || isTopicSelectionPending);
	const placeholderKey = isTopicSelectionPending
		? "topicSelectionInputPlaceholder"
		: placeholder;

	const isSubmitDisabled = useMemo(() => {
		if (waitingForChatResponse) return false;
		return (
			inputValue.trim() === "" ||
			chatStore.waitingForResponse ||
			isDisabled
		);
	}, [
		waitingForChatResponse,
		inputValue,
		chatStore.waitingForResponse,
		isDisabled,
	]);

	const isFileUploadDisabled = chatStore.waitingForResponse || isDisabled;

	const styleVars = useMemo<CSSVariableProperties>(
		() => ({
			"--controls-count": (allowFileUploads ? 2 : 1).toString(),
		}),
		[allowFileUploads],
	);

	const adjustTextAreaHeight = useCallback(() => {
		const textarea = textareaRef.current;
		if (!textarea) return;
		textarea.style.height = "var(--chat--textarea--height)";
		const newHeight = Math.min(textarea.scrollHeight, 480);
		textarea.style.height = `${newHeight}px`;
	}, []);

	useEffect(() => {
		const focusHandler = () => textareaRef.current?.focus();
		const blurHandler = () => textareaRef.current?.blur();
		const valueHandler = (value?: string) => {
			setInputValue(value ?? "");
			focusHandler();
		};

		const offFocus = chatEventBus.on("focusInput", focusHandler);
		const offBlur = chatEventBus.on("blurInput", blurHandler);
		const offSetValue = chatEventBus.on("setInputValue", valueHandler);

		return () => {
			offFocus();
			offBlur();
			offSetValue();
		};
	}, []);

	useEffect(() => {
		if (!textareaRef.current) return;

		resizeObserverRef.current = new ResizeObserver(() =>
			adjustTextAreaHeight(),
		);
		resizeObserverRef.current.observe(textareaRef.current);

		return () => {
			resizeObserverRef.current?.disconnect();
			resizeObserverRef.current = null;
		};
	}, [adjustTextAreaHeight]);

	const appendMessage = chatStore.appendMessage;

	const attachFiles = useCallback(() => {
		if (!files.length) return [];
		const currentFiles = [...files];
		setFiles([]);
		return currentFiles;
	}, [files]);

	const processFiles = useCallback(async (data: File[] | undefined) => {
		if (!data || data.length === 0) return [];

		return await Promise.all(
			data.map(
				(file) =>
					new Promise<{ name: string; type: string; data: string }>(
						(resolve, reject) => {
							const reader = new FileReader();

							reader.onload = () =>
								resolve({
									name: file.name,
									type: file.type,
									data: reader.result as string,
								});

							reader.onerror = () =>
								reject(
									new Error(
										`Error reading file: ${reader.error?.message ?? "Unknown error"}`,
									),
								);

							reader.readAsDataURL(file);
						},
					),
			),
		);
	}, []);

	const respondToChatNode = useCallback(
		async (ws: WebSocket, messageText: string, attachedFiles: File[]) => {
			const sentMessage: ChatMessage = {
				id: uuidv4(),
				text: messageText,
				sender: "user",
				files: attachedFiles,
			};

			appendMessage(sentMessage);
			ws.send(
				JSON.stringify({
					sessionId: chatStore.currentSessionId,
					action: "sendMessage",
					chatInput: messageText,
					files: await processFiles(attachedFiles),
				}),
			);
			chatStore.setWaitingForResponse(true);
			setWaitingForChatResponse(false);
		},
		[appendMessage, chatStore, processFiles],
	);

	const setupWebsocketConnection = useCallback(
		(executionId: string) => {
			if (!options.webhookUrl || !chatStore.currentSessionId) return;

			try {
				const wsUrl = constructChatWebsocketUrl(
					options.webhookUrl,
					executionId,
					chatStore.currentSessionId,
					true,
				);
				const ws = new WebSocket(wsUrl);
				chatStore.websocketRef.current = ws;
				ws.onmessage = (event) => {
					if (event.data === "chat-widget|heartbeat") {
						ws.send("chat-widget|heartbeat-ack");
						return;
					}

					if (event.data === "chat-widget|continue") {
						setWaitingForChatResponse(false);
						chatStore.setWaitingForResponse(true);
						return;
					}

					const newMessage: ChatMessage = {
						id: uuidv4(),
						text: event.data,
						sender: "bot",
					};

					appendMessage(newMessage);
					setWaitingForChatResponse(true);
					chatStore.setWaitingForResponse(false);
				};

				ws.onclose = () => {
					chatStore.websocketRef.current = null;
					setWaitingForChatResponse(false);
					chatStore.setWaitingForResponse(false);
				};
			} catch (error) {
				console.error("Error setting up websocket connection", error);
			}
		},
		[appendMessage, chatStore, options.webhookUrl],
	);

	const handleSubmit = useCallback(
		async (event: React.MouseEvent | React.KeyboardEvent) => {
			event.preventDefault();
			if (isSubmitDisabled) return;

			const messageText = inputValue;
			const attachedFiles = attachFiles();
			setInputValue("");
			setIsSubmitting(true);

			if (chatStore.websocketRef.current && waitingForChatResponse) {
				await respondToChatNode(
					chatStore.websocketRef.current,
					messageText,
					attachedFiles,
				);
				setIsSubmitting(false);
				return;
			}

			const response = await chatStore.sendMessage(
				messageText,
				attachedFiles,
			);

			if (response?.executionId) {
				setupWebsocketConnection(response.executionId);
			}

			setIsSubmitting(false);
		},
		[
			isSubmitDisabled,
			inputValue,
			attachFiles,
			chatStore,
			waitingForChatResponse,
			respondToChatNode,
			setupWebsocketConnection,
		],
	);

	const handleEnterKey = async (
		event: React.KeyboardEvent<HTMLTextAreaElement>,
	) => {
		const isComposing = Boolean(
			(event.nativeEvent &&
				"isComposing" in event.nativeEvent &&
				event.nativeEvent.isComposing) ||
				("isComposing" in event &&
					(event as unknown as { isComposing?: boolean })
						.isComposing),
		);

		if (event.shiftKey || isComposing) {
			return;
		}

		await handleSubmit(event);
		adjustTextAreaHeight();
	};

	const handleFileRemove = (file: File) => {
		setFiles((prev) =>
			prev.filter((current) => current.name !== file.name),
		);
	};

	const handleFileButtonClick = () => {
		if (isFileUploadDisabled) return;
		fileInputRef.current?.click();
	};

	const handleFileInputChange = (
		event: React.ChangeEvent<HTMLInputElement>,
	) => {
		const newFiles = Array.from(event.target.files ?? []);
		if (!newFiles.length) return;
		setFiles((prev) => [...prev, ...newFiles]);
		event.target.value = "";
	};

	const handleTextareaKeyDown = (
		event: React.KeyboardEvent<HTMLTextAreaElement>,
	) => {
		if (event.key === "ArrowUp" || event.key === "ArrowDown") {
			event.preventDefault();
		} else if (event.key === "Escape") {
			event.preventDefault();
		}
	};

	return (
		<div
			className="chat-input"
			style={styleVars}
			onKeyDown={(event) => event.stopPropagation()}
		>
			<div className="chat-inputs">
				<textarea
					ref={textareaRef}
					value={inputValue}
					data-test-id="chat-input"
					disabled={isDisabled}
					placeholder={t(placeholderKey)}
					onChange={(event) => {
						setInputValue(event.target.value);
						adjustTextAreaHeight();
					}}
					onKeyDown={(event) => {
						handleTextareaKeyDown(event);
						if (event.key === "Enter") {
							void handleEnterKey(event);
						}
					}}
					onInput={adjustTextAreaHeight}
					onMouseDown={adjustTextAreaHeight}
					onFocus={adjustTextAreaHeight}
				/>

				<div className="chat-inputs-controls">
					{allowFileUploads && (
						<button
							type="button"
							disabled={isFileUploadDisabled}
							className="chat-input-file-button"
							data-test-id="chat-attach-file-button"
							aria-label="Attach files"
							onClick={handleFileButtonClick}
						>
							<IconPaperclip height="24" width="24" />
						</button>
					)}
					<button
						type="button"
						disabled={isSubmitDisabled}
						className="chat-input-send-button"
						data-test-id="chat-send-button"
						aria-label="Send message"
						onClick={handleSubmit}
					>
						<IconSend height="24" width="24" />
					</button>
				</div>
			</div>

			{files.length > 0 && (!isSubmitting || waitingForChatResponse) && (
				<div className="chat-files">
					{files.map((file) => (
						<ChatFile
							key={file.name}
							file={file}
							isRemovable
							isPreviewable
							onRemove={handleFileRemove}
						/>
					))}
				</div>
			)}

			<input
				ref={fileInputRef}
				type="file"
				style={{ display: "none" }}
				multiple
				accept={allowedFileTypes}
				onChange={handleFileInputChange}
			/>

			{complianceNotice !== "" && (
				<p className="chat-input-compliance-notice">
					{complianceNotice}
				</p>
			)}
		</div>
	);
}
