<script lang="ts">
	import { onMount } from 'svelte';
	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 { skeletonOut } from '$lib/utils/skeleton-fade';
	import { listUnlessDefault, readList, syncSearchParams } from '$lib/utils/url-state';

	import { ChartContainer, ChartToolbar } from '$lib/components/charts';
	import { Checkbox } from '$lib/components/ui/checkbox';
	import { Label } from '$lib/components/ui/label';
	import { Switch } from '$lib/components/ui/switch';

	import {
		CHART_COLORS,
		CanvasChart,
		type ChartSeries,
		SERIES_COLORS,
		calculateAverage,
		findUnit,
		groupRange,
		isColumnUnit
	} from '$lib/charts';
	import * as m from '$lib/paraglide/messages';
	import {
		type DaylightBand,
		type ModelCompareResult,
		fetchModelComparison
	} from '$lib/services/weather';

	import { useHeroActions } from '../../hero.svelte';
	import { findModel, hourly, modelGroups } from '../../options';
	import { defaultParameters } from '../../options';
	import ModelPictogramTimeline from './ModelPictogramTimeline.svelte';

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

	const models = modelGroups.map((group) => group.models);

	const CHART_GROUP = 'model-compare';

	// ─── Display State (does NOT trigger data re-fetch) ─────────────────────────

	let showLegend = $state(false);

	// ─── Data Fetch State ───────────────────────────────────────────────────────

	let chartComponents: CanvasChart[] = $state([]);
	let mounted = $state(false);
	let loading = $state(true);
	let loadError = $state<string | null>(null);
	let requestVersion = 0;

	let { data }: { data: PageData } = $props();

	// the page cross-fade waits for this before revealing the new page
	reportPageReady(() => fetchedData != null || loadError != null);

	useHeroActions(heroActions);

	// the URL is the source of truth: location comes from the load function,
	// which is also correct on hydrated prerendered pages. The persisted store
	// only mirrors it so the header and bare /weather/* redirects follow along.
	let location = $derived(data.location);
	$effect(() => {
		storedLocation.set(data.location);
	});

	const DEFAULT_VARS = ['temperature_2m', 'rain', 'wind_speed_10m'];
	const DEFAULT_MODELS = [
		'ecmwf_ifs',
		'meteofrance_seamless',
		'ukmo_seamless',
		'icon_seamless',
		'gfs_seamless'
	];

	let params = $state({
		...defaultParameters,
		hourly: [...DEFAULT_VARS],
		models: [...DEFAULT_MODELS]
	});

	// units live in a persisted store; mirror them into params so a change
	// re-runs the fetch effect (which reads params.*_unit)
	$effect(() => {
		params.temperature_unit = $storedUnits.temperature_unit;
		params.wind_speed_unit = $storedUnits.wind_speed_unit;
		params.precipitation_unit = $storedUnits.precipitation_unit;
	});

	// ─── Cached API Response ────────────────────────────────────────────────────

	interface FetchedData {
		hourly: Record<string, unknown>;
		hourly_units: Record<string, string>;
		timezone: string;
		daylightBands: DaylightBand[];
		timestamps: number[];
		sunrise: number[];
		sunset: number[];
	}

	let fetchedData: FetchedData | null = $state(null);

	// ─── Lifecycle ──────────────────────────────────────────────────────────────

	onMount(() => {
		// A link carries the exact comparison it was shared with; without one, the
		// model chosen elsewhere on the site joins the default line-up.
		const url = get(page).url;
		const urlModels = readList(url, 'models');
		const urlVars = readList(url, 'vars');

		if (urlModels) params.models = urlModels;
		else {
			const selectedModel = get(storedModel);
			if (selectedModel !== 'best_match' && !params.models.includes(selectedModel)) {
				params.models = [selectedModel, ...params.models];
			}
		}
		if (urlVars) params.hourly = urlVars;
		mounted = true;
	});

	// Mirror the comparison back into the URL so it can be shared or reloaded.
	$effect(() => {
		const models = params.models;
		const vars = params.hourly;
		if (!mounted) return;
		syncSearchParams(get(page).url, {
			models: listUnlessDefault(models, DEFAULT_MODELS),
			vars: listUnlessDefault(vars, DEFAULT_VARS)
		});
	});

	// components persist across refetches; entries are null while unmounted
	let liveCharts = $derived(chartComponents.filter((chart) => chart != null));

	// ─── Zoom range controls (mirrors the 7-day meteograms) ─────────────────────

	const SECONDS_PER_DAY = 24 * 3600;

	function dayStartSec(day: Date): number | null {
		if (!fetchedData) return null;
		const tz = fetchedData.timezone;
		const target = formatZoned(day, tz, 'yyyy-MM-dd');
		const idx = fetchedData.timestamps.findIndex(
			(t) => formatZoned(new Date(t), tz, 'yyyy-MM-dd') === target
		);
		return idx === -1 ? null : fetchedData.timestamps[idx] / 1000;
	}

	function setRangeDays(from: Date, days: number): void {
		const start = dayStartSec(from);
		if (start == null || liveCharts.length === 0) return;
		// charts share the group, so setting the range on one syncs all of them
		liveCharts[0].setRange(start, start + days * SECONDS_PER_DAY);
	}

	function resetZoom(): void {
		liveCharts[0]?.resetRange();
	}

	const rangePresets = [
		{ label: 'Today', apply: () => setRangeDays(new Date(), 1) },
		{ label: '3 days', apply: () => setRangeDays(new Date(), 3) },
		{ label: '5 days', apply: () => setRangeDays(new Date(), 5) },
		{ label: 'All', apply: () => resetZoom() }
	];

	let zoomActive = $derived(groupRange(CHART_GROUP) != null);

	// ─── Data Fetching (only when params.hourly or params.models change) ───────

	$effect(() => {
		const hourlyVars = params.hourly;
		const modelList = params.models;

		if (!mounted || !hourlyVars?.length || !modelList?.length) return;

		const loc = location;

		// versioned so a slow stale response can never overwrite a newer one
		const version = ++requestVersion;
		loading = true;
		loadError = null;

		fetchModelComparison({
			latitude: loc.latitude!,
			longitude: loc.longitude!,
			hourlyVariables: [...new Set([...hourlyVars, 'weather_code'])],
			models: modelList,
			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',
			timezone: loc.timezone
		})
			.then((result: ModelCompareResult) => {
				if (version !== requestVersion) return;

				fetchedData = {
					hourly: result.hourlyFlat,
					hourly_units: result.hourlyUnitsFlat,
					timezone: result.timezone,
					daylightBands: result.daylightBands,
					timestamps: result.timestamps,
					sunrise: result.sunrise,
					sunset: result.sunset
				};

				loading = false;
			})
			.catch((err: unknown) => {
				if (version !== requestVersion) return;
				loadError = err instanceof Error ? err.message : String(err);
				loading = false;
			});
	});

	// ─── Chart Building (runs when fetchedData or the variable list changes) ────

	// Timestamps from the service are in milliseconds; CanvasChart uses seconds.
	let timestampsSec = $derived.by(() =>
		fetchedData ? fetchedData.timestamps.map((t) => t / 1000) : []
	);

	interface ChartDef {
		title?: string;
		subtitle?: string;
		unit: string;
		showCredit: boolean;
		series: ChartSeries[];
	}

	// Human labels for the compared variables (API names → readable title)
	const VAR_LABELS: Record<string, string> = {
		temperature_2m: 'Temperature',
		apparent_temperature: 'Feels like',
		dew_point_2m: 'Dew point',
		precipitation: 'Precipitation',
		rain: 'Rain',
		showers: 'Showers',
		snowfall: 'Snowfall',
		wind_speed_10m: 'Wind speed',
		wind_gusts_10m: 'Wind gusts',
		relative_humidity_2m: 'Relative humidity',
		cloud_cover: 'Cloud cover',
		pressure_msl: 'Pressure (MSL)'
	};
	const varLabel = (v: string): string =>
		VAR_LABELS[v] ?? v.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());

	let chartDefs = $derived.by((): ChartDef[] => {
		if (!fetchedData) return [];

		const { hourly: hourlyData, hourly_units, timestamps } = fetchedData;
		const chartVariables = params.hourly?.filter((v) => v !== 'weather_code') || [];
		const variableCount = chartVariables.length;
		const timeLength = timestamps.length;
		const defs: ChartDef[] = [];

		for (let vi = 0; vi < variableCount; vi++) {
			const variable = chartVariables[vi];
			const unit = findUnit(hourly_units, hourlyData, variable);
			const isColumn = isColumnUnit(unit);

			const series: ChartSeries[] = [];

			let modelIndex = 0;
			for (const [model, values] of Object.entries(hourlyData)) {
				if (model === 'time') continue;
				if (!model.startsWith(variable)) continue;

				// strip the variable prefix and use the concise model label so the
				// tooltip/legend stay readable (model ids are very long)
				const modelId = model.slice(variable.length + 1);
				series.push({
					name: findModel(modelId)?.label ?? modelId,
					type: isColumn ? 'bar' : 'line',
					color: SERIES_COLORS[modelIndex % SERIES_COLORS.length],
					data: values as (number | null)[],
					width: 2
				});
				modelIndex++;
			}

			const { average } = calculateAverage(hourlyData, variable, timeLength);
			series.push({
				name: 'Average',
				type: isColumn ? 'bar' : 'line',
				color: CHART_COLORS.average,
				data: average,
				width: 4,
				dashed: !isColumn,
				outline: !isColumn
			});

			const isFirst = vi === 0;
			const isLast = vi === variableCount - 1;

			defs.push({
				// label every chart with its variable so each is identifiable
				title: varLabel(variable),
				subtitle: isFirst
					? `${params.models?.length ?? 0} models · dashed = average`
					: `across ${params.models?.length ?? 0} models`,
				unit,
				showCredit: isLast,
				series
			});
		}

		return defs;
	});
