import type { CssLocation, CssNode } from "css-tree";
import type { SemanticCssCompilerDiagnosticCode } from "./diagnostics";
import type {
	CompiledDeclaration,
	CompiledMediaQuery,
	CompiledStyleRule,
	MediaFeature,
	ParsedStylesheet,
	SemanticCssDiagnostic,
	SourceRange,
	StyleProgram,
} from "./types";
import * as csstree from "css-tree";
import { createDiagnostic, EMPTY_SOURCE_RANGE } from "./diagnostics";
import { SEMANTIC_CSS_LIMITS_V1 } from "./limits";
import {
	PROPERTY_REGISTRY_V1,
	SEMANTIC_CSS_BORDER_STYLE_VALUES_V1,
	SEMANTIC_CSS_CSS_WIDE_KEYWORDS_V1,
	SEMANTIC_CSS_LENGTH_PROPERTIES_V1,
	SEMANTIC_CSS_LENGTH_UNITS_V1,
	SEMANTIC_CSS_LENGTH_VALUE_KEYWORDS_V1,
} from "./registry/properties";
import { compileSelector } from "./selector";

export type CompileProgramResult = {
	program: StyleProgram | null;
	diagnostics: readonly SemanticCssDiagnostic[];
};

type AstNode = CssNode & {
	block?: AstNode | null;
	children?: Iterable<AstNode>;
	important?: boolean | string;
	name?: string;
	prelude?: AstNode | null;
	property?: string;
	value?: AstNode | string;
};

const absoluteUnitToPt = {
	pt: 1,
	px: 72 / 96,
	in: 72,
	mm: 72 / 25.4,
	cm: 72 / 2.54,
} as const;

const spacingShorthands = new Set(["margin", "padding"]);
const sides = ["top", "right", "bottom", "left"] as const;
const borderStyles = new Set<string>(SEMANTIC_CSS_BORDER_STYLE_VALUES_V1);
const cssWideKeywords = new Set<string>(SEMANTIC_CSS_CSS_WIDE_KEYWORDS_V1);
const lengthValueKeywords = new Set<string>(SEMANTIC_CSS_LENGTH_VALUE_KEYWORDS_V1);
const maxMediaQueryBranches = SEMANTIC_CSS_LIMITS_V1.maxRules;
const lengthUnitPattern = SEMANTIC_CSS_LENGTH_UNITS_V1.map((unit) => (unit === "%" ? "%" : unit)).join("|");
const borderWidthPattern = new RegExp(
	`^(?:thin|medium|thick|[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?(?:${lengthUnitPattern})?)$`,
	"i",
);

function isCssWideKeyword(value: string): boolean {
	return cssWideKeywords.has(value.toLowerCase());
}

function isRegisteredPropertyValue(property: string, value: string): boolean {
	return (
		PROPERTY_REGISTRY_V1[property]?.values.some((candidate) => candidate.toLowerCase() === value.toLowerCase()) ?? false
	);
}

function isRegisteredShorthandComponent(property: string, value: string): boolean {
	return !isCssWideKeyword(value) && isRegisteredPropertyValue(property, value);
}

export const SEMANTIC_CSS_LENGTH_PROPERTIES = new Set<string>(SEMANTIC_CSS_LENGTH_PROPERTIES_V1);

function range(location: CssLocation | null | undefined): SourceRange {
	return location ? { start: { ...location.start }, end: { ...location.end } } : EMPTY_SOURCE_RANGE;
}

export function decodeCssEscapes(value: string): string {
	return value.replaceAll(
		/\\([0-9a-f]{1,6})[ \t\r\n\f]?|\\(.)/gi,
		(_match, hex: string | undefined, escaped: string | undefined) => {
			const codePoint = hex ? Number.parseInt(hex, 16) : 0;
			return hex ? String.fromCodePoint(codePoint === 0 || codePoint > 0x10ffff ? 0xfffd : codePoint) : (escaped ?? "");
		},
	);
}

