import type { Specificity } from "./selector";
import type {
	CompiledDeclaration,
	CompiledMediaQuery,
	CompiledStyleRule,
	ResolvedNodeStyle,
	ResolvedPageDimensions,
	ResolvedPageSize,
	ResolveStylesheetContext,
	ResolveStylesheetResult,
	SemanticCssDiagnostic,
	SemanticNode,
	SourceRange,
	StructuralPresentation,
	StyleProgram,
} from "./types";
import { createDiagnostic } from "./diagnostics";
import { SEMANTIC_CSS_LIMITS_V1 } from "./limits";
import { PROPERTY_REGISTRY_V1 } from "./registry/properties";
import { createSystemVariables } from "./registry/system-variables";
import { createSelectorMatcher } from "./selector";
import {
	cssFunctionDepth,
	decodeCssEscapes,
	expandShorthand,
	SEMANTIC_CSS_LENGTH_PROPERTIES,
	valueSyntaxError,
} from "./values";

type FlatNode = {
	node: SemanticNode;
	parent: FlatNode | null;
	pageKey: string | null;
};

type MatchedRule = {
	rule: CompiledStyleRule;
	specificity: Specificity;
};

type Winner = {
	declaration: CompiledDeclaration;
	specificity: Specificity;
};

type LengthContext = {
	page: ResolvedPageDimensions;
	parentFontSize: number;
	fontSize: number;
	rootFontSize: number;
};

type CssWideKeyword = "inherit" | "initial" | "revert" | "unset";

type VariableExpansionBudget = {
	work: number;
};

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

const cssWideKeywords = new Set(["inherit", "initial", "revert", "unset"]);
const maxVariableExpansionOutputCodeUnits = SEMANTIC_CSS_LIMITS_V1.maxSourceBytes;
const maxVariableExpansionWorkCodeUnits = SEMANTIC_CSS_LIMITS_V1.maxSourceBytes * 4;

const structuralKeys: Readonly<Record<string, keyof StructuralPresentation>> = {
	"break-before": "breakBefore",
	"break-inside": "breakInside",
	"-resume-fixed": "fixed",
	"-resume-min-presence-ahead": "minPresenceAhead",
	orphans: "orphans",
	widows: "widows",
	size: "pageSize",
};

function cssWideKeyword(value: string): CssWideKeyword | null {
	const keyword = value.toLowerCase();
	return cssWideKeywords.has(keyword) ? (keyword as CssWideKeyword) : null;
}

function compareSpecificity(left: Specificity, right: Specificity): number {
	return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
}

function candidateWins(candidate: Winner, current: Winner | undefined): boolean {
	if (!current) return true;
	if (candidate.declaration.important !== current.declaration.important) return candidate.declaration.important;
	return (
		compareSpecificity(candidate.specificity, current.specificity) > 0 ||
		(compareSpecificity(candidate.specificity, current.specificity) === 0 &&
			candidate.declaration.sourceOrder > current.declaration.sourceOrder)
	);
}

function flattenTree(root: SemanticNode): FlatNode[] | null {
	const result: FlatNode[] = [];
	const stack: { node: SemanticNode; parent: FlatNode | null; pageKey: string | null }[] = [
		{ node: root, parent: null, pageKey: null },
	];
	while (stack.length > 0) {
		const next = stack.pop();
		if (!next) break;
		if (result.length >= SEMANTIC_CSS_LIMITS_V1.maxSemanticNodes) return null;
		const pageKey = next.node.kind === "page" ? next.node.key : next.pageKey;
		const flat = { node: next.node, parent: next.parent, pageKey };
		result.push(flat);
		const childCount = next.node.children.length;
		if (childCount > SEMANTIC_CSS_LIMITS_V1.maxSemanticNodes - result.length - stack.length) return null;
		for (let index = childCount - 1; index >= 0; index--) {
			const child = next.node.children[index];
			if (child) stack.push({ node: child, parent: flat, pageKey });
		}
	}
	return result;
}

function matchingSpecificity(
	rule: CompiledStyleRule,
	nodeKey: string,
	matches: ReturnType<typeof createSelectorMatcher>,
): Specificity | null {
	let best: Specificity | null = null;
	for (const selector of rule.selector.selectors) {
		if (!matches({ selectors: [selector] }, nodeKey)) continue;
		if (!best || compareSpecificity(selector.specificity, best) > 0) best = selector.specificity;
	}
	return best;
}