</script>

<!-- the zoom controls ride in the layout's location row (see weather/+layout) -->
{#snippet heroActions()}
	<!-- Range / zoom controls, aligned with the title like the other pages -->
	<div class="lg:absolute lg:right-0 lg:top-20 z-40 flex flex-wrap items-center gap-3">
		<span class="hidden text-xs text-muted-foreground lg:inline">
			{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>
		{#if zoomActive}
			<button
				type="button"
				class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-primary/50 bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary/15"
				onclick={resetZoom}
			>
				<svg
					class="h-3.5 w-3.5"
					fill="none"
					stroke="currentColor"
					viewBox="0 0 24 24"
					stroke-width="2"
				>
					<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v6h6M20 20v-6h-6" />
					<path stroke-linecap="round" d="M20 10a8 8 0 0 0-14-4M4 14a8 8 0 0 0 14 4" />
				</svg>
				{m.reset_zoom()}
			</button>
		{/if}
		<div
			class="inline-flex items-center rounded-lg bg-muted p-0.5 text-xs font-semibold"
			role="group"
			aria-label={m.range_group_aria()}
		>
			{#each rangePresets as preset (preset.label)}
				<button
					type="button"
					class="cursor-pointer rounded-md px-2.5 py-1 whitespace-nowrap text-muted-foreground transition-colors hover:bg-background hover:text-foreground hover:shadow-sm"
					onclick={preset.apply}
				>
					{preset.label}
				</button>
			{/each}
		</div>
	</div>
{/snippet}

<!-- ─── Chart Area ─────────────────────────────────────────────────────────── -->

{#if loadError}
	<div
		class="mb-4 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"
	>
		Failed to load weather data: {loadError}
	</div>
{/if}

<!-- chart count derives from the selected variables (not the fetched data),
     so the reserved height is right even before the response arrives -->
<!-- `relative` lets the placeholder dissolve over the finished charts (skeletonOut) -->
<div class="relative">
	{#if fetchedData}
		<!-- full-bleed graphs until lg / contained card on lg+; titles stay within
		     the page margins (padded), the graphs bleed to the edges -->
		<div class="-mx-3 border-y border-border/70 bg-card shadow-sm lg:mx-0 lg:rounded-2xl lg:border">
			{#each chartDefs as def, i (i)}
				<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">
						<h4 class="text-sm font-bold tracking-tight">{def.title}</h4>
						{#if def.subtitle}
							<p class="text-xs text-muted-foreground">{def.subtitle}</p>
						{/if}
					</div>
					<ChartContainer {loading} chartCount={1} chartHeight={300} minWidth={520} bleed={false}>
						<CanvasChart
							bind:this={chartComponents[i]}
							timestamps={timestampsSec}
							timezone={fetchedData.timezone}
							series={def.series}
							bands={fetchedData.daylightBands}
							unit={def.unit}
							showCredit={def.showCredit}
							{showLegend}
							height={300}
							group={CHART_GROUP}
						/>
					</ChartContainer>
				</div>
			{/each}
		</div>
	{:else}
		<!-- reserve the chart area height before data arrives (no layout shift) -->
		<div in:fade={{ duration: 200 }} out:skeletonOut>
			<ChartContainer
				loading
				chartCount={params.hourly?.filter((v) => v !== 'weather_code').length || 1}
				chartHeight={340}
				bleed={false}
			/>
		</div>
	{/if}
</div>

{#if fetchedData && !loading}
	<ModelPictogramTimeline
		timestamps={fetchedData.timestamps}
		hourlyFlat={fetchedData.hourly as Record<string, number[]>}
		models={params.models || []}
		sunrise={fetchedData.sunrise}
		sunset={fetchedData.sunset}
		timezone={fetchedData.timezone}
	/>
{/if}

<!-- ─── Toolbar: Controls + Download ───────────────────────────────────────── -->

<div class="mt-6 md:mt-10">
	<ChartToolbar charts={liveCharts} fileName="model-comparison">
		{#snippet controls()}
			<div class="flex items-center gap-2">
				<Switch id="show_legend" name="Show legend" bind:checked={showLegend} />
				<Label for="show_legend" class="cursor-pointer text-base leading-none"
					>{m.legend_show()}</Label
				>
			</div>
		{/snippet}
	</ChartToolbar>
</div>

<!-- ─── Models Selection ───────────────────────────────────────────────────── -->

<div class="mt-4 md:mt-8">
	<div class="flex">
		<a href="#models"
			><h2 id="models" class="text-2xl md:text-3xl">{m.compare_models_heading()}</h2></a
		>
		{#if params.models && params.models.length > 0}
			<div transition:fade={{ duration: 200 }} class="relative mt-1.25">
				<div
					class="absolute -top-1 ml-2 rounded-full border-2 border-foreground/25 bg-secondary px-3 py-1 text-sm no-underline"
				>
					{params.models?.length || 0}&nbsp;/&nbsp;{models.flat().length}
				</div>
			</div>
		{/if}
	</div>

	<div class="mt-2 grid sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
		{#each models as group, i (i)}
			<div class="mb-3">
				{#each group as item (item.value)}
					{@const { value, label } = item as { value: string; label: string }}
					<div class="group flex items-center" title={label}>
						<Checkbox
							id="{value}_model"
							class="border-border-dark cursor-pointer bg-muted/50 duration-100 group-hover:border-[currentColor]"
							{value}
							checked={params.models?.includes(value)}
							aria-labelledby="{value}_label"
							onCheckedChange={() => {
								if (params.models?.includes(value)) {
									params.models = params.models.filter((item) => {
										return item !== value;
									});
								} else if (params.models) {
									params.models = [...params.models, value];
								}
							}}
						/>
						<Label
							id="{value}_model_label"
							for="{value}_model"
							class="cursor-pointer truncate py-[0.1rem] pl-[0.42rem]">{label}</Label
						>
					</div>
				{/each}
			</div>
		{/each}
	</div>

	<!-- ─── Hourly Variables Selection ─────────────────────────────────────── -->

	<div class="mt-6 md:mt-12">
		<div class="flex">
			<a href="#hourly_weather_variables"
				><h2 id="hourly_weather_variables" class="text-2xl md:text-3xl">
					{m.compare_variables_heading()}
				</h2></a
			>
			{#if params.hourly && params.hourly.length > 0}
				<div transition:fade={{ duration: 200 }} class="relative mt-1.25">
					<div
						class="absolute -top-1 ml-2 rounded-full border-2 border-foreground/25 bg-secondary px-3 py-1 text-sm no-underline"
					>
						{params.hourly?.length || 0}&nbsp;/&nbsp;{hourly.flat().length}
					</div>
				</div>
			{/if}
		</div>

		<div
			class="mt-2 grid grid-flow-row gap-x-2 gap-y-2 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4"
		>
			{#each hourly as group, i (i)}
				<div>
					{#each group as { value, label } (value)}
						<div class="group flex items-center" title={label}>
							<Checkbox
								id="{value}_hourly"
								class="border-border-dark cursor-pointer bg-muted/50 duration-100 group-hover:border-[currentColor]"
								{value}
								checked={params.hourly?.includes(value)}
								aria-labelledby="{value}_label"
								onCheckedChange={() => {
									if (params.hourly?.includes(value)) {
										params.hourly = params.hourly.filter((item) => {
											return item !== value;
										});
									} else if (params.hourly) {
										params.hourly = [...params.hourly, value];
									}
								}}
							/>
							<Label
								id="{value}_label"
								for="{value}_hourly"
								class="cursor-pointer truncate py-[0.1rem] pl-[0.42rem]">{label}</Label
							>
						</div>
					{/each}
				</div>
			{/each}
		</div>
	</div>
</div>