export function cssFunctionDepth(value: string): number {
	const stack: boolean[] = [];
	let currentDepth = 0;
	let maximumDepth = 0;
	let identifier = "";
	let quote = "";

	for (let index = 0; index < value.length; index++) {
		const character = value[index] ?? "";
		if (quote) {
			if (character === quote && value[index - 1] !== "\\") quote = "";
			continue;
		}
		if (character === '"' || character === "'") {
			quote = character;
			identifier = "";
			continue;
		}
		if (character === "/" && value[index + 1] === "*") {
			const end = value.indexOf("*/", index + 2);
			index = end < 0 ? value.length : end + 1;
			continue;
		}
		if (/[-_a-zA-Z0-9\\]/.test(character)) {
			identifier += character;
			continue;
		}
		if (character === "(") {
			const isFunction = identifier.length > 0;
			stack.push(isFunction);
			if (isFunction) maximumDepth = Math.max(maximumDepth, ++currentDepth);
			identifier = "";
			continue;
		}
		if (character === ")") {
			if (stack.pop()) currentDepth--;
		}
		identifier = "";
	}

	return maximumDepth;
}

function identifier(value: string): string {
	return decodeCssEscapes(value);
}

function children(node: AstNode | null | undefined): AstNode[] {
	return node?.children ? [...node.children] : [];
}

function diagnostic(
	diagnostics: SemanticCssDiagnostic[],
	code: SemanticCssCompilerDiagnosticCode,
	message: string,
	node?: AstNode,
	severity: SemanticCssDiagnostic["severity"] = "error",
): void {
	diagnostics.push(createDiagnostic(code, severity, message, range(node?.loc)));
}

function splitValue(value: string): string[] {
	const parts: string[] = [];
	let start = 0;
	let depth = 0;
	let quote = "";
	for (let index = 0; index < value.length; index++) {
		const character = value[index] ?? "";
		if (quote) {
			if (character === quote && value[index - 1] !== "\\") quote = "";
			continue;
		}
		if (character === '"' || character === "'") {
			quote = character;
			continue;
		}
		if (character === "(") depth++;
		if (character === ")") depth--;
		if (/\s/.test(character) && depth === 0) {
			if (start < index) parts.push(value.slice(start, index));
			start = index + 1;
		}
	}
	if (start < value.length) parts.push(value.slice(start));
	return parts.filter(Boolean);
}

function fourSides(property: string, values: readonly string[]): readonly [property: string, value: string][] | null {
	if (values.length < 1 || values.length > 4) return null;
	const [top, right = top, bottom = top, left = right] =
		values.length === 3
			? [values[0], values[1], values[2], values[1]]
			: values.length === 2
				? [values[0], values[1], values[0], values[1]]
				: values;
	return sides.map((side, index) => [`${property}-${side}`, [top, right, bottom, left][index] as string]);
}

function borderComponents(value: string): readonly [component: string, value: string][] | null {
	if (isCssWideKeyword(value)) {
		return [
			["width", value],
			["style", value],
			["color", value],
		];
	}
	const values = splitValue(value);
	let width = "medium";
	let style = "none";
	let color = "currentcolor";
	for (const part of values) {
		if (borderWidthPattern.test(part)) {
			if (width !== "medium") return null;
			width = part;
		} else if (borderStyles.has(part.toLowerCase())) {
			if (style !== "none") return null;
			style = part;
		} else if (color === "currentcolor") color = part;
		else return null;
	}
	return [
		["width", width],
		["style", style],
		["color", color],
	];
}

