import { memo, useCallback, useEffect, useRef, useState } from "react";

import { cleanPageValues } from "@ax/forms";
import { deepClone, isEmptyObj } from "@ax/helpers";

import isEqual from "lodash.isequal";

const DEBUG_DIRTY = false;

const useDebounce = (value: any) => {
	// State and setters for debounced value
	const [debouncedValue, setDebouncedValue] = useState(value);

	useEffect(() => {
		// Set debouncedValue to value (passed in) after the specified delay
		const handler = setTimeout(() => {
			setDebouncedValue(value);
		}, 500);

		return () => {
			clearTimeout(handler);
		};
	}, [value]);

	return debouncedValue;
};

const useDebouncedCallback = <T extends unknown[]>(callback: (...args: T) => void, delay: number) => {
	const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
	const callbackRef = useRef(callback);

	useEffect(() => {
		callbackRef.current = callback;
	}, [callback]);

	return useCallback(
		(...args: T) => {
			if (delay === 0) {
				callbackRef.current(...args);
				return;
			}
			clearTimeout(timeoutRef.current);
			timeoutRef.current = setTimeout(() => callbackRef.current(...args), delay);
		},
		[delay],
	);
};

const useEqualStructured = (component: any) => {
	return memo(component, (prevProps: any, newProps: any) => {
		const { fieldKey } = prevProps;
		const prevValue = prevProps.form.content?.[fieldKey];
		const newValue = newProps.form.content[fieldKey];

		return prevValue === newValue;
	});
};

/**
 * Stores the provided value (object or string) and returns the previous value,
 * updating it only when a "saved" state is detected (either from the external state
 * manager, or explicitly via options). Useful for detecting changes and implementing dirty checks.
 *
 * @param value - The current value to track.
 * @param options - Optional configuration object.
 * @param options.isSaved - Flag to indicate if the value has been saved/reset externally.
 * @param options.force - Force update of the stored value on next render.
 * @returns The previously saved value, or undefined for first render.
 */
const usePrevious = (
	value: Record<string, unknown> | string,
	options?: {
		isSaved?: boolean;
		force?: boolean;
	},
) => {
	const { isSaved, force } = options || {};
	const valueStr = value && JSON.stringify(value);
	const ref = useRef<Record<string, unknown> | string | undefined>(undefined);

	useEffect(() => {
		const currentValue = valueStr && JSON.parse(valueStr);
		if (!ref.current || isEmptyObj(ref.current) || force) {
			ref.current = currentValue;
		}
	}, [valueStr, force]);

	// biome-ignore lint/correctness/useExhaustiveDependencies: TODO fix this
	useEffect(() => {
		if (isSaved) {
			ref.current = valueStr && JSON.parse(valueStr);
		}
	}, [isSaved]);

	return ref.current;
};

const getChangedKeys = (
	prev: Record<string, unknown>,
	next: Record<string, unknown>,
): Array<{ key: string; prev: unknown; next: unknown }> => {
	const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)]);
	const diffs: Array<{ key: string; prev: unknown; next: unknown }> = [];
	for (const key of allKeys) {
		if (!isEqual(prev[key], next[key])) {
			diffs.push({ key, prev: prev[key], next: next[key] });
		}
	}
	return diffs;
};

