<script lang="ts">
	import { onDestroy, onMount } from 'svelte';
	import { SvelteSet } from 'svelte/reactivity';
	import { get } from 'svelte/store';
	import { fade } from 'svelte/transition';

	import { page } from '$app/stores';

	import { reportPageReady } from '$lib/stores/page-transition.svelte';
	import { storedLocation, storedModel, storedUnits } from '$lib/stores/settings';

	import { formatZoned } from '$lib/utils/date';
	import { readList, syncSearchParams } from '$lib/utils/url-state';

	import {
		ChartContainer,
		ChartToolbar,
		type ExportableChart,
		type ExportLegendItem
	} from '$lib/components/charts';
	import { Label } from '$lib/components/ui/label';
	import { Switch } from '$lib/components/ui/switch';

	import {
		CHART_COLORS,
		CanvasChart,
		type ChartAgreementStrip,
		type ChartSeries,
		groupRange,
		isColumnUnit,
		setGroupRange
	} from '$lib/charts';
	import * as m from '$lib/paraglide/messages';
	import {
		type FriendlyWeatherError,
		type ModelCompareResult,
		fetchModelComparison,
		humanizeWeatherError
	} from '$lib/services/weather';

	import { useHeroActions } from '../../hero.svelte';
	import { defaultParameters, hourly, modelGroups, models as modelOptions } from '../../options';
	import ComparisonSelectionDrawer from './ComparisonSelectionDrawer.svelte';
	import ModelPictogramTimeline from './ModelPictogramTimeline.svelte';
	import {
		cardinalDirection,
		isWindDirection,
		modelColor,
		modelLabel,
		modelMean,
		precipitationAgreement,
		sanitizeList,
		validDirectionValues
	} from './comparison';

	import type { PageData } from './$types';

	const CHART_GROUP = 'model-compare';
	const MODEL_IDS = new Set(modelOptions.map((model) => model.value));
	const MODEL_ORDER = new Map(modelOptions.map((model, index) => [model.value, index]));
	const AUTOMATIC_SEAMLESS_MODEL_IDS = new Set(
		modelGroups.find((group) => group.value === 'automatic_seamless')?.models.map((model) => model.value)
	);
	// Weather code and total cloud cover form one comparison feature. Cloud cover
	// remains an auxiliary API field for pictogram shading, not a standalone chart.
	const COMPARISON_HOURLY = hourly.map((group) =>
		group.filter((variable) => variable.value !== 'cloud_cover')
	);
	const VARIABLE_IDS = new Set(COMPARISON_HOURLY.flat().map((variable) => variable.value));
	const VARIABLE_ORDER = new Map(
		COMPARISON_HOURLY.flat().map((variable, index) => [variable.value, index])
	);
	const SECONDS_PER_HOUR = 3600;
	const FETCH_DEBOUNCE_MS = 300;
	const STANDARD_COMPARE_VARIABLES = [
		'temperature_2m',
		'precipitation',
		'wind_speed_10m',
		'wind_direction_10m',
		'weather_code',
		'relative_humidity_2m'
	];

	let { data }: { data: PageData } = $props();
	let location = $derived(data.location);
	let mounted = $state(false);
	let loading = $state(true);
	let refreshing = $state(false);
	let loadError = $state<FriendlyWeatherError | null>(null);
	let retryNonce = $state(0);
	let requestVersion = 0;
	let activeController: AbortController | null = null;
	let chartComponents: CanvasChart[] = $state([]);
	let pictogramExporter: ExportableChart | null = $state(null);
	let showLegend = $state(true);
	const hiddenModels = new SvelteSet<string>();

	let isMobile = $state(false);
	let comparisonPlotInsetLeft = $derived(isMobile ? 92 : 116);
	let comparisonPlotInsetRight = $derived(isMobile ? 8 : 20);
	let selectionDrawerMode = $state<'models' | 'variables' | null>(null);

	let params = $state({
		...defaultParameters,
		hourly: [...STANDARD_COMPARE_VARIABLES],
		models: orderModels([
			'ecmwf_ifs',
			'meteofrance_arpege_world',
			'ukmo_global_deterministic_10km',
			'icon_global',
			'gfs_global'
		])
	});
	let appliedHourly = $state<string[]>([...STANDARD_COMPARE_VARIABLES]);
	let appliedModels = $state<string[]>([...params.models]);

	interface FetchedData {
		result: ModelCompareResult;
		selection: { models: string[]; hourly: string[] };
	}

	let fetchedData: FetchedData | null = $state(null);
	let liveCharts = $derived(chartComponents.filter((chart) => chart != null));
	let selectionEmpty = $derived(appliedModels.length === 0 || appliedHourly.length === 0);
	let selectedModelWarning = $derived(params.models.length > 10);

	function hasSameVariables(left: string[], right: string[]): boolean {
		return left.length === right.length && left.every((variable) => right.includes(variable));
	}

	function hasSameOrder(left: string[], right: string[]): boolean {
		return left.length === right.length && left.every((value, index) => value === right[index]);
	}

	function orderModels(values: string[]): string[] {
		return [...values].sort(
			(left, right) =>
				(MODEL_ORDER.get(left) ?? Number.MAX_SAFE_INTEGER) -
				(MODEL_ORDER.get(right) ?? Number.MAX_SAFE_INTEGER)
		);
	}

	function orderVariables(values: string[]): string[] {
		return [...values].sort(
			(left, right) =>
				(VARIABLE_ORDER.get(left) ?? Number.MAX_SAFE_INTEGER) -
				(VARIABLE_ORDER.get(right) ?? Number.MAX_SAFE_INTEGER)
		);
	}

	let variablesDirty = $derived(!hasSameVariables(params.hourly, appliedHourly));
	let modelsDirty = $derived(!hasSameOrder(params.models, appliedModels));
	let displayedVariablesStale = $derived(
		fetchedData !== null && !hasSameVariables(fetchedData.selection.hourly, appliedHourly)
	);
	let displayedModelsStale = $derived(
		fetchedData !== null && !hasSameOrder(fetchedData.selection.models, appliedModels)
	);
	let comparisonSelectionDirty = $derived(variablesDirty || modelsDirty);
	let comparisonMuted = $derived(
		comparisonSelectionDirty || displayedVariablesStale || displayedModelsStale
	);

	reportPageReady(() => mounted && !loading);
	useHeroActions(heroActions);

	$effect(() => storedLocation.set(data.location));
	$effect(() => {
		params.temperature_unit = $storedUnits.temperature_unit;
		params.wind_speed_unit = $storedUnits.wind_speed_unit;
		params.precipitation_unit = $storedUnits.precipitation_unit;
	});

	onMount(() => {
		const url = get(page).url;
		const urlModels = sanitizeList(readList(url, 'models'), MODEL_IDS);
		const urlVars = sanitizeList(readList(url, 'vars'), VARIABLE_IDS);
		if (urlModels) params.models = orderModels(urlModels);
		else {
			const selectedModel = get(storedModel);
			if (
				selectedModel !== 'best_match' &&
				!AUTOMATIC_SEAMLESS_MODEL_IDS.has(selectedModel) &&
				MODEL_IDS.has(selectedModel) &&
				!params.models.includes(selectedModel)
			) {
				params.models = orderModels([...params.models, selectedModel]);
			}
		}
		appliedModels = [...params.models];
		if (urlVars) {
			params.hourly = urlVars;
			appliedHourly = [...urlVars];
		}

		const media = window.matchMedia('(max-width: 639px)');
		const applyMedia = () => {
			isMobile = media.matches;
		};
		applyMedia();
		media.addEventListener('change', applyMedia);
		mounted = true;
		return () => media.removeEventListener('change', applyMedia);
	});

	onDestroy(() => activeController?.abort());

	$effect(() => {
		const models = appliedModels;
		const vars = appliedHourly;
		if (!mounted) return;
		syncSearchParams(get(page).url, {
			models: models.length ? models.join(',') : null,
			vars: vars.length ? vars.join(',') : null
		});
	});

	$effect(() => {
		const modelList = [...appliedModels];
		const hourlyVars = [...appliedHourly];
		const loc = location;
		const units = {
			temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit',
			wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn',
			precipitation_unit: params.precipitation_unit as 'mm' | 'inch'
		};
		void retryNonce;
		if (!mounted) return;

		const version = ++requestVersion;
		activeController?.abort();
		activeController = null;

		if (modelList.length === 0 || hourlyVars.length === 0) {
			fetchedData = null;
			loading = false;
			refreshing = false;
			loadError = null;
			return;
		}

		loading = fetchedData === null;
		refreshing = fetchedData !== null;
		loadError = null;
		const timer = window.setTimeout(() => {
			const controller = new AbortController();
			activeController = controller;
			fetchModelComparison(
				{
					latitude: loc.latitude,
					longitude: loc.longitude,
					hourlyVariables: [
						...new Set([
							...hourlyVars,
							...(hourlyVars.includes('weather_code') ? ['cloud_cover'] : [])
						])
					],
					models: modelList,
					...units,
					timezone: loc.timezone
				},
				{ signal: controller.signal }
			)
				.then((result) => {
					if (version !== requestVersion) return;
					fetchedData = { result, selection: { models: modelList, hourly: hourlyVars } };
					loading = false;
					refreshing = false;
				})
				.catch((error: unknown) => {
					if (version !== requestVersion || controller.signal.aborted) return;
					loadError = humanizeWeatherError(error);
					loading = false;
					refreshing = false;
				});
		}, FETCH_DEBOUNCE_MS);

		return () => window.clearTimeout(timer);
	});

	// ─── Chart ranges ────────────────────────────────────────────────────────────

	function setRangeDays(days: number): void {
		if (!fetchedData) return;
		const { timestamps, timezone } = fetchedData.result;
		const today = formatZoned(new Date(), timezone, 'yyyy-MM-dd');
		const startIndex = timestamps.findIndex(
			(timestamp) => formatZoned(new Date(timestamp), timezone, 'yyyy-MM-dd') === today
		);
		if (startIndex === -1) return;
		const dayKeys: string[] = [];
		let endIndex = timestamps.length;
		for (let i = startIndex; i < timestamps.length; i++) {
			const key = formatZoned(new Date(timestamps[i]), timezone, 'yyyy-MM-dd');
			if (!dayKeys.includes(key)) dayKeys.push(key);
			if (dayKeys.length > days) {
				endIndex = i;
				break;
			}
		}
		const start = timestamps[startIndex] / 1000;
		const end =
			endIndex < timestamps.length
				? timestamps[endIndex] / 1000
				: timestamps[timestamps.length - 1] / 1000 + SECONDS_PER_HOUR;
		if (liveCharts[0]) liveCharts[0].setRange(start, end);
		else setGroupRange(CHART_GROUP, { start, end });
	}

	function resetZoom(): void {
		if (liveCharts[0]) liveCharts[0].resetRange();
		else setGroupRange(CHART_GROUP, null);
	}

	const rangePresets = [
		{ label: m.range_today, apply: () => setRangeDays(1) },
		{ label: m.range_3_days, apply: () => setRangeDays(3) },
		{ label: m.range_5_days, apply: () => setRangeDays(5) },
		{ label: m.range_all, apply: resetZoom }
	];
	let zoomActive = $derived(groupRange(CHART_GROUP) != null);

	// ─── Chart definitions ──────────────────────────────────────────────────────

	const variableLabel = (variable: string): string => {
		const labels: Record<string, () => string> = {
			temperature_2m: m.var_temperature,
			apparent_temperature: m.var_apparent,
			dew_point_2m: m.var_dew_point,
			precipitation: m.var_precipitation,
			precipitation_probability: m.var_pop,
			rain: m.var_rain,
			showers: m.var_showers,
			snowfall: m.var_snowfall,
			wind_speed_10m: m.var_wind,
			wind_gusts_10m: m.var_gusts,
			relative_humidity_2m: m.var_humidity,
			cloud_cover: m.var_cloud,
			cloud_cover_low: m.var_cloud_low,
			cloud_cover_mid: m.var_cloud_mid,
			cloud_cover_high: m.var_cloud_high,
			weather_code: m.compare_weather_conditions,
			pressure_msl: m.var_pressure,
			surface_pressure: m.var_surface_pressure
		};
		if (isWindDirection(variable)) {
			const height = variable.match(/_(\d+)m$/)?.[1] ?? '10';
			return m.compare_wind_direction_height({ height });
		}
		return (
			labels[variable]?.() ?? variable.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
		);
	};

	interface ChartDef {
		title: string;
		subtitle: string;
		unit: string;
		series: ChartSeries[];
		zeroBaseLeft: boolean;
		yMin?: number;
		yMax?: number;
		yTicks?: number[];
		yTickFormat?: (value: number) => string;
		agreementStrip?: ChartAgreementStrip;
	}

	let chartDefs = $derived.by((): ChartDef[] => {
		if (!fetchedData) return [];
		const { result, selection } = fetchedData;
		const variables = selection.hourly.filter(
			(variable) => variable !== 'weather_code' && variable !== 'cloud_cover'
		);
		return variables.map((variable) => {
			const direction = isWindDirection(variable);
			const unit = result.units[variable] ?? '';
			const column = !direction && isColumnUnit(unit);
			const series: ChartSeries[] = result.models.map((model) => ({
				name: modelLabel(model.modelId),
				type: direction ? 'point' : column ? 'bar' : 'line',
				color: modelColor(model.modelId, selection.models),
				data: direction
					? validDirectionValues(model.variables[variable])
					: (model.variables[variable] ?? []),
				width: 2,
				pointRadius: 3.25,
				hidden: hiddenModels.has(model.modelId),
				format: direction
					? (value) => `${value.toFixed(0)}° ${cardinalDirection(value)}`
					: undefined
			}));

			if (!direction && variable !== 'precipitation') {
				series.push({
					name: m.compare_model_mean(),
					type: column ? 'bar' : 'line',
					color: CHART_COLORS.average,
					data: modelMean(result.models, variable, result.timestamps.length),
					width: 4,
					dashed: !column,
					outline: !column
				});
			}

			const percentage = [
				'relative_humidity_2m',
				'cloud_cover',
				'cloud_cover_low',
				'cloud_cover_mid',
				'cloud_cover_high',
				'precipitation_probability'
			].includes(variable);
			const floating = [
				'pressure_msl',
				'surface_pressure',
				'temperature_2m',
				'dew_point_2m'
			].includes(variable);
			const amountDecimals = unit === 'inch' || unit === 'in' ? 3 : 1;
			const wetThreshold = unit === 'inch' || unit === 'in' ? 0.1 / 25.4 : 0.1;
			const agreementStrip: ChartAgreementStrip | undefined =
				variable === 'precipitation'
					? {
							points: precipitationAgreement(
								result.models.filter((model) => !hiddenModels.has(model.modelId)),
								variable,
								result.timestamps.length,
								wetThreshold
							),
							label: m.compare_precipitation_agreement(),
							format: (point) =>
								m.compare_precipitation_agreement_tooltip({
									wet: point.wetCount,
									available: point.availableCount,
									median: point.median.toFixed(amountDecimals),
									min: point.min.toFixed(amountDecimals),
									max: point.max.toFixed(amountDecimals),
									unit
								})
						}
					: undefined;
			return {
				title: variableLabel(variable),
				subtitle:
					variable === 'precipitation'
						? m.compare_precipitation_subtitle({ count: result.models.length })
						: direction
							? m.compare_direction_subtitle({ count: result.models.length })
							: m.compare_scalar_subtitle({ count: result.models.length }),
				unit,
				series,
				agreementStrip,
				zeroBaseLeft: !floating,
				yMin: direction || percentage ? 0 : undefined,
				yMax: direction ? 360 : percentage ? 100 : undefined,
				yTicks: direction ? [0, 90, 180, 270, 360] : undefined,
				yTickFormat: direction
					? (value) => `${cardinalDirection(value)} ${value.toFixed(0)}°`
					: undefined
			};
		});
	});

	let timestampsSec = $derived.by((): number[] => {
		const current: FetchedData | null = fetchedData;
		return current ? current.result.timestamps.map((timestamp) => timestamp / 1000) : [];
	});
	let displayedModels = $derived.by(() => {
		const current: FetchedData | null = fetchedData;
		return current?.result.models ?? [];
	});
	let chartVariablesEmpty = $derived.by(() => {
		const current: FetchedData | null = fetchedData;
		return (
			current?.selection.hourly.every(
				(variable) => variable === 'weather_code' || variable === 'cloud_cover'
			) ?? false
		);
	});
	let noUsableData = $derived.by(() => {
		if (!fetchedData || loading) return false;
		return !fetchedData.result.models.some((model) =>
			Object.values(model.variables).some((values) =>
				values.some((value) => Number.isFinite(value))
			)
		);
	});

	let exportLegend = $derived.by((): ExportLegendItem[] => [
		...displayedModels
			.filter((model) => !hiddenModels.has(model.modelId))
			.map((model) => ({
				name: modelLabel(model.modelId),
				color: modelColor(model.modelId, fetchedData?.selection.models),
				style: 'point' as const
			})),
		...(chartDefs.some((def) =>
			def.series.some((series) => series.name === m.compare_model_mean())
		)
			? [{ name: m.compare_model_mean(), color: CHART_COLORS.average, style: 'dashed' as const }]
			: [])
	]);

	function toggleModel(modelId: string): void {
		if (hiddenModels.has(modelId)) hiddenModels.delete(modelId);
		else hiddenModels.add(modelId);
	}

	function toggleSelection(list: 'models' | 'hourly', value: string): void {
		const values = params[list];
		const nextValues = values.includes(value)
			? values.filter((item) => item !== value)
			: [...values, value];
		params[list] = list === 'models' ? orderModels(nextValues) : orderVariables(nextValues);
	}

	function toggleGroupSelection(
		list: 'models' | 'hourly',
		values: string[],
		select: boolean
	): void {
		const groupValues = new Set(values);
		const nextValues = select
			? [...new Set([...params[list], ...values])]
			: params[list].filter((value) => !groupValues.has(value));
		params[list] = list === 'models' ? orderModels(nextValues) : orderVariables(nextValues);
	}

	function restoreStandardVariables(): void {
		params.hourly = [...STANDARD_COMPARE_VARIABLES];
	}

	function applyComparisonSelection(): void {
		const variablesChanged = !hasSameVariables(params.hourly, appliedHourly);
		const modelsChanged = !hasSameOrder(params.models, appliedModels);
		if (modelsChanged) {
			for (const modelId of params.models) {
				if (!appliedModels.includes(modelId)) hiddenModels.delete(modelId);
			}
			appliedModels = [...params.models];
		}
		if (variablesChanged) appliedHourly = [...params.hourly];
		if (!variablesChanged && !modelsChanged) retryNonce++;
	}

	const variableGroupLabels = [
		m.compare_group_temperature,
		m.compare_group_precipitation,
		m.compare_group_clouds,
		m.compare_group_wind,
		m.compare_group_upper_air
	];
	const allHourlyVariables = COMPARISON_HOURLY.flat();
	let selectedModels = $derived(
		params.models.flatMap((value) => {
			const model = modelOptions.find((candidate) => candidate.value === value);
			return model ? [model] : [];
		})
	);
	let modelDrawerGroups = $derived(
		modelGroups.map((group) => ({
			value: group.value,
			label: group.label,
			options: group.models.map((model) => ({
				value: model.value,
				label: model.label,
				metadata: [model.resolution, model.update].filter(Boolean).join(' · ')
			}))
		}))
	);
	let selectedVariables = $derived(
		params.hourly.flatMap((value) => {
			const variable = allHourlyVariables.find((candidate) => candidate.value === value);
			return variable ? [variable] : [];
		})
	);
	let standardVariablesActive = $derived(
		params.hourly.length === STANDARD_COMPARE_VARIABLES.length &&
			STANDARD_COMPARE_VARIABLES.every((variable) => params.hourly.includes(variable))
	);
	let variableDrawerGroups = $derived(
		COMPARISON_HOURLY.map((variables, index) => ({
			value: `variables_${index}`,
			label: variableGroupLabels[index](),
			options: variables.map((variable) => ({
				value: variable.value,
				label: variableLabel(variable.value)
			}))
		}))
	);