export function expandShorthand(property: string, value: string): readonly [property: string, value: string][] | null {
	const valueParts = splitValue(value);
	if (PROPERTY_REGISTRY_V1[property] && valueParts.length > 1 && valueParts.some(isCssWideKeyword)) return null;

	if (!spacingShorthands.has(property)) {
		if (property.endsWith("-horizontal")) {
			const prefix = property.slice(0, -"-horizontal".length);
			return [
				[`${prefix}-left`, value],
				[`${prefix}-right`, value],
			];
		}
		if (property.endsWith("-vertical")) {
			const prefix = property.slice(0, -"-vertical".length);
			return [
				[`${prefix}-top`, value],
				[`${prefix}-bottom`, value],
			];
		}
		if (property === "gap") {
			const values = splitValue(value);
			if (values.length < 1 || values.length > 2) return null;
			return [
				["row-gap", values[0] as string],
				["column-gap", (values[1] ?? values[0]) as string],
			];
		}
		if (property === "flex-flow") {
			if (isCssWideKeyword(value))
				return [
					["flex-direction", value],
					["flex-wrap", value],
				];
			const values = splitValue(value);
			if (values.length < 1 || values.length > 2) return null;
			const direction = values.find((part) => isRegisteredShorthandComponent("flex-direction", part)) ?? "row";
			const wrap = values.find((part) => isRegisteredShorthandComponent("flex-wrap", part)) ?? "nowrap";
			if (values.some((part) => part !== direction && part !== wrap)) return null;
			return [
				["flex-direction", direction],
				["flex-wrap", wrap],
			];
		}
		if (property === "flex") {
			if (isCssWideKeyword(value))
				return [
					["flex-grow", value],
					["flex-shrink", value],
					["flex-basis", value],
				];
			if (value === "none")
				return [
					["flex-grow", "0"],
					["flex-shrink", "0"],
					["flex-basis", "auto"],
				];
			if (value === "auto")
				return [
					["flex-grow", "1"],
					["flex-shrink", "1"],
					["flex-basis", "auto"],
				];
			const values = splitValue(value);
			if (values.length < 1 || values.length > 3) return null;
			const numeric = (part: string | undefined) => part !== undefined && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(part);
			if (!numeric(values[0])) {
				return values.length === 1
					? [
							["flex-grow", "1"],
							["flex-shrink", "1"],
							["flex-basis", values[0] as string],
						]
					: null;
			}
			const grow = values[0] as string;
			const hasShrink = numeric(values[1]);
			if (!hasShrink && values.length > 2) return null;
			const shrink = hasShrink ? (values[1] as string) : "1";
			const basis = values[hasShrink ? 2 : 1] ?? "0%";
			return [
				["flex-grow", grow],
				["flex-shrink", shrink],
				["flex-basis", basis],
			];
		}
		if (property === "border") {
			const components = borderComponents(value);
			return (
				components?.flatMap(([component, componentValue]) =>
					sides.map((side) => [`border-${side}-${component}`, componentValue] as const),
				) ?? null
			);
		}
		const sideBorder = property.match(/^border-(top|right|bottom|left)$/);
		if (sideBorder) {
			const components = borderComponents(value);
			return (
				components?.map(
					([component, componentValue]) => [`border-${sideBorder[1]}-${component}`, componentValue] as [string, string],
				) ?? null
			);
		}
		if (/^border-(width|style|color)$/.test(property)) {
			const component = property.slice("border-".length);
			return (
				fourSides("border", splitValue(value))?.map(([sideProperty, sideValue]) => [
					`${sideProperty}-${component}`,
					sideValue,
				]) ?? null
			);
		}
		if (property === "border-radius") {
			if (value.includes("/")) return null;
			const corners = fourSides("border", splitValue(value));
			if (!corners) return null;
			const cornerNames = ["top-left", "top-right", "bottom-right", "bottom-left"] as const;
			return corners.map(([, cornerValue], index) => [`border-${cornerNames[index]}-radius`, cornerValue]);
		}
		return [[property, value]];
	}

	const values = splitValue(value);
	return fourSides(property, values);
}

function parseAbsoluteLength(value: string): number | null {
	const match = value.trim().match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)\s*(pt|px|in|mm|cm)?$/i);
	if (!match) return null;
	const number = Number(match[1]);
	const unit = (match[2]?.toLowerCase() ?? "pt") as keyof typeof absoluteUnitToPt;
	return number * absoluteUnitToPt[unit];
}

const lengthPattern = new RegExp(`^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?(?:${lengthUnitPattern})?$`, "i");

