import {
	ArrowBackOutlined,
	CancelOutlined,
	DeleteOutline,
	EditOutlined,
	SaveOutlined,
} from "@mui/icons-material";
import {
	CircularProgress,
	Stack,
	Checkbox,
	FormControlLabel,
} from "@mui/material";
import { AxiosError, HttpStatusCode } from "axios";
import { FormikProvider } from "formik";
import { useEffect, useState, JSX, ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { z } from "zod";

import { useSignalR } from "@/contexts/signalR/useSignalR";
import {
	CREATED_NOTIFICATION,
	DELETED_NOTIFICATION,
	UPDATED_NOTIFICATION,
} from "@/enums/notifications.enum";
import { useFormikWithZod } from "@/utils/useFormikWithZod";

import { http } from "../../../services/http.service";
import { useCard } from "../../hooks/use-card-params.hook";
import { CardAction } from "../../models/card-action";
import { Entity } from "../../models/Entity";
import { ButtonWithIcon } from "../ButtonWithIcon";
import { Centroid } from "../Centroid";
import { Confirmation } from "../Confirmation";

import { MultiSelectCardField } from "./card-fields/MultiSelectCardField";
import { TextCardField } from "./card-fields/TextCardField";
import { CardField } from "./CardField";

export const SELECT_TYPE = "select";
export const MULTISELECT_TYPE = "multiselect";
export const INPUT_TYPE = "input";

export interface ErrorContext {
	status: HttpStatusCode;
	errors: {
		code: string;
		message: string;
	}[];
}

interface Props<T extends Entity> {
	defaultValue: T;
	baseUrl: string;

	/** Поля:  */
	fields: CardField<T>[];
	notificationMethodPrefix: string;
	getModelTitle: (model: T) => string;
	validationSchema?: unknown;
	canEdit?: boolean;
	getCanEdit?: (model: T) => boolean;
	canRemove?: boolean;
	getCanRemove?: (model: T) => boolean;
	extraTitle?: (model: T) => JSX.Element | null;
	viewTitle?: string;
	editTitle?: string;
	createTitle?: string;
	extraActions?: CardAction<T>[];
	getExtraActions?: (model: T) => CardAction<T>[];
	filters?: Record<string, unknown>;
	children?: ReactNode;
	onError?: (response: ErrorContext) => void;
	onLoaded?: (model: T) => T;
	onSubmit?: (model: T) => Promise<T>;

	onDeleteConfirmationMessage?: string;
	extraFields?: (model: T, isEdit: boolean) => ReactNode;
}

export function Card<T extends Entity>(props: Props<T>) {
	const { isNew: isCardNew, modelId } = useCard();
	const [isNew, setIsNew] = useState(isCardNew);
	const [isEdit, setIsEdit] = useState(isNew);
	const [isLoading, setIsLoading] = useState(false);
	const navigate = useNavigate();
	const [initialValues, setInitialValues] = useState(props.defaultValue);
	const { subscribe, unsubscribe } = useSignalR();
	const { t } = useTranslation();
	const [removeConfirmOpen, setRemoveConfirmOpen] = useState(false);

	useEffect(() => {
		subscribe(
			`${props.notificationMethodPrefix}.${CREATED_NOTIFICATION}`,
			onCreated,
		);
		subscribe(
			`${props.notificationMethodPrefix}.${UPDATED_NOTIFICATION}`,
			onUpdated,
		);
		subscribe(
			`${props.notificationMethodPrefix}.${DELETED_NOTIFICATION}`,
			onDeleted,
		);

		return () => {
			unsubscribe(
				`${props.notificationMethodPrefix}.${CREATED_NOTIFICATION}`,
				onCreated,
			);
			unsubscribe(
				`${props.notificationMethodPrefix}.${UPDATED_NOTIFICATION}`,
				onUpdated,
			);
			unsubscribe(
				`${props.notificationMethodPrefix}.${DELETED_NOTIFICATION}`,
				onDeleted,
			);
		};
	}, []);

	const onCreated = (x: T) => {
		toast.success(
			t("{{title}} is created", { title: props.getModelTitle(x) }),
		);
	};
	const onUpdated = (x: T) => {
		if (x.id === modelId) {
			toast.success(
				t("{{title}} is updated", { title: props.getModelTitle(x) }),
			);

			const message = t("objectUpdatedByAnotherUser");
			if (window.confirm(message)) {
				initialize();
			}
		}
	};
	const onDeleted = (x: T) => {
		if (x.id === modelId) {
			toast.success(
				t("{{title}} is removed", { title: props.getModelTitle(x) }),
			);

			back();
		}
	};

	const formik = useFormikWithZod({
		initialValues,
		validationSchema: props.validationSchema as z.ZodSchema<
			typeof initialValues
		>,
		onSubmit: async (model) => {
			if (!isEdit) {
				return;
			}

			makeNulls(model);
			makeNumbers(model);

			const modelWithFilters = {
				...model,
				...props.filters,
			};

			try {
				if (isNew) {
					await create(modelWithFilters);
				} else {
					await update(modelWithFilters);
				}
				setIsEdit(false);
			} catch (error) {
				const response = (error as AxiosError).response;
				if (response && props.onError) {
					props.onError({
						status: response.status,
						errors: response.data as ErrorContext["errors"],
					});
				}
			}
		},
	});

	const {
		setValues,
		resetForm,
		values,
		handleSubmit,
		errors,
		touched,
		handleChange,
		handleBlur,
		handleReset,
		isValid,
		isSubmitting,
		setFieldValue,
	} = formik;

	const makeNulls = (model: T) => {
		for (const field of props.fields) {
			if (
				field.nullable &&
				(model as Record<keyof T, unknown>)[field.name] === ""
			) {
				(model as Record<keyof T, unknown>)[field.name] = null;
			}
		}
	};

	const makeNumbers = (model: T) => {
		for (const field of props.fields) {
			if (field.type === "number") {
				if (
					field.nullable &&
					!(model as Record<keyof T, unknown>)[field.name]
				) {
					(model as Record<keyof T, unknown>)[field.name] = null;
				} else {
					(model as Record<keyof T, unknown>)[field.name] = Number(
						(model as Record<keyof T, unknown>)[field.name],
					);
				}
			}
		}
	};

	useEffect(() => {
		initialize();
	}, [modelId]);

	useEffect(() => {
		setValues(initialValues);
	}, [initialValues, setValues]);

	const initialize = async () => {
		if (modelId) {
			await fetchModel(modelId);
		}
	};

	const fetchModel = async (id: string) => {
		try {
			setIsLoading(true);

			const model = await http.get<T>(`${props.baseUrl}/${id}`);

			if (!model) {
				throw new Error(
					t(
						"onFetch must be returned not undefined model or threw an exception",
					),
				);
			}

			const finalModel = props.onLoaded ? props.onLoaded(model) : model;

			setValues(finalModel);
			setInitialValues(finalModel);
		} finally {
			setIsLoading(false);
		}
	};

	if (isLoading) {
		return (
			<Centroid>
				<CircularProgress />
			</Centroid>
		);
	}

	function clone<T>(x: T): T {
		return JSON.parse(JSON.stringify(x));
	}

	const create = async (model: T) => {
		const modelClone = clone(model);
		const submitData = props.onSubmit
			? await props.onSubmit(modelClone)
			: modelClone;
		const response = await http.post<T>(props.baseUrl, submitData);

		setIsNew(false);

		navigate(`../${response.id}`, { relative: "path" });
	};

	const update = async (model: T) => {
		const modelClone = clone(model);
		const submitData = props.onSubmit
			? await props.onSubmit(modelClone)
			: modelClone;
		await http.put(`${props.baseUrl}/${model.id}`, submitData);
		setInitialValues(model);
	};

	const handleRemove = async () => {
		try {
			await http.remove(`${props.baseUrl}/${values.id}`);

			back();
		} catch (e: unknown) {
			console.error((e as Error).message);
		}
	};

	const cancel = () => {
		if (isNew) {
			navigate(-1);
		} else {
			setIsEdit(false);
			resetForm({ values: initialValues });
		}
	};

	const back = () => {
		navigate("..", {
			relative: "path",
		});
	};

	const edit = () => {
		setIsEdit(true);
	};

	const groupFields = (fields: CardField<T>[]) => {
		const groups = new Map<string, CardField<T>[]>();
		const ungrouped: CardField<T>[] = [];

		fields.forEach((field) => {
			if (field.group) {
				if (!groups.has(field.group)) {
					groups.set(field.group, []);
				}
				groups.get(field.group)!.push(field);
			} else {
				ungrouped.push(field);
			}
		});

		return { groups, ungrouped };
	};

	const renderComponent = (field: CardField<T>): ReactNode => {
		const props = {
			field,
			key: field.name,
			values,
			errors,
			touched,
			setFieldValue,
			handleBlur,
			handleChange,
			handleReset,
			isEdit,
		};
		return field.component ? (
			<field.component {...props} key={field.name} />
		) : null;
	};

	const renderField = (field: CardField<T>, inGroup = false): ReactNode => {
		if (isEdit && field.hideFromEditor) return null;

		const fieldClassName = field.required
			? "border-l-4 border-l-blue-500 pl-3 bg-blue-50 rounded"
			: "";

		const fieldContent = field.component ? (
			renderComponent(field)
		) : field.type === "multiselect" ? (
			<MultiSelectCardField
				key={field.name.toString()}
				{...field}
				formik={formik}
				readOnly={!isEdit || field.readOnly}
				nullable={field.nullable}
			/>
		) : field.type === "checkbox" ? (
			<FormControlLabel
				control={
					<Checkbox
						checked={Boolean(values[field.name])}
						onChange={handleChange}
						name={field.name.toString()}
						disabled={!isEdit || field.readOnly}
					/>
				}
				label={t(field.title)}
			/>
		) : (
			<TextCardField
				key={field.name.toString()}
				{...field}
				formik={formik}
				readOnly={!isEdit || field.readOnly}
				nullable={field.nullable}
			/>
		);

		return (
			<div
				key={field.name.toString()}
				className={`${fieldClassName} ${inGroup ? "mb-3" : ""}`}
			>
				{fieldContent}
			</div>
		);
	};

	return (
		<FormikProvider value={formik}>
			<Stack spacing={3} padding={3}>
				{isEdit ? (
					isNew ? (
						<h3>{t(props.createTitle ?? "Create model")}</h3>
					) : (
						<h3>{t(props.editTitle ?? "Edit model")}</h3>
					)
				) : (
					<h3>{t(props.viewTitle ?? "View model")}</h3>
				)}

				{/* TODO: Может завернуть в див? */}
				{/* {props.extraTitle && props.extraTitle(values)} */}

				{!isEdit && (
					<div className="flex flex-wrap gap-3 items-center">
						<ButtonWithIcon
							color="secondary"
							disabled={isSubmitting}
							onClick={back}
							icon={<ArrowBackOutlined />}
							title={t("Back")}
						/>
						{(props.canEdit ||
							(props.getCanEdit && props.getCanEdit(values))) && (
							<ButtonWithIcon
								color="primary"
								disabled={isSubmitting}
								onClick={edit}
								icon={<EditOutlined />}
								title={t("Edit")}
							/>
						)}
						{(props.canRemove ||
							(props.getCanRemove &&
								props.getCanRemove(values))) && (
							<>
								<ButtonWithIcon
									color="error"
									disabled={isSubmitting}
									onClick={() => setRemoveConfirmOpen(true)}
									icon={<DeleteOutline />}
									title={t("Remove")}
								/>
								<Confirmation
									open={removeConfirmOpen}
									onConfirm={handleRemove}
									text={t(
										props.onDeleteConfirmationMessage ??
											"Are you sure you want to delete this model?",
									)}
								/>
							</>
						)}

						{props.extraActions &&
							!isEdit &&
							props.extraActions.map((x, index) => (
								<ButtonWithIcon
									key={index}
									disabled={isSubmitting}
									onClick={async () => {
										const model = await x.action(values);
										setValues(model);
									}}
									{...x}
								/>
							))}

						{props.getExtraActions &&
							!isEdit &&
							props.getExtraActions(values).map((x, index) => (
								<ButtonWithIcon
									key={index}
									disabled={isSubmitting}
									onClick={async () => {
										const model = await x.action(values);
										setValues(model);
									}}
									{...x}
								/>
							))}
					</div>
				)}

				<form onSubmit={handleSubmit} className="flex flex-col gap-3">
					{isEdit && (
						<div className="flex gap-3">
							<ButtonWithIcon
								color="secondary"
								disabled={isSubmitting}
								onClick={cancel}
								icon={<CancelOutlined />}
								title={t("Cancel")}
							/>
							<ButtonWithIcon
								type="submit"
								color="primary"
								disabled={!isValid || isSubmitting}
								icon={
									<>
										{isSubmitting && (
											<CircularProgress size="small" />
										)}
										<SaveOutlined />
									</>
								}
								title={t("Save")}
							/>
						</div>
					)}

					{(() => {
						const { groups, ungrouped } = groupFields(props.fields);
						const result: ReactNode[] = [];

						if (ungrouped.length > 0) {
							ungrouped.forEach((field) => {
								result.push(renderField(field, false));
							});
						}

						groups.forEach((groupFields, groupName) => {
							result.push(
								<div
									key={groupName}
									className="border rounded-lg p-4 bg-gray-50 mt-4"
								>
									<h4 className="text-base font-semibold text-gray-800 border-b border-gray-300 pb-2 mb-4">
										{t(groupName)}
									</h4>
									<div className="space-y-3">
										{groupFields.map((field) =>
											renderField(field, true),
										)}
									</div>
								</div>,
							);
						});

						return result;
					})()}

					{props.children}

					{props.extraFields &&
						props.extraFields(formik.values, isEdit)}
				</form>
			</Stack>
		</FormikProvider>
	);
}