function defaultPageDimensions(
	format: ResolveStylesheetContext["baseSettings"]["page"]["format"],
): ResolvedPageDimensions {
	if (format === "letter") return { width: 612, height: 792 };
	return { width: 595.28, height: 841.89 };
}

function initialPages(
	flatNodes: readonly FlatNode[],
	context: ResolveStylesheetContext,
): Map<string, ResolvedPageDimensions> {
	const authored = new Map(context.pages.map((page) => [page.pageKey, { width: page.width, height: page.height }]));
	const fallback = defaultPageDimensions(context.baseSettings.page.format);
	const pages = new Map<string, ResolvedPageDimensions>();
	for (const { node } of flatNodes) {
		if (node.kind === "page") pages.set(node.key, authored.get(node.key) ?? fallback);
	}
	return pages;
}

function pageFor(
	node: FlatNode,
	pages: ReadonlyMap<string, ResolvedPageDimensions>,
	fallback: ResolvedPageDimensions,
): ResolvedPageDimensions {
	return (node.pageKey ? pages.get(node.pageKey) : undefined) ?? pages.values().next().value ?? fallback;
}

function toPoints(value: string, property: string, context: LengthContext): number | string | null {
	const match = value
		.trim()
		.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)\s*(pt|px|in|mm|cm|%|vw|vh|em|rem)?$/i);
	if (!match) return null;
	const number = Number(match[1]);
	if (!Number.isFinite(number)) return null;
	const unit = match[2]?.toLowerCase() ?? "pt";
	if (unit === "%") return property === "font-size" ? (number / 100) * context.parentFontSize : `${number}%`;
	if (unit === "vw") return (number / 100) * context.page.width;
	if (unit === "vh") return (number / 100) * context.page.height;
	if (unit === "rem") return number * context.rootFontSize;
	if (unit === "em") {
		return number * (property === "font-size" ? context.parentFontSize : context.fontSize);
	}
	return number * absoluteUnitToPt[unit as keyof typeof absoluteUnitToPt];
}

function mediaMatches(query: CompiledMediaQuery, dimensions: ResolvedPageDimensions, rootFontSize: number): boolean {
	const lengthContext = {
		page: dimensions,
		parentFontSize: rootFontSize,
		fontSize: rootFontSize,
		rootFontSize,
	};
	return query.features.every((feature) => {
		if (feature.name === "orientation") {
			return feature.value === (dimensions.width > dimensions.height ? "landscape" : "portrait");
		}
		const expected = toPoints(feature.value, feature.name, lengthContext);
		if (typeof expected !== "number") return false;
		const actual = dimensions[feature.name];
		if (feature.comparison === "min") return actual >= expected;
		if (feature.comparison === "max") return actual <= expected;
		return actual === expected;
	});
}

function ruleApplies(
	rule: CompiledStyleRule,
	dimensions: ResolvedPageDimensions,
	rootFontSize: number,
	includeMedia: boolean,
): boolean {
	if (rule.media.length === 0) return true;
	return includeMedia && rule.media.some((query) => mediaMatches(query, dimensions, rootFontSize));
}

function winnersFor(
	matches: readonly MatchedRule[],
	dimensions: ResolvedPageDimensions,
	rootFontSize: number,
	includeMedia: boolean,
): Map<string, Winner> {
	const winners = new Map<string, Winner>();
	for (const { rule, specificity } of matches) {
		if (!ruleApplies(rule, dimensions, rootFontSize, includeMedia)) continue;
		for (const declaration of rule.declarations) {
			const candidate = { declaration, specificity };
			if (candidateWins(candidate, winners.get(declaration.property))) {
				winners.set(declaration.property, candidate);
			}
		}
	}
	return winners;
}

function splitVariable(value: string): [name: string, fallback: string | null] {
	let depth = 0;
	for (let index = 0; index < value.length; index++) {
		if (value[index] === "(") depth++;
		if (value[index] === ")") depth--;
		if (value[index] === "," && depth === 0) {
			return [value.slice(0, index).trim(), value.slice(index + 1).trim()];
		}
	}
	return [value.trim(), null];
}

function matchingParenthesis(value: string, opening: number): number {
	let depth = 0;
	for (let index = opening; index < value.length; index++) {
		if (value[index] === "(") depth++;
		if (value[index] === ")" && --depth === 0) return index;
	}
	return -1;
}

