import { GRIDDO_ID } from "@ax/constants";
import type { HeadingFilter, HeadingLevel, HeadingNode } from "@ax/types";

const MAX_HEADING_LENGTH = 70;

const headColorsRGB: Record<string, { r: number; g: number; b: number }> = {
	h1: { r: 255, g: 240, b: 109 },
	h2: { r: 255, g: 184, b: 248 },
	h3: { r: 115, g: 248, b: 200 },
	h4: { r: 155, g: 237, b: 255 },
	h5: { r: 198, g: 193, b: 255 },
	h6: { r: 255, g: 206, b: 149 },
};

const getHeadColor = (tag: string, opacity?: number): string => {
	const color = headColorsRGB[tag];
	if (!color) return "rgb(255, 255, 255)";
	if (opacity !== undefined) {
		return `rgba(${color.r}, ${color.g}, ${color.b}, ${opacity})`;
	}
	return `rgb(${color.r}, ${color.g}, ${color.b})`;
};

const flattenHeadings = (headings: HeadingNode[], counter = { value: 0 }): IFlatHeading[] => {
	const result: IFlatHeading[] = [];

	for (const heading of headings) {
		counter.value += 1;
		result.push({
			id: counter.value,
			level: heading.level,
			tag: heading.tag,
			text: heading.text,
			isHidden: heading.isHidden,
		});

		if (heading.children.length > 0) {
			result.push(...flattenHeadings(heading.children, counter));
		}
	}

	return result;
};

const analyzeHeadings = (headings: HeadingNode[], isFiltering = false): IHeadingError[] => {
	const errors: IHeadingError[] = [];
	const flatHeadings = flattenHeadings(headings);

	if (flatHeadings.length === 0) {
		return errors;
	}

	// 1. Check for missing H1
	const hasH1 = flatHeadings.some((h) => h.tag === "h1");
	if (!isFiltering && !hasH1) {
		errors.push({
			message: "No H1 in this page",
			description: (
				<>
					Add a H1 tag to improve page
					<br />
					structure and SEO visibility.
				</>
			),
			headingIds: [],
		});
	}

	// 2. Check for incorrect nesting (skipped levels)
	const nestingErrorIds: number[] = [];
	let lastValidLevel = flatHeadings[0].level;
	for (let i = 1; i < flatHeadings.length; i++) {
		const currLevel = flatHeadings[i].level;

		if (currLevel > lastValidLevel + 1) {
			// Heading skips levels - mark as error
			nestingErrorIds.push(flatHeadings[i].id);
		} else {
			// Correct nesting - update last valid level
			lastValidLevel = currLevel;
		}
	}
	if (nestingErrorIds.length > 0) {
		errors.push({
			message: "Check heading structure for SEO compliance",
			description: (
				<>
					Ensure each heading is properly nested
					<br />
					and follows SEO best practices.
				</>
			),
			headingIds: nestingErrorIds,
		});
	}

	// 3. Check for duplicate texts
	const textMap = new Map<string, number[]>();
	const visibleHeadings = flatHeadings.filter((h) => !h.isHidden);
	for (const heading of visibleHeadings) {
		const normalizedText = heading.text.toLowerCase().trim();
		if (normalizedText) {
			const ids = textMap.get(normalizedText) || [];
			ids.push(heading.id);
			textMap.set(normalizedText, ids);
		}
	}
	const duplicateIds: number[] = [];
	for (const ids of textMap.values()) {
		if (ids.length > 1) {
			duplicateIds.push(...ids);
		}
	}
	if (duplicateIds.length > 0) {
		errors.push({
			message: "Avoid header repetitions for SEO",
			description: (
				<>
					Diversify your headings to enhance SEO.
					<br />
					Avoid using the same heading multiple times.
				</>
			),
			headingIds: duplicateIds,
		});
	}

	// 4. Check for excessive length
	const excessiveLengthIds: number[] = [];
	for (const heading of flatHeadings) {
		if (heading.text.length > MAX_HEADING_LENGTH) {
			excessiveLengthIds.push(heading.id);
		}
	}
	if (excessiveLengthIds.length > 0) {
		errors.push({
			message: "Some headings are too long",
			description: (
				<>
					Optimize SEO by shortening
					<br />
					headings to ensure clarity.
				</>
			),
			headingIds: excessiveLengthIds,
		});
	}

	return errors;
};