</script>

<svelte:head>
	<title>{m.page_compare_title()} | Drizz.li</title>
	<link rel="canonical" href="https://drizz.li/weather/compare" />
	<meta name="description" content={m.page_compare_description()} />
</svelte:head>

{#snippet rangeControls(compact = false)}
	<div class="flex min-w-max items-center gap-2" aria-label={m.range_group_aria()}>
		{#if zoomActive}
			<button
				type="button"
				class="rounded-lg border border-primary/50 bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary"
				onclick={resetZoom}>{m.reset_zoom()}</button
			>
		{/if}
		<div class="inline-flex rounded-lg bg-muted p-0.5 text-xs font-semibold" role="group">
			{#each rangePresets as preset (preset.label())}
				<button
					type="button"
					class="rounded-md px-2.5 py-1 whitespace-nowrap text-muted-foreground transition-colors hover:bg-background hover:text-foreground hover:shadow-sm {compact
						? 'min-h-9'
						: ''}"
					onclick={preset.apply}>{preset.label()}</button
				>
			{/each}
		</div>
	</div>
{/snippet}

{#snippet heroActions()}
	<div class="lg:absolute lg:right-0 lg:top-20 z-40 hidden items-center gap-3 lg:flex">
		<span class="text-xs text-muted-foreground">
			{m.meteograms_zoom_hint()}
			<kbd class="rounded border border-border bg-muted px-1 py-0.5 font-sans text-[10px]">Ctrl</kbd
			>
			{m.meteograms_zoom_hint_end()}
		</span>
		{@render rangeControls()}
	</div>
{/snippet}

<div class="-mx-3 mb-3 overflow-x-auto px-3 lg:hidden">{@render rangeControls(true)}</div>

{#if loadError}
	<div
		class="mb-4 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm"
		role="alert"
	>
		<p class="font-semibold text-destructive">{loadError.title}</p>
		{#if loadError.hint}<p class="mt-0.5 text-destructive/90">{loadError.hint}</p>{/if}
		{#if fetchedData}<p class="mt-1 text-xs text-destructive/80">
				{m.compare_showing_previous()}
			</p>{/if}
		<button
			type="button"
			class="mt-2 rounded-md border border-destructive/40 bg-background px-3 py-1 text-xs font-semibold text-destructive"
			onclick={() => retryNonce++}>{m.action_try_again()}</button
		>
		{#if loadError.detail}
			<details class="mt-2 text-xs text-destructive/70">
				<summary>{m.error_technical_details()}</summary>
				<p class="mt-1 font-mono break-all">{loadError.detail}</p>
			</details>
		{/if}
	</div>
{/if}

{#if selectionEmpty}
	<div
		class="rounded-xl border border-dashed border-border bg-muted/30 px-4 py-10 text-center text-sm text-muted-foreground"
	>
		{m.compare_empty_selection()}
	</div>
{:else if noUsableData}
	<div
		class="rounded-xl border border-amber-300/60 bg-amber-50 px-4 py-4 text-sm text-amber-800 dark:border-amber-800/50 dark:bg-amber-950/30 dark:text-amber-200"
	>
		{m.compare_no_data()}
	</div>
{:else if loadError && !fetchedData}
	<!-- The actionable error panel above replaces the chart until a retry succeeds. -->
{:else if fetchedData && chartVariablesEmpty}
	<div
		class="rounded-xl border border-border bg-muted/30 px-4 py-6 text-center text-sm text-muted-foreground"
	>
		{m.compare_weather_codes_only()}
	</div>
{:else if fetchedData}
	<div
		class="relative -mx-3 border-y border-border/70 bg-card shadow-sm lg:mx-0 lg:rounded-2xl lg:border"
	>
		<div
			class="transition-[opacity,filter] duration-200 {comparisonMuted
				? 'pointer-events-none opacity-35 saturate-50'
				: ''}"
		>
			{#each chartDefs as def, i (def.title)}
				<div class="px-0 pt-1.5 pb-1 lg:px-4 lg:pb-3 {i > 0 ? 'border-t border-border/50' : ''}">
					<div class="mb-1 px-3 lg:px-0">
						<h2 class="text-sm font-bold tracking-tight">{def.title}</h2>
						<p class="text-xs text-muted-foreground">{def.subtitle}</p>
					</div>
					<ChartContainer
						loading={loading && !fetchedData}
						chartCount={1}
						chartHeight={280}
						minWidth={520}
						bleed={false}
					>
						<CanvasChart
							bind:this={chartComponents[i]}
							timestamps={timestampsSec}
							timezone={fetchedData.result.timezone}
							series={def.series}
							agreementStrip={def.agreementStrip}
							bands={fetchedData.result.daylightBands}
							unit={def.unit}
							zeroBaseLeft={def.zeroBaseLeft}
							yMin={def.yMin}
							yMax={def.yMax}
							yTicks={def.yTicks}
							yTickFormat={def.yTickFormat}
							plotInsetLeft={comparisonPlotInsetLeft}
							plotInsetRight={comparisonPlotInsetRight}
							showCredit={i === chartDefs.length - 1}
							height={280}
							group={CHART_GROUP}
							ariaLabel={`${def.title}. ${def.subtitle}`}
						/>
					</ChartContainer>
				</div>
			{/each}
		</div>
		{#if comparisonMuted}
			<div
				class="absolute inset-0 z-40 flex items-start justify-center bg-background/30 px-4 py-6 backdrop-blur-[1px]"
			>
				<div
					class="sticky top-24 flex flex-col items-center gap-2 rounded-xl border border-border/70 bg-background/95 p-3 text-center shadow-lg"
					role="status"
				>
					<span class="text-xs font-semibold text-muted-foreground">
						{m.compare_selection_pending()}
					</span>
					<button
						type="button"
						class="min-h-10 rounded-lg bg-primary px-4 py-2 text-sm font-bold text-primary-foreground hover:bg-primary/90 disabled:cursor-wait disabled:opacity-70"
						disabled={(displayedVariablesStale || displayedModelsStale) && refreshing}
						onclick={applyComparisonSelection}
					>
						{m.compare_apply_selection()}
					</button>
				</div>
			</div>
		{/if}
	</div>
{:else}
	<ChartContainer
		loading
		chartCount={appliedHourly.filter((v) => v !== 'weather_code' && v !== 'cloud_cover').length || 1}
		chartHeight={300}
		bleed={false}
	/>
{/if}

{#if fetchedData && !loading && appliedHourly.includes('weather_code')}
	<div
		class="transition-[opacity,filter] duration-200 {comparisonMuted
			? 'pointer-events-none opacity-35 saturate-50'
			: ''}"
	>
		<ModelPictogramTimeline
			timestamps={fetchedData.result.timestamps}
			models={fetchedData.result.models}
			modelOrder={fetchedData.selection.models}
			sunrise={fetchedData.result.sunrise}
			sunset={fetchedData.result.sunset}
			timezone={fetchedData.result.timezone}
			group={CHART_GROUP}
			plotInsetLeft={comparisonPlotInsetLeft}
			plotInsetRight={comparisonPlotInsetRight}
			registerExporter={(exporter) => (pictogramExporter = exporter)}
		/>
	</div>
{/if}

{#if fetchedData && displayedModels.length > 0}
	<div
		class="mt-5 rounded-xl border border-border/70 bg-card px-3 py-2.5 transition-opacity {comparisonMuted
			? 'pointer-events-none opacity-40'
			: ''}"
	>
		<div class="flex items-center justify-between gap-3">
			<p class="text-xs font-semibold text-muted-foreground">{m.compare_model_colors()}</p>
			<div class="flex items-center gap-2">
				<Switch id="show_legend" bind:checked={showLegend} />
				<Label for="show_legend" class="text-xs">{m.legend_show()}</Label>
			</div>
		</div>
		{#if showLegend}
			<div
				class="-mx-1 mt-2 flex gap-3 overflow-x-auto px-1 pb-1 sm:flex-wrap"
				transition:fade={{ duration: 150 }}
			>
				{#each displayedModels as model (model.modelId)}
					<button
						type="button"
						class="flex min-h-8 shrink-0 items-center gap-1.5 text-xs {hiddenModels.has(
							model.modelId
						)
							? 'opacity-40'
							: ''}"
						onclick={() => toggleModel(model.modelId)}
						aria-pressed={!hiddenModels.has(model.modelId)}
					>
						<span
							class="size-2.5 rounded-full"
							style:background-color={modelColor(model.modelId, fetchedData.selection.models)}
						></span>
						{modelLabel(model.modelId)}
					</button>
				{/each}
				{#if chartDefs.some( (def) => def.series.some((series) => series.name === m.compare_model_mean()) )}
					<span class="flex shrink-0 items-center gap-1.5 text-xs">
						<span class="w-4 border-t-2 border-dashed" style:border-color={CHART_COLORS.average}
						></span>
						{m.compare_model_mean()}
					</span>
				{/if}
			</div>
		{/if}
	</div>

	<div
		class="mt-4 flex justify-end transition-opacity {comparisonMuted
			? 'pointer-events-none opacity-40'
			: ''}"
	>
		<ChartToolbar
			charts={[
				...chartDefs.map((def, i) => ({ chart: chartComponents[i], title: def.title })),
				{ chart: pictogramExporter, title: m.compare_timeline_title() }
			]}
			fileName="model-comparison"
			exportOptions={{
				title: `${location.name} · ${m.page_compare_subtitle()}`,
				legend: exportLegend
			}}
		/>
	</div>
{/if}

<!-- Compact summaries keep the page scannable; full editing happens in the shared drawer. -->
<section class="mt-6 border-t border-border/60 pt-5 md:mt-10" aria-labelledby="models">
	<div class="flex items-baseline justify-between gap-3">
		<h2 id="models" class="text-2xl md:text-3xl">{m.compare_models_heading()}</h2>
		<span class="shrink-0 text-sm text-muted-foreground">
			{params.models.length}/{modelOptions.length}
		</span>
	</div>
	{#if selectedModelWarning}
		<p
			class="mt-2 rounded-md bg-amber-500/10 px-3 py-2 text-xs text-amber-800 dark:text-amber-200"
			role="status"
		>
			{m.compare_many_models_warning({ count: params.models.length })}
		</p>
	{/if}
	<div class="mt-3 rounded-xl border border-border/70 bg-card p-3 shadow-sm sm:p-4">
		<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
			<div class="min-w-0 flex-1">
				<div class="flex flex-wrap items-center gap-2">
					<h3 class="text-sm font-bold">{m.compare_selected_models()}</h3>
					{#if modelsDirty}
						<span
							class="rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] font-bold tracking-wide text-amber-700 uppercase dark:text-amber-300"
						>
							{m.compare_selection_pending()}
						</span>
					{/if}
				</div>
				{#if selectedModels.length > 0}
					<div class="mt-2 flex flex-wrap gap-1.5">
						{#each selectedModels as model, index (model.value)}
							<button
								type="button"
								class="inline-flex min-h-8 max-w-full items-center gap-1.5 rounded-full border border-border/70 bg-background px-2.5 py-1 text-xs hover:border-destructive/40 hover:bg-destructive/5"
								onclick={() => toggleSelection('models', model.value)}
								aria-label={m.compare_remove_model({ model: model.label })}
							>
								<span class="text-[10px] font-bold text-muted-foreground tabular-nums"
									>{index + 1}</span
								>
								<span
									class="size-2.5 shrink-0 rounded-full"
									style:background-color={modelColor(model.value, params.models)}
								></span>
								<span class="truncate">{model.label}</span>
								<span aria-hidden="true" class="text-muted-foreground">×</span>
							</button>
						{/each}
					</div>
				{:else}
					<p class="mt-1.5 text-xs text-muted-foreground">{m.compare_no_models_selected()}</p>
				{/if}
			</div>
			<div class="flex shrink-0 flex-wrap gap-2 sm:justify-end">
				<button
					type="button"
					class="min-h-9 rounded-lg border border-border px-3 py-1.5 text-xs font-semibold hover:bg-muted"
					onclick={() => (selectionDrawerMode = 'models')}
					aria-haspopup="dialog"
				>
					{m.compare_customize_models()}
				</button>
				{#if modelsDirty}
					<button
						type="button"
						class="min-h-9 rounded-lg bg-primary px-3 py-1.5 text-xs font-bold text-primary-foreground hover:bg-primary/90"
						onclick={applyComparisonSelection}
					>
						{m.compare_apply_selection()}
					</button>
				{/if}
			</div>
		</div>
	</div>
</section>

<section
	class="mt-5 border-t border-border/60 pt-5 md:mt-10"
	aria-labelledby="hourly_weather_variables"
>
	<div class="flex items-baseline justify-between gap-3">
		<h2 id="hourly_weather_variables" class="text-2xl md:text-3xl">
			{m.compare_variables_heading()}
		</h2>
		<span class="shrink-0 text-sm text-muted-foreground">
			{params.hourly.length}/{allHourlyVariables.length}
		</span>
	</div>

	<div
		class="mt-3 rounded-xl border bg-card shadow-sm {standardVariablesActive
			? 'border-primary/35'
			: 'border-border/70'}"
	>
		<div class="flex flex-col gap-3 p-3 sm:flex-row sm:items-start sm:justify-between sm:p-4">
			<div class="flex min-w-0 gap-3">
				<span
					class="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border {standardVariablesActive
						? 'border-primary bg-primary'
						: 'border-muted-foreground/40 bg-muted'}"
					aria-hidden="true"
				>
					{#if standardVariablesActive}<span class="size-2 rounded-full bg-primary-foreground"></span>{/if}
				</span>
				<div class="min-w-0">
					<div class="flex flex-wrap items-center gap-2">
						<h3 class="font-bold">{m.compare_standard_preset()}</h3>
						<span
							class="rounded-full px-2 py-0.5 text-[10px] font-bold tracking-wide uppercase {variablesDirty
								? 'bg-amber-500/15 text-amber-700 dark:text-amber-300'
								: standardVariablesActive
									? 'bg-primary/10 text-primary'
									: 'bg-muted text-muted-foreground'}"
						>
							{variablesDirty
								? m.compare_variables_pending()
								: standardVariablesActive
									? m.compare_preset_active()
									: m.compare_custom_selection({ count: params.hourly.length })}
						</span>
					</div>
					<p class="mt-0.5 text-sm text-muted-foreground">
						{m.compare_standard_preset_description()}
					</p>
					{#if selectedVariables.length > 0}
						<p class="mt-2 text-xs leading-relaxed text-foreground/80">
							{selectedVariables.map((variable) => variableLabel(variable.value)).join(' · ')}
						</p>
					{/if}
				</div>
			</div>
			<div class="flex shrink-0 flex-wrap gap-2 sm:justify-end">
				{#if !standardVariablesActive}
					<button
						type="button"
						class="min-h-9 rounded-lg border border-border px-3 py-1.5 text-xs font-semibold hover:bg-muted"
						onclick={restoreStandardVariables}
					>
						{m.compare_restore_defaults()}
					</button>
				{/if}
				<button
					type="button"
					class="min-h-9 rounded-lg border border-border px-3 py-1.5 text-xs font-semibold hover:bg-muted"
					onclick={() => (selectionDrawerMode = 'variables')}
					aria-haspopup="dialog"
				>
					{m.compare_customize_variables()}
				</button>
				{#if variablesDirty}
					<button
						type="button"
						class="min-h-9 rounded-lg bg-primary px-3 py-1.5 text-xs font-bold text-primary-foreground hover:bg-primary/90"
						onclick={applyComparisonSelection}
					>
						{m.compare_apply_selection()}
					</button>
				{/if}
			</div>
		</div>
	</div>
</section>

{#if selectionDrawerMode === 'models'}
	<ComparisonSelectionDrawer
		open
		mode="models"
		groups={modelDrawerGroups}
		selected={params.models}
		dirty={comparisonSelectionDirty}
		onToggle={(value) => toggleSelection('models', value)}
		onToggleGroup={(values, select) => toggleGroupSelection('models', values, select)}
		onApply={applyComparisonSelection}
		onClose={() => (selectionDrawerMode = null)}
	/>
{:else if selectionDrawerMode === 'variables'}
	<ComparisonSelectionDrawer
		open
		mode="variables"
		groups={variableDrawerGroups}
		selected={params.hourly}
		dirty={comparisonSelectionDirty}
		onToggle={(value) => toggleSelection('hourly', value)}
		onToggleGroup={(values, select) => toggleGroupSelection('hourly', values, select)}
		onApply={applyComparisonSelection}
		onClose={() => (selectionDrawerMode = null)}
		onRestoreDefaults={restoreStandardVariables}
	/>
{/if}