const useIsDirty = (
	updatedValues: any,
	isNew?: boolean,
	debug = DEBUG_DIRTY,
): { isDirty: boolean; setIsDirty(state: boolean): void; resetDirty: any } => {
	const [isDirty, setIsDirty] = useState(false);
	const [isSaved, setIsSaved] = useState(false);
	const [isResetting, setIsResetting] = useState(false);
	const [force, setForce] = useState(false);

	const prevContent = usePrevious(updatedValues, { isSaved, force });

	useEffect(() => {
		if (force) setForce(false);
	}, [force]);

	const hasChanged = useCallback((): boolean => {
		if (prevContent && updatedValues) {
			const originalValuesCloned = prevContent && deepClone(prevContent);
			const updatedValuesCloned = updatedValues && deepClone(updatedValues);

			const { cleanUpdatedValues, cleanOriginalValues } = cleanPageValues(updatedValuesCloned, originalValuesCloned);

			const hasChanged = !isEqual(cleanUpdatedValues, cleanOriginalValues);

			if (debug) {
				console.groupCollapsed(
					`[useIsDirty] hasChanged: %c${hasChanged}`,
					hasChanged ? "color: red; font-weight: bold" : "color: green",
				);
				console.log("Snapshot (cleaned):", cleanOriginalValues);
				console.log("Current  (cleaned):", cleanUpdatedValues);
				const diffs = getChangedKeys(
					cleanOriginalValues as Record<string, unknown>,
					cleanUpdatedValues as Record<string, unknown>,
				);
				if (diffs.length > 0) {
					console.table(diffs);
				}
				console.groupEnd();
			}

			return hasChanged;
		}
		return false;
	}, [prevContent, updatedValues, debug]);

	const resetDirty = useCallback(
		(reseting = true, forceUpdate = false) => {
			if (debug) {
				console.log(`[useIsDirty] resetDirty(resetting=${reseting}, force=${forceUpdate})`);
			}
			setIsDirty(false);
			setIsSaved(true);
			reseting && setIsResetting(true);
			setForce(forceUpdate);
		},
		[debug],
	);

	// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
	useEffect(() => {
		if (isResetting) {
			setIsResetting(false);
			return;
		}

		if (isNew) {
			if (debug) console.log("[useIsDirty] isNew=true → isDirty=false");
			setIsDirty(false);
		} else if (prevContent && updatedValues) {
			const isUpdated = hasChanged();

			if (isUpdated) {
				if (debug) console.log("[useIsDirty] dirty=true");
				setIsDirty(true);
				setIsSaved(false);
			} else {
				if (debug) console.log("[useIsDirty] dirty=false (no changes)");
				setIsDirty(false);
			}
		}
	}, [updatedValues]);

	return { isDirty, setIsDirty, resetDirty };
};

const useShouldBeSaved = (form: Record<string, any> | null | undefined, debug = DEBUG_DIRTY) => {
	const [isDirty, setIsDirtyState] = useState(false);
	const originalRef = useRef<Record<string, any>>();

	// Intercept setIsDirty to update baseline when saved
	const setIsDirty = useCallback(
		(value: boolean) => {
			setIsDirtyState(value);
			if (value === false && form) {
				originalRef.current = deepClone(form);
			}
		},
		[form],
	);

	useEffect(() => {
		if (!form) {
			if (debug) console.log("[useShouldBeSaved] form=null → isDirty=false");
			setIsDirtyState(false);
			return;
		}

		// Initialize baseline on first render
		if (!originalRef.current) {
			originalRef.current = deepClone(form);
			return;
		}

		// Compare against the baseline (original)
		const original = { ...originalRef.current } as Record<string, unknown>;
		const current = { ...form } as Record<string, unknown>;
		delete original.modified;
		delete current.modified;

		const hasChanges = !isEqual(original, current);

		if (debug) {
			console.groupCollapsed(
				`[useShouldBeSaved] isDirty: %c${hasChanges}`,
				hasChanges ? "color: red; font-weight: bold" : "color: green",
			);
			console.log("Original (cleaned):", original);
			console.log("Current  (cleaned):", current);
			const diffs = getChangedKeys(original as Record<string, unknown>, current as Record<string, unknown>);
			if (diffs.length > 0) {
				console.table(diffs);
			}
			console.groupEnd();
		}

		setIsDirtyState(hasChanges);
	}, [form, debug]);

	return { isDirty, setIsDirty };
};

export { useDebounce, useDebouncedCallback, useEqualStructured, usePrevious, useIsDirty, useShouldBeSaved };