const parseHeadingsTree = (html: HTMLDivElement): HeadingNode[] => {
	const frameObject = html.querySelector<HTMLIFrameElement>(".frame-content");
	const frameContent = frameObject?.contentWindow?.document.getElementById(GRIDDO_ID) as HTMLElement;

	if (!frameContent) {
		return [];
	}

	const iframeWindow = frameObject?.contentWindow;
	const headings = Array.from(frameContent.querySelectorAll("h1, h2, h3, h4, h5, h6")) as HTMLHeadingElement[];

	const root: HeadingNode[] = [];
	const stack: HeadingNode[] = [];

	const isElementHidden = (el: HTMLElement): boolean => {
		if (!iframeWindow) return false;

		const style = iframeWindow.getComputedStyle(el);

		// 1. Check CSS properties directly on element (only definite cases)
		if (style.display === "none" || style.visibility === "hidden") {
			return true;
		}

		// 2. Check for hidden attribute
		if (el.hidden) {
			return true;
		}

		// 3. Check dimensions (width or height is 0)
		const rect = el.getBoundingClientRect();
		if (rect.width === 0 && rect.height === 0) {
			return true;
		}

		return false;
	};

	const isAncestorHidden = (el: HTMLElement): boolean => {
		let node: HTMLElement | null = el.parentElement;
		while (node && node !== frameContent) {
			if (iframeWindow) {
				const style = iframeWindow.getComputedStyle(node);

				// Check for display:none or visibility:hidden in ancestor
				if (style.display === "none" || style.visibility === "hidden") {
					return true;
				}

				// Check for opacity:0 with animation check
				if (parseFloat(style.opacity) === 0) {
					const hasAnimation = style.animationName !== "none";
					const transitionProps = style.transitionProperty.split(",").map((p) => p.trim());
					const hasOpacityTransition = transitionProps.includes("opacity") || transitionProps.includes("all");
					if (!hasAnimation && !hasOpacityTransition) return true;

					// If opacity is 0 and element is outside document flow or can't receive events,
					// it's effectively hidden regardless of transition state
					if (style.pointerEvents === "none" || (node as HTMLElement).offsetParent === null) {
						return true;
					}
				}

				// Check if ancestor is in collapsed navigation (only when explicitly collapsed)
				const navSelector = "nav, header, [role='navigation'], .nav, .menu, .navbar, .sidebar";
				if (node.matches(navSelector)) {
					const nodeHeight = parseFloat(style.height);
					const nodeMaxHeight = parseFloat(style.maxHeight);

					// Only mark as hidden if height is explicitly 0
					if ((nodeHeight === 0 || nodeMaxHeight === 0) && style.overflow === "hidden") {
						return true;
					}
				}
			}
			node = node.parentElement;
		}
		return false;
	};

	for (const heading of headings) {
		const level = Number(heading.tagName[1]) as HeadingLevel;
		const isFixedPosition = iframeWindow?.getComputedStyle(heading).position === "fixed";
		const isHidden =
			isElementHidden(heading) || (heading.offsetParent === null && !isFixedPosition) || isAncestorHidden(heading);

		const node: HeadingNode = {
			level,
			tag: heading.tagName.toLowerCase() as HeadingNode["tag"],
			text: heading.textContent?.trim() ?? "",
			isHidden: !!isHidden,
			children: [],
		};

		// Subir en la jerarquía si el nivel actual es menor o igual
		while (stack.length > 0 && stack[stack.length - 1].level >= level) {
			stack.pop();
		}

		if (stack.length === 0) {
			root.push(node);
		} else {
			stack[stack.length - 1].children.push(node);
		}

		stack.push(node);
	}

	return root;
};

const extractUniqueHeadingTypes = (headings: HeadingNode[]): HeadingFilter[] => {
	const types = new Set<HeadingFilter>();

	const traverse = (nodes: HeadingNode[]) => {
		for (const node of nodes) {
			types.add(node.tag);
			if (node.children && node.children.length > 0) {
				traverse(node.children);
			}
		}
	};

	traverse(headings);
	return Array.from(types).sort();
};

const filterHeadings = (headings: HeadingNode[], selectedType: string, includeHidden = true): HeadingNode[] => {
	if (selectedType === "all" && includeHidden) return headings;

	if (selectedType === "all") {
		// Filter by visibility but keep hierarchy
		const filterNode = (node: HeadingNode): HeadingNode | null => {
			const isVisible = !node.isHidden;
			const filteredChildren = node.children.map(filterNode).filter((n): n is HeadingNode => n !== null);

			if (isVisible) {
				return { ...node, children: filteredChildren };
			}

			return filteredChildren.length > 0 ? { ...node, children: filteredChildren } : null;
		};

		return headings.map(filterNode).filter((n): n is HeadingNode => n !== null);
	}

	// For specific types, flatten the result
	return flattenHeadings(headings)
		.filter((h) => h.tag === selectedType)
		.filter((h) => includeHidden || !h.isHidden)
		.map((h) => ({
			level: h.level,
			tag: h.tag,
			text: h.text,
			isHidden: h.isHidden,
			children: [],
		}));
};

const hasHiddenHeadings = (headings: HeadingNode[], filterType = "all"): boolean => {
	const check = (node: HeadingNode): boolean => {
		const matchesType = filterType === "all" || node.tag === filterType;
		if (matchesType && node.isHidden) return true;
		return node.children.length > 0 && node.children.some(check);
	};

	return headings.some(check);
};

interface IHeadingError {
	message: string;
	description: React.ReactNode;
	headingIds: number[];
}

interface IFlatHeading {
	id: number;
	level: HeadingLevel;
	tag: HeadingNode["tag"];
	text: string;
	isHidden: boolean;
}

export { parseHeadingsTree, extractUniqueHeadingTypes, filterHeadings, analyzeHeadings, getHeadColor, hasHiddenHeadings };
export type { IHeadingError };