export function valueSyntaxError(property: string, value: string): string | null {
	const normalized = value.trim().toLowerCase();
	if (!normalized) return "Values cannot be empty.";
	if (cssWideKeywords.has(normalized) || /var\s*\(/i.test(decodeCssEscapes(value))) return null;
	if (SEMANTIC_CSS_LENGTH_PROPERTIES.has(property)) {
		if (lengthPattern.test(normalized)) {
			if (property === "font-size" && Number.parseFloat(normalized) < 0) {
				return "font-size cannot be negative.";
			}
			return null;
		}
		if (lengthValueKeywords.has(normalized)) return null;
		return `${property} requires a supported PDF length.`;
	}
	if (property === "size") {
		const parts = splitValue(normalized);
		return isRegisteredPropertyValue(property, normalized) ||
			(parts.length >= 1 && parts.length <= 2 && parts.every((part) => lengthPattern.test(part)))
			? null
			: "size requires A4, letter, or one or two PDF lengths.";
	}
	if (property === "display")
		return isRegisteredPropertyValue(property, normalized) ? null : "display supports flex or none.";
	if (property === "direction")
		return isRegisteredPropertyValue(property, normalized) ? null : "direction supports ltr or rtl.";
	if (/^(?:border-style|border-(?:top|right|bottom|left)-style)$/.test(property)) {
		return isRegisteredPropertyValue(property, normalized) ? null : "border styles support dotted, dashed, or solid.";
	}
	if (property === "break-before")
		return isRegisteredPropertyValue(property, normalized) ? null : "break-before supports auto or page.";
	if (property === "break-inside")
		return isRegisteredPropertyValue(property, normalized) ? null : "break-inside supports auto or avoid.";
	if (property === "-resume-fixed")
		return isRegisteredPropertyValue(property, normalized) ? null : "-resume-fixed requires a boolean.";
	if (
		property === "order" ||
		property === "orphans" ||
		property === "widows" ||
		property === "z-index" ||
		property === "max-lines"
	) {
		const number = Number(normalized);
		return Number.isInteger(number) ? null : `${property} requires an integer.`;
	}
	if (property === "opacity" || property === "flex-grow" || property === "flex-shrink") {
		const number = Number(normalized);
		if (!Number.isFinite(number)) return `${property} requires a finite number.`;
		if (property === "opacity" && (number < 0 || number > 1)) return "opacity must be between 0 and 1.";
		return null;
	}
	if (property === "line-height") {
		return isRegisteredPropertyValue(property, normalized) || lengthPattern.test(normalized)
			? null
			: "line-height requires a number or PDF length.";
	}
	return null;
}

function validateValue(property: string, value: string, node: AstNode, diagnostics: SemanticCssDiagnostic[]): void {
	const decoded = decodeCssEscapes(value).toLowerCase();
	if (property === "src" || /\burl\s*\(/i.test(decoded)) {
		diagnostic(diagnostics, "FORBIDDEN_CSS_VALUE", "External CSS resources are not supported.", node);
		return;
	}

	for (const match of decoded.matchAll(
		/(^|[\s,(])([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)\s*(pt|px|in|mm|cm)(?=$|[\s,)])/gi,
	)) {
		const points = parseAbsoluteLength(`${match[2]}${match[3]}`);
		if (points === null || !Number.isFinite(points) || Math.abs(points) > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt) {
			diagnostic(
				diagnostics,
				"INVALID_VALUE",
				"Absolute lengths must be finite and within the Semantic CSS limit.",
				node,
			);
			return;
		}
		if (property === "font-size" && (points < 4 || points > 72)) {
			diagnostic(diagnostics, "EXTREME_VALUE", "This font size is renderable but unusually extreme.", node, "warning");
		}
	}
}

function parseMediaFeature(source: string): MediaFeature | null {
	const orientation = source.match(/^\(\s*orientation\s*:\s*(portrait|landscape)\s*\)$/i);
	if (orientation) return { name: "orientation", value: orientation[1]?.toLowerCase() as "portrait" | "landscape" };

	const dimensions = source.match(/^\(\s*(min-|max-)?(width|height)\s*:\s*([^)]+)\s*\)$/i);
	if (!dimensions) return null;
	return {
		name: dimensions[2]?.toLowerCase() as "width" | "height",
		comparison: dimensions[1] ? (dimensions[1].toLowerCase().startsWith("min") ? "min" : "max") : "equal",
		value: dimensions[3]?.trim() ?? "",
	};
}

function splitOutsideParentheses(value: string, separator: "," | "and"): string[] {
	const parts: string[] = [];
	let start = 0;
	let depth = 0;
	for (let index = 0; index < value.length; index++) {
		const character = value[index];
		if (character === "(") depth++;
		if (character === ")") depth--;
		if (depth !== 0) continue;
		if (separator === "," && character === ",") {
			parts.push(value.slice(start, index).trim());
			start = index + 1;
		} else if (
			separator === "and" &&
			value.slice(index, index + 3).toLowerCase() === "and" &&
			/\s/.test(value[index - 1] ?? " ") &&
			/\s/.test(value[index + 3] ?? " ")
		) {
			parts.push(value.slice(start, index).trim());
			start = index + 3;
		}
	}
	parts.push(value.slice(start).trim());
	return parts;
}

function parseMedia(node: AstNode, diagnostics: SemanticCssDiagnostic[]): readonly CompiledMediaQuery[] | null {
	const source = node.prelude ? csstree.generate(node.prelude) : "";
	const querySources = splitOutsideParentheses(source, ",");
	if (querySources.length > maxMediaQueryBranches) {
		diagnostic(diagnostics, "RESOURCE_LIMIT", "The media query list exceeds the Semantic CSS branch limit.", node);
		return null;
	}
	const queries = querySources.map((query) => {
		const features = splitOutsideParentheses(query, "and").map(parseMediaFeature);
		return features.every((feature): feature is MediaFeature => feature !== null) ? { features } : null;
	});
	const invalidValue = queries.some((query) =>
		query?.features.some((feature) => {
			if (feature.name === "orientation") return false;
			const match = feature.value.match(
				/^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)(?:pt|px|in|mm|cm|vw|vh|em|rem)?$/i,
			);
			if (!match) {
				return true;
			}
			if (!Number.isFinite(Number(match[1]))) return true;
			const absolute = parseAbsoluteLength(feature.value);
			return (
				absolute !== null &&
				(!Number.isFinite(absolute) || Math.abs(absolute) > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt)
			);
		}),
	);
	if (queries.length === 0 || queries.some((query) => query === null) || invalidValue) {
		diagnostic(
			diagnostics,
			"INVALID_MEDIA_QUERY",
			"Semantic CSS media queries support only width, height, and orientation.",
			node,
		);
		return null;
	}
	return queries as readonly CompiledMediaQuery[];
}

function combineMedia(
	parent: readonly CompiledMediaQuery[],
	child: readonly CompiledMediaQuery[],
): readonly CompiledMediaQuery[] | null {
	const parentBranches = Math.max(parent.length, 1);
	if (child.length > Math.floor(maxMediaQueryBranches / parentBranches)) return null;
	if (parent.length === 0) return child;
	return parent.flatMap((left) => child.map((right) => ({ features: [...left.features, ...right.features] })));
}

export function compileProgram(stylesheet: ParsedStylesheet, languageVersion: number): CompileProgramResult {
	const diagnostics: SemanticCssDiagnostic[] = [];
	const rules: CompiledStyleRule[] = [];
	let ruleCount = 0;
	let declarationCount = 0;
	let sourceOrder = 0;

	const compileRule = (node: AstNode, media: readonly CompiledMediaQuery[]) => {
		if (++ruleCount > SEMANTIC_CSS_LIMITS_V1.maxRules) {
			if (ruleCount === SEMANTIC_CSS_LIMITS_V1.maxRules + 1) {
				diagnostic(diagnostics, "RESOURCE_LIMIT", "The stylesheet has too many rules.", node);
			}
			return;
		}

		const selectorResult = node.prelude
			? compileSelector(node.prelude)
			: { selector: null, error: "Missing selector." };
		if (!selectorResult.selector) {
			const code = /too many|too long/i.test(selectorResult.error ?? "") ? "RESOURCE_LIMIT" : "INVALID_SELECTOR";
			diagnostic(diagnostics, code, selectorResult.error ?? "Invalid selector.", node.prelude ?? node);
			return;
		}

		const declarations: CompiledDeclaration[] = [];
		for (const declaration of children(node.block)) {
			if (declaration.type !== "Declaration" || !declaration.property) continue;
			if (++declarationCount > SEMANTIC_CSS_LIMITS_V1.maxDeclarations) {
				if (declarationCount === SEMANTIC_CSS_LIMITS_V1.maxDeclarations + 1) {
					diagnostic(diagnostics, "RESOURCE_LIMIT", "The stylesheet has too many declarations.", declaration);
				}
				continue;
			}

			const decodedProperty = identifier(declaration.property);
			const property = decodedProperty.startsWith("--") ? decodedProperty : decodedProperty.toLowerCase();
			const lowerProperty = property.toLowerCase();
			if (lowerProperty.startsWith("--resume-")) {
				diagnostic(
					diagnostics,
					"SYSTEM_VARIABLE_READONLY",
					"System variables beginning with --resume- are read-only.",
					declaration,
				);
				continue;
			}
			if (!property.startsWith("--") && !PROPERTY_REGISTRY_V1[property]) {
				diagnostic(
					diagnostics,
					property === "src" ? "FORBIDDEN_CSS_VALUE" : "UNSUPPORTED_PROPERTY",
					`The ${property} property is not supported by Semantic CSS.`,
					declaration,
				);
				continue;
			}
			if (media.length > 0 && property === "size") {
				diagnostic(diagnostics, "MEDIA_PAGE_SIZE", "The size property is not allowed inside @media.", declaration);
				continue;
			}

			const value =
				typeof declaration.value === "string" ? declaration.value : csstree.generate(declaration.value as CssNode);
			const trimmedValue = value.trim();
			const expanded = /var\s*\(/i.test(decodeCssEscapes(trimmedValue))
				? ([[property, trimmedValue]] as const)
				: expandShorthand(property, trimmedValue);
			if (!expanded) {
				diagnostic(diagnostics, "INVALID_VALUE", `Invalid ${property} shorthand.`, declaration);
				continue;
			}
			for (const [expandedProperty, expandedValue] of expanded) {
				validateValue(expandedProperty, expandedValue, declaration, diagnostics);
				const syntaxError = property.startsWith("--") ? null : valueSyntaxError(expandedProperty, expandedValue);
				if (syntaxError) diagnostic(diagnostics, "INVALID_VALUE", syntaxError, declaration);
				declarations.push({
					property: expandedProperty,
					value: expandedValue,
					important: declaration.important === true || declaration.important === "important",
					sourceOrder: sourceOrder++,
					range: range(declaration.loc),
				});
			}
		}

		rules.push({
			selector: selectorResult.selector,
			declarations,
			media,
			range: range(node.loc),
		});
	};

	const visit = (nodes: readonly AstNode[], media: readonly CompiledMediaQuery[], mediaDepth: number): void => {
		for (const node of nodes) {
			if (node.type === "Rule") {
				compileRule(node, media);
				continue;
			}
			if (node.type !== "Atrule" || !node.name) continue;
			const name = identifier(node.name).toLowerCase();
			if (name === "version") continue;
			if (name !== "media") {
				diagnostic(
					diagnostics,
					name === "import" || name === "font-face" ? "FORBIDDEN_AT_RULE" : "UNSUPPORTED_AT_RULE",
					`@${name} is not supported by Semantic CSS.`,
					node,
				);
				continue;
			}
			if (mediaDepth >= SEMANTIC_CSS_LIMITS_V1.maxMediaNesting) {
				diagnostic(diagnostics, "RESOURCE_LIMIT", "Media nesting exceeds the Semantic CSS limit.", node);
				continue;
			}
			const compiledMedia = parseMedia(node, diagnostics);
			if (!compiledMedia) continue;
			const combinedMedia = combineMedia(media, compiledMedia);
			if (!combinedMedia) {
				diagnostic(diagnostics, "RESOURCE_LIMIT", "Nested media queries exceed the Semantic CSS branch limit.", node);
				continue;
			}
			visit(children(node.block), combinedMedia, mediaDepth + 1);
		}
	};

	const ast = stylesheet.ast as AstNode | null;
	if (ast) visit(children(ast), [], 0);
	const program = { languageVersion, rules } satisfies StyleProgram;
	return diagnostics.some(({ severity }) => severity === "error")
		? { program: null, diagnostics }
		: { program: structuredClone(program), diagnostics };
}