function expandVariables(
	source: string,
	custom: ReadonlyMap<string, string>,
	system: Readonly<Record<string, string>>,
	diagnostics: SemanticCssDiagnostic[],
	range: SourceRange,
	stack: readonly string[] = [],
	depth = 0,
	budget: VariableExpansionBudget = { work: 0 },
): string | null {
	if (depth > SEMANTIC_CSS_LIMITS_V1.maxVariableExpansionDepth) {
		diagnostics.push(
			createDiagnostic("RESOURCE_LIMIT", "error", "Variable expansion exceeds the Semantic CSS limit.", range),
		);
		return null;
	}
	if (
		source.length > maxVariableExpansionOutputCodeUnits ||
		source.length > maxVariableExpansionWorkCodeUnits - budget.work
	) {
		diagnostics.push(
			createDiagnostic("RESOURCE_LIMIT", "error", "Variable expansion exceeds the Semantic CSS limit.", range),
		);
		return null;
	}
	budget.work += source.length;

	let value = decodeCssEscapes(source);
	if (value.length > maxVariableExpansionOutputCodeUnits) {
		diagnostics.push(
			createDiagnostic("RESOURCE_LIMIT", "error", "Variable expansion exceeds the Semantic CSS limit.", range),
		);
		return null;
	}
	while (true) {
		const match = /var\s*\(/i.exec(value);
		if (!match) break;
		const opening = (match.index ?? 0) + match[0].lastIndexOf("(");
		const closing = matchingParenthesis(value, opening);
		if (closing < 0) {
			diagnostics.push(createDiagnostic("INVALID_VALUE", "error", "Malformed var() expression.", range));
			return null;
		}
		const [name, fallback] = splitVariable(value.slice(opening + 1, closing));
		if (!name.startsWith("--")) {
			diagnostics.push(createDiagnostic("INVALID_VALUE", "error", "var() requires a custom-property name.", range));
			return null;
		}
		if (stack.includes(name)) {
			diagnostics.push(createDiagnostic("VARIABLE_CYCLE", "error", `Variable cycle detected at ${name}.`, range));
			return null;
		}
		const replacement = custom.get(name) ?? system[name] ?? fallback;
		if (replacement === null || replacement === undefined) {
			diagnostics.push(
				createDiagnostic("UNRESOLVED_VARIABLE", "error", `No value or fallback exists for ${name}.`, range),
			);
			return null;
		}
		const expanded = expandVariables(
			replacement,
			custom,
			system,
			diagnostics,
			range,
			[...stack, name],
			depth + 1,
			budget,
		);
		if (expanded === null) return null;
		const nextLength = (match.index ?? 0) + expanded.length + value.length - closing - 1;
		if (
			nextLength > maxVariableExpansionOutputCodeUnits ||
			nextLength > maxVariableExpansionWorkCodeUnits - budget.work
		) {
			diagnostics.push(
				createDiagnostic("RESOURCE_LIMIT", "error", "Variable expansion exceeds the Semantic CSS limit.", range),
			);
			return null;
		}
		budget.work += nextLength;
		value = `${value.slice(0, match.index)}${expanded}${value.slice(closing + 1)}`;
	}

	if (/\burl\s*\(/i.test(value)) {
		diagnostics.push(
			createDiagnostic("FORBIDDEN_CSS_VALUE", "error", "External CSS resources are not supported.", range),
		);
		return null;
	}
	if (cssFunctionDepth(value) > SEMANTIC_CSS_LIMITS_V1.maxFunctionDepth) {
		diagnostics.push(
			createDiagnostic("RESOURCE_LIMIT", "error", "CSS function nesting exceeds the Semantic CSS limit.", range),
		);
		return null;
	}
	return value.trim();
}

function expandedWinnersFor(
	matches: readonly MatchedRule[],
	dimensions: ResolvedPageDimensions,
	rootFontSize: number,
	custom: ReadonlyMap<string, string>,
	system: Readonly<Record<string, string>>,
	diagnostics: SemanticCssDiagnostic[],
): Map<string, Winner> {
	const cascaded = new Map<string, Winner>();
	for (const { rule, specificity } of matches) {
		if (!ruleApplies(rule, dimensions, rootFontSize, true)) continue;
		for (const declaration of rule.declarations) {
			if (declaration.property.startsWith("--")) continue;
			const targets = expandShorthand(declaration.property, "inherit") ?? [[declaration.property, "inherit"]];
			for (const [property] of targets) {
				const candidate = { declaration, specificity };
				if (candidateWins(candidate, cascaded.get(property))) cascaded.set(property, candidate);
			}
		}
	}

	const expansions = new Map<CompiledDeclaration, readonly [property: string, value: string][] | null>();
	const winners = new Map<string, Winner>();
	for (const [property, winner] of cascaded) {
		if (property === "size") {
			winners.set(property, winner);
			continue;
		}
		let declarations = expansions.get(winner.declaration);
		if (declarations === undefined) {
			const expandedValue = expandVariables(
				winner.declaration.value,
				custom,
				system,
				diagnostics,
				winner.declaration.range,
			);
			declarations = expandedValue === null ? null : expandShorthand(winner.declaration.property, expandedValue);
			if (expandedValue !== null && !declarations) {
				diagnostics.push(
					createDiagnostic(
						"INVALID_VALUE",
						"error",
						`Invalid ${winner.declaration.property} shorthand.`,
						winner.declaration.range,
					),
				);
			}
			expansions.set(winner.declaration, declarations);
		}
		const value = declarations?.find(([expandedProperty]) => expandedProperty === property)?.[1];
		if (value === undefined) continue;
		winners.set(property, {
			declaration: { ...winner.declaration, property, value },
			specificity: winner.specificity,
		});
	}
	return winners;
}

function customProperties(
	winners: ReadonlyMap<string, Winner>,
	parent: ReadonlyMap<string, string> | undefined,
): Map<string, string> {
	let custom: Map<string, string> | undefined;
	for (const [property, { declaration }] of winners) {
		if (!property.startsWith("--")) continue;
		const keyword = cssWideKeyword(declaration.value);
		if (keyword === "inherit" || keyword === "revert" || keyword === "unset") continue;
		if (keyword === "initial") {
			if (parent?.has(property)) {
				custom ??= new Map(parent);
				custom.delete(property);
			}
		} else if (parent?.get(property) !== declaration.value) {
			custom ??= new Map(parent);
			custom.set(property, declaration.value);
		}
	}
	return custom ?? (parent as Map<string, string> | undefined) ?? new Map();
}

function parsePageSize(value: string, lengthContext: LengthContext): ResolvedPageSize | null {
	if (value.toLowerCase() === "a4") return "A4";
	if (value.toLowerCase() === "letter") return "LETTER";
	const parts = value.trim().split(/\s+/);
	if (parts.length < 1 || parts.length > 2) return null;
	const width = toPoints(parts[0] ?? "", "size", lengthContext);
	const height = parts[1] ? toPoints(parts[1], "size", lengthContext) : undefined;
	if (typeof width !== "number" || (height !== undefined && typeof height !== "number")) return null;
	if (
		width <= 0 ||
		Math.abs(width) > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt ||
		(height !== undefined && (height <= 0 || Math.abs(height) > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt))
	) {
		return null;
	}
	return height === undefined ? { width } : { width, height };
}

function dimensionsForSize(size: ResolvedPageSize, authored: ResolvedPageDimensions): ResolvedPageDimensions {
	if (size === "A4") return { width: 595.28, height: 841.89 };
	if (size === "LETTER") return { width: 612, height: 792 };
	return { width: size.width, height: size.height ?? authored.height };
}

function builderPageSize(context: ResolveStylesheetContext, nodeKey: string): ResolvedPageSize {
	return (
		context.baseStyles[nodeKey]?.structural.pageSize ??
		(context.baseSettings.page.format === "letter" ? "LETTER" : "A4")
	);
}

function cssWideValue(
	keyword: CssWideKeyword,
	property: string,
	base: Readonly<Record<string, string | number>>,
	parent: Readonly<Record<string, string | number>> | undefined,
): string | number | undefined {
	const definition = PROPERTY_REGISTRY_V1[property];
	if (keyword === "inherit") return parent?.[property];
	if (keyword === "initial") return;
	if (keyword === "unset") return definition?.inheritable ? parent?.[property] : undefined;
	return base[property] ?? (definition?.inheritable ? parent?.[property] : undefined);
}

function normalizeValue(property: string, value: string, context: LengthContext): string | number | null {
	if (SEMANTIC_CSS_LENGTH_PROPERTIES.has(property)) {
		const length = toPoints(value, property, context);
		if (length !== null) return length;
		if (/^(auto|none|normal|max-content|min-content|fit-content|thin|medium|thick)$/i.test(value)) {
			return value.toLowerCase();
		}
		return null;
	}
	if (property === "line-height") {
		if (value.toLowerCase() === "normal") return "normal";
		return toPoints(value, property, context);
	}
	if (/^(opacity|flex-grow|flex-shrink|order|orphans|widows|z-index|max-lines)$/i.test(property)) {
		const number = Number(value);
		if (!Number.isFinite(number)) return null;
		if (property === "opacity" && (number < 0 || number > 1)) return null;
		return number;
	}
	return value;
}

function applyStructuralCssWide(
	keyword: CssWideKeyword,
	property: string,
	structural: StructuralPresentation,
	base: ResolvedNodeStyle,
	parent: ResolvedNodeStyle | undefined,
	revertPageSize: ResolvedPageSize,
): boolean {
	const key = structuralKeys[property];
	if (!key) return false;
	const value =
		keyword === "inherit"
			? parent?.structural[key]
			: keyword === "revert"
				? (base.structural[key] ?? (key === "pageSize" ? revertPageSize : undefined))
				: undefined;
	if (value === undefined) delete structural[key];
	else Object.assign(structural, { [key]: value });
	return true;
}

function structuralValue(
	property: string,
	value: string | number,
	structural: StructuralPresentation,
	pageSize?: ResolvedPageSize,
): boolean {
	if (property === "break-before") {
		if (value === "page") structural.breakBefore = "page";
		else if (value === "auto") delete structural.breakBefore;
		else return false;
	}
	if (property === "break-inside") {
		if (value === "avoid") structural.breakInside = "avoid";
		else if (value === "auto") delete structural.breakInside;
		else return false;
	}
	if (property === "-resume-fixed") {
		if (value === "true" || value === "1" || value === 1) structural.fixed = true;
		else if (value === "false" || value === "0" || value === 0) structural.fixed = false;
		else return false;
	}
	if (property === "-resume-min-presence-ahead") {
		if (typeof value === "number" && value >= 0) structural.minPresenceAhead = value;
		else return false;
	}
	if (property === "orphans" || property === "widows") {
		if (typeof value !== "number" || !Number.isInteger(value) || value < 1) return false;
		if (property === "orphans") structural.orphans = value;
		else structural.widows = value;
	}
	if (property === "size") {
		if (!pageSize) return false;
		structural.pageSize = pageSize;
	}
	return true;
}

function createRenderTree(
	tree: SemanticNode,
	flatNodes: readonly FlatNode[],
	nodes: Readonly<Record<string, ResolvedNodeStyle>>,
): SemanticNode {
	const rendered = new Map<string, SemanticNode>();
	for (let index = flatNodes.length - 1; index >= 0; index--) {
		const source = flatNodes[index]?.node;
		if (!source) continue;
		const children = source.children
			.map((child, sourceIndex) => ({ child: rendered.get(child.key), sourceIndex }))
			.filter((entry): entry is { child: SemanticNode; sourceIndex: number } => entry.child !== undefined)
			.filter(({ child }) => !nodes[child.key]?.hidden)
			.sort((left, right) => {
				const order = (nodes[left.child.key]?.order ?? 0) - (nodes[right.child.key]?.order ?? 0);
				return order || left.sourceIndex - right.sourceIndex;
			})
			.map(({ child }) => child);
		rendered.set(source.key, { ...source, attributes: { ...source.attributes }, roles: [...source.roles], children });
	}
	return rendered.get(tree.key) ?? tree;
}

export function resolveStylesheet(
	program: StyleProgram,
	tree: SemanticNode,
	context: ResolveStylesheetContext,
): ResolveStylesheetResult {
	const flatNodes = flattenTree(tree);
	if (!flatNodes) {
		return {
			nodes: {},
			renderTree: tree,
			diagnostics: [
				createDiagnostic("RESOURCE_LIMIT", "error", "The semantic tree exceeds the Semantic CSS node limit."),
			],
		};
	}
	if (
		context.pages.some(
			({ width, height }) =>
				!Number.isFinite(width) ||
				!Number.isFinite(height) ||
				width <= 0 ||
				height <= 0 ||
				width > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt ||
				height > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt,
		)
	) {
		return {
			nodes: {},
			renderTree: tree,
			diagnostics: [
				createDiagnostic("INVALID_VALUE", "error", "Authored page dimensions must be finite and positive."),
			],
		};
	}
	const diagnostics: SemanticCssDiagnostic[] = [];

	const matched = new Map<string, MatchedRule[]>();
	const selectorMatches = createSelectorMatcher(tree);
	for (const { node } of flatNodes) {
		const rules: MatchedRule[] = [];
		for (const rule of program.rules) {
			const specificity = matchingSpecificity(rule, node.key, selectorMatches);
			if (specificity) rules.push({ rule, specificity });
		}
		matched.set(node.key, rules);
	}
	const nodeKinds = new Map(flatNodes.map(({ node }) => [node.key, node.kind]));
	const cascadeKinds = new Map<string, ReadonlySet<SemanticNode["kind"]>>();
	for (const [canonicalKey, aliasKeys] of Object.entries(context.aliases ?? {})) {
		const canonicalKind = nodeKinds.get(canonicalKey);
		if (!canonicalKind) continue;
		const kinds = new Set<SemanticNode["kind"]>([canonicalKind]);
		const rules = [...(matched.get(canonicalKey) ?? [])];
		for (const aliasKey of aliasKeys) {
			const aliasKind = nodeKinds.get(aliasKey);
			if (!aliasKind) continue;
			kinds.add(aliasKind);
			rules.push(...(matched.get(aliasKey) ?? []));
		}
		matched.set(canonicalKey, rules);
		cascadeKinds.set(canonicalKey, kinds);
	}
	for (const rule of program.rules) {
		const matchingNodes = flatNodes.filter(({ node }) =>
			matched.get(node.key)?.some((matchedRule) => matchedRule.rule === rule),
		);
		if (matchingNodes.length === 0) {
			diagnostics.push(
				createDiagnostic("SELECTOR_NO_MATCH", "warning", "This selector matches no semantic resume node.", rule.range),
			);
			continue;
		}
		for (const declaration of rule.declarations) {
			if (declaration.property.startsWith("--")) continue;
			const definition = PROPERTY_REGISTRY_V1[declaration.property];
			if (definition && !matchingNodes.some(({ node }) => definition.appliesTo.includes(node.kind))) {
				diagnostics.push(
					createDiagnostic(
						"PROPERTY_NOT_APPLICABLE",
						"warning",
						`${declaration.property} cannot apply to the matched semantic node kinds.`,
						declaration.range,
					),
				);
			}
		}
	}

	const rootFontSize = context.baseSettings.typography.body.fontSize;
	const fallbackDimensions = defaultPageDimensions(context.baseSettings.page.format);
	const pages = initialPages(flatNodes, context);
	const resolvedPageSizes = new Map<string, ResolvedPageSize>();
	const resolvedPageSizeValues = new Map<string, string>();
	const preliminaryCustom = new Map<string, Map<string, string>>();

	for (const node of flatNodes) {
		const dimensions = pageFor(node, pages, fallbackDimensions);
		const rawWinners = winnersFor(matched.get(node.node.key) ?? [], dimensions, rootFontSize, false);
		const custom = customProperties(rawWinners, node.parent ? preliminaryCustom.get(node.parent.node.key) : undefined);
		preliminaryCustom.set(node.node.key, custom);
		if (node.node.kind !== "page") continue;
		const size = rawWinners.get("size");
		if (!size) continue;
		const system = createSystemVariables(context.baseSettings, dimensions);
		const expanded = expandVariables(size.declaration.value, custom, system, diagnostics, size.declaration.range);
		if (!expanded) continue;
		const keyword = cssWideKeyword(expanded);
		const parsed =
			keyword === "revert"
				? builderPageSize(context, node.node.key)
				: keyword === "inherit"
					? node.parent
						? context.baseStyles[node.parent.node.key]?.structural.pageSize
						: undefined
					: keyword
						? undefined
						: parsePageSize(expanded, {
								page: dimensions,
								parentFontSize: rootFontSize,
								fontSize: rootFontSize,
								rootFontSize,
							});
		if (keyword && !parsed) {
			resolvedPageSizeValues.set(node.node.key, expanded);
			continue;
		}
		if (!parsed) {
			diagnostics.push(createDiagnostic("INVALID_VALUE", "error", "Invalid page size.", size.declaration.range));
			continue;
		}
		resolvedPageSizeValues.set(node.node.key, expanded);
		resolvedPageSizes.set(node.node.key, parsed);
		pages.set(node.node.key, dimensionsForSize(parsed, dimensions));
	}

	const customByNode = new Map<string, Map<string, string>>();
	const resolved: Record<string, ResolvedNodeStyle> = {};
	for (const node of flatNodes) {
		const dimensions = pageFor(node, pages, fallbackDimensions);
		const rawWinners = winnersFor(matched.get(node.node.key) ?? [], dimensions, rootFontSize, true);
		const custom = customProperties(rawWinners, node.parent ? customByNode.get(node.parent.node.key) : undefined);
		customByNode.set(node.node.key, custom);
		const system = createSystemVariables(context.baseSettings, dimensions);
		const winners = expandedWinnersFor(
			matched.get(node.node.key) ?? [],
			dimensions,
			rootFontSize,
			custom,
			system,
			diagnostics,
		);

		const base = context.baseStyles[node.node.key] ?? { style: {}, structural: {}, hidden: false, order: 0 };
		const parent = node.parent ? resolved[node.parent.node.key] : undefined;
		const style: Record<string, string | number> = { ...base.style };
		const specifiedStyleProperties = new Set<string>();
		const hostBaseStyleProperties = new Set<string>();
		for (const [property, definition] of Object.entries(PROPERTY_REGISTRY_V1)) {
			if (!definition?.inheritable) continue;
			const inheritedFromAuthoredRule = parent?.specifiedStyleProperties?.includes(property) && !winners.has(property);
			if (inheritedFromAuthoredRule) {
				specifiedStyleProperties.add(property);
				if (parent?.hostBaseStyleProperties?.includes(property)) hostBaseStyleProperties.add(property);
				if (parent?.style[property] === undefined) delete style[property];
				else style[property] = parent.style[property];
			} else if (style[property] === undefined && parent?.style[property] !== undefined) {
				style[property] = parent.style[property];
			}
		}

		const fontSizeWinner = winners.get("font-size");
		const parentFontSize =
			typeof parent?.style["font-size"] === "number"
				? parent.style["font-size"]
				: context.baseSettings.typography.body.fontSize;
		const applicableKinds = cascadeKinds.get(node.node.key) ?? new Set([node.node.kind]);
		if (fontSizeWinner && PROPERTY_REGISTRY_V1["font-size"]?.appliesTo.some((kind) => applicableKinds.has(kind))) {
			const expanded = fontSizeWinner.declaration.value;
			const keyword = cssWideKeyword(expanded);
			if (keyword) {
				specifiedStyleProperties.add("font-size");
				if (keyword === "revert") hostBaseStyleProperties.add("font-size");
				const wide = cssWideValue(keyword, "font-size", base.style, parent?.style);
				if (wide === undefined) delete style["font-size"];
				else style["font-size"] = wide;
			} else {
				const syntaxError = valueSyntaxError("font-size", expanded);
				if (syntaxError) {
					diagnostics.push(createDiagnostic("INVALID_VALUE", "error", syntaxError, fontSizeWinner.declaration.range));
				} else {
					const normalized = normalizeValue("font-size", expanded, {
						page: dimensions,
						parentFontSize,
						fontSize: parentFontSize,
						rootFontSize,
					});
					if (normalized === null) {
						diagnostics.push(
							createDiagnostic("INVALID_VALUE", "error", "Invalid font-size value.", fontSizeWinner.declaration.range),
						);
					} else if (
						typeof normalized === "number" &&
						(normalized < 0 || normalized > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt)
					) {
						diagnostics.push(
							createDiagnostic(
								"INVALID_VALUE",
								"error",
								"font-size exceeds the Semantic CSS length limit.",
								fontSizeWinner.declaration.range,
							),
						);
					} else {
						specifiedStyleProperties.add("font-size");
						style["font-size"] = normalized;
						if (typeof normalized === "number" && (normalized < 4 || normalized > 72)) {
							diagnostics.push(
								createDiagnostic(
									"EXTREME_VALUE",
									"warning",
									"This font size is renderable but unusually extreme.",
									fontSizeWinner.declaration.range,
								),
							);
						}
					}
				}
			}
		}

		const fontSize = typeof style["font-size"] === "number" ? style["font-size"] : parentFontSize;
		const structural: StructuralPresentation = { ...base.structural };
		let hidden = base.hidden;
		let order = base.order;
		for (const [property, winner] of winners) {
			if (property.startsWith("--") || property === "font-size") continue;
			const definition = PROPERTY_REGISTRY_V1[property];
			if (!definition?.appliesTo.some((kind) => applicableKinds.has(kind))) continue;
			const expanded = winner.declaration.value;
			if (property === "size") {
				const resolvedValue = resolvedPageSizeValues.get(node.node.key);
				if (resolvedValue === undefined) continue;
				const sizeKeyword = cssWideKeyword(resolvedValue);
				if (sizeKeyword) {
					applyStructuralCssWide(
						sizeKeyword,
						property,
						structural,
						base,
						parent,
						builderPageSize(context, node.node.key),
					);
				} else {
					const pageSize = resolvedPageSizes.get(node.node.key);
					if (pageSize) structuralValue(property, resolvedValue, structural, pageSize);
				}
				continue;
			}
			const keyword = cssWideKeyword(expanded);
			if (keyword) {
				if (property === "display") {
					if (keyword === "inherit") {
						hidden = parent?.hidden ?? false;
						if (!hidden && parent?.style.display !== undefined) style.display = parent.style.display;
						else delete style.display;
					} else if (keyword === "revert") {
						hidden = base.hidden;
						if (base.style.display === undefined) delete style.display;
						else style.display = base.style.display;
					} else {
						hidden = false;
						delete style.display;
					}
				} else if (property === "order") {
					order = keyword === "inherit" ? (parent?.order ?? 0) : keyword === "revert" ? base.order : 0;
				} else if (definition.category === "structural") {
					applyStructuralCssWide(keyword, property, structural, base, parent, builderPageSize(context, node.node.key));
				} else {
					specifiedStyleProperties.add(property);
					if (keyword === "revert") hostBaseStyleProperties.add(property);
					const wide = cssWideValue(keyword, property, base.style, parent?.style);
					if (wide === undefined) delete style[property];
					else style[property] = wide;
				}
				continue;
			}
			const syntaxError = valueSyntaxError(property, expanded);
			if (syntaxError) {
				diagnostics.push(createDiagnostic("INVALID_VALUE", "error", syntaxError, winner.declaration.range));
				continue;
			}

			const normalized = normalizeValue(property, expanded, {
				page: dimensions,
				parentFontSize,
				fontSize,
				rootFontSize,
			});
			if (normalized === null) {
				diagnostics.push(
					createDiagnostic("INVALID_VALUE", "error", `Invalid ${property} value.`, winner.declaration.range),
				);
				continue;
			}
			if (typeof normalized === "number" && Math.abs(normalized) > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt) {
				diagnostics.push(
					createDiagnostic(
						"INVALID_VALUE",
						"error",
						`${property} exceeds the Semantic CSS length limit.`,
						winner.declaration.range,
					),
				);
				continue;
			}
			if (property === "display") {
				hidden = normalized === "none";
				if (hidden) delete style.display;
				else style.display = normalized;
				continue;
			}
			if (property === "order") {
				const nextOrder = typeof normalized === "number" ? normalized : Number(normalized);
				if (!Number.isInteger(nextOrder)) {
					diagnostics.push(
						createDiagnostic("INVALID_VALUE", "error", "order must be an integer.", winner.declaration.range),
					);
				} else order = nextOrder;
				continue;
			}
			if (definition.category === "structural") {
				if (!structuralValue(property, normalized, structural)) {
					diagnostics.push(
						createDiagnostic("INVALID_VALUE", "error", `Invalid ${property} value.`, winner.declaration.range),
					);
				}
				continue;
			}
			specifiedStyleProperties.add(property);
			style[property] = normalized;
		}
		resolved[node.node.key] = {
			style,
			specifiedStyleProperties: [...specifiedStyleProperties],
			hostBaseStyleProperties: [...hostBaseStyleProperties],
			structural,
			hidden,
			order,
		};
	}

	if (diagnostics.some(({ severity }) => severity === "error")) {
		return { nodes: {}, renderTree: tree, diagnostics };
	}
	return { nodes: resolved, renderTree: createRenderTree(tree, flatNodes, resolved), diagnostics };
}
