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

	import { storedLocation, storedModel } from '$lib/stores/settings';

	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,
		isColumnUnit
	} from '$lib/charts';
	import {
		type DaylightBand,
		type ModelCompareResult,
		fetchModelComparison
	} from '$lib/services/weather';

	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 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);
	});

	let params = $state({
		...defaultParameters,
		hourly: ['temperature_2m', 'rain', 'relative_humidity_2m', 'wind_speed_10m'],
		models: ['ecmwf_ifs', 'meteofrance_seamless', 'ukmo_seamless', 'icon_seamless', 'gfs_seamless']
	});

	// ─── 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(() => {
		// the model chosen elsewhere on the site is always part of the comparison
		const selectedModel = get(storedModel);
		if (selectedModel !== 'best_match' && !params.models.includes(selectedModel)) {
			params.models = [selectedModel, ...params.models];
		}
		mounted = true;
	});

	// components persist across refetches; entries are null while unmounted
	let liveCharts = $derived(chartComponents.filter((chart) => chart != 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[];
	}

	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
			});

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

			defs.push({
				title: isFirst ? 'Model Compare' : undefined,
				subtitle: isFirst
					? `Compare ${chartVariables.join(', ') || ''} in models: ${params.models?.join(', ') || ''}`
					: undefined,
				unit,
				showCredit: isLast,
				series
			});
		}

		return defs;
	});
</script>

<!-- ─── 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 -->
<ChartContainer
	{loading}
	chartCount={params.hourly?.filter((v) => v !== 'weather_code').length || 1}
	chartHeight={300}
>
	{#if fetchedData}
		{#each chartDefs as def, i (i)}
			<CanvasChart
				bind:this={chartComponents[i]}
				timestamps={timestampsSec}
				timezone={fetchedData.timezone}
				series={def.series}
				bands={fetchedData.daylightBands}
				unit={def.unit}
				title={def.title}
				subtitle={def.subtitle}
				showCredit={def.showCredit}
				{showLegend}
				height={300}
				group={CHART_GROUP}
			/>
		{/each}
	{/if}
</ChartContainer>

{#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 gap-2">
				<Switch id="show_legend" name="Show legend" bind:checked={showLegend} />
				<Label for="show_legend" class="mb-0.5 cursor-pointer text-lg">Show legend</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">Models</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">
					Hourly Weather Variables
				</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>
