<!--
  System resources panel: host capacity + workload usage chart + disk
  breakdown + top consumers. Drops into the dashboard as its own section.
-->
<script lang="ts">
	import type { SystemStats, SystemStatsSample, TopContainerSample } from '$lib/types';
	import * as api from '$lib/api';
	import ResourceChart from './ResourceChart.svelte';
	import type { EChartsOption } from 'echarts';
	import { t } from '$lib/i18n';
	import { formatBytes } from '$lib/format/bytes';
	import { statsInterval, ensureStatsIntervalLoaded } from '$lib/stores/statsInterval';

	let current = $state<SystemStats | null>(null);
	let history = $state<SystemStatsSample[]>([]);
	let top = $state<TopContainerSample[]>([]);
	let topBy = $state<'cpu' | 'memory'>('cpu');
	let historyWindow = $state<'30m' | '2h' | '6h' | '24h'>('2h');
	let dockerDown = $state(false);
	let otherError = $state('');

	ensureStatsIntervalLoaded();

	async function load(signal?: AbortSignal) {
		// Each request is handled independently so a 503 on `current` does
		// not prevent history/top from populating (they read from SQLite
		// which is available even when Docker is down).
		const [currRes, histRes, topRes] = await Promise.allSettled([
			api.fetchSystemStats(signal),
			api.fetchSystemStatsHistory(historyWindow, signal),
			api.fetchTopContainers(topBy, 5, signal)
		]);

		if (currRes.status === 'fulfilled') {
			current = currRes.value;
			dockerDown = false;
			otherError = '';
		} else if (!isAbort(currRes.reason)) {
			if (isDockerDown(currRes.reason)) {
				dockerDown = true;
				otherError = '';
			} else {
				otherError = errorMessage(currRes.reason);
			}
		}

		if (histRes.status === 'fulfilled') {
			history = histRes.value;
		}
		if (topRes.status === 'fulfilled') {
			top = topRes.value;
		}
	}

	function isAbort(e: unknown): boolean {
		return e instanceof DOMException && e.name === 'AbortError';
	}

	function isDockerDown(e: unknown): boolean {
		const msg = errorMessage(e).toLowerCase();
		return msg.includes('docker') && (msg.includes('not available') || msg.includes('503'));
	}

	function errorMessage(e: unknown): string {
		if (e instanceof Error) return e.message;
		return String(e);
	}

	$effect(() => {
		// Read historyWindow/topBy so this effect re-runs when they change.
		void historyWindow;
		void topBy;
		const controller = new AbortController();
		load(controller.signal);
		const intervalId = setInterval(() => load(controller.signal), 15_000);
		return () => {
			controller.abort();
			clearInterval(intervalId);
		};
	});

	const chartOption = $derived<EChartsOption>({
		animation: false,
		grid: { top: 8, right: 12, bottom: 24, left: 40 },
		tooltip: {
			trigger: 'axis',
			axisPointer: { type: 'line' },
			valueFormatter: (v) => {
				if (typeof v !== 'number') return String(v);
				return v.toFixed(1) + '%';
			}
		},
		legend: {
			data: [$t('resources.cpuSeries'), $t('resources.memorySeries')],
			bottom: 0,
			textStyle: { fontSize: 11 }
		},
		xAxis: {
			type: 'time',
			axisLabel: { fontSize: 10, color: '#94a3b8' }
		},
		yAxis: {
			type: 'value',
			min: 0,
			max: 100,
			axisLabel: { fontSize: 10, color: '#94a3b8', formatter: '{value}%' },
			splitLine: { lineStyle: { color: 'rgba(148,163,184,0.15)' } }
		},
		series: [
			{
				name: $t('resources.cpuSeries'),
				type: 'line',
				smooth: true,
				showSymbol: false,
				data: history.map((s) => {
					const cap = (s.ncpu || 1) * 100;
					const pct = cap > 0 ? (s.workload_cpu_percent / cap) * 100 : 0;
					return [s.ts * 1000, Number(pct.toFixed(2))];
				}),
				lineStyle: { color: '#10b981', width: 2 },
				areaStyle: { color: 'rgba(16, 185, 129, 0.15)' }
			},
			{
				name: $t('resources.memorySeries'),
				type: 'line',
				smooth: true,
				showSymbol: false,
				data: history.map((s) => {
					const pct = s.memory_total > 0 ? (s.workload_mem_usage / s.memory_total) * 100 : 0;
					return [s.ts * 1000, Number(pct.toFixed(2))];
				}),
				lineStyle: { color: '#3b82f6', width: 2 },
				areaStyle: { color: 'rgba(59, 130, 246, 0.15)' }
			}
		]
	});
</script>

{#if dockerDown}
	<p class="rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-800 dark:border-amber-700 dark:bg-amber-950/30 dark:text-amber-300">
		{$t('resources.dockerUnavailable')}
	</p>
{:else if otherError}
	<p class="text-xs text-red-500">{otherError}</p>
{/if}

{#if current}
	<!-- Capacity summary -->
	<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
		<div class="rounded-lg border border-[var(--border-primary)] bg-[var(--surface-page)] p-3">
			<div class="text-[10px] uppercase tracking-wide text-[var(--text-tertiary)]">{$t('resources.cpuCores')}</div>
			<div class="mt-1 text-lg font-semibold text-[var(--text-primary)]">{current.ncpu}</div>
		</div>
		<div class="rounded-lg border border-[var(--border-primary)] bg-[var(--surface-page)] p-3">
			<div class="text-[10px] uppercase tracking-wide text-[var(--text-tertiary)]">{$t('resources.memory')}</div>
			<div class="mt-1 text-lg font-semibold text-[var(--text-primary)]">{formatBytes(current.memory_total)}</div>
		</div>
		<div class="rounded-lg border border-[var(--border-primary)] bg-[var(--surface-page)] p-3">
			<div class="text-[10px] uppercase tracking-wide text-[var(--text-tertiary)]">{$t('resources.running')}</div>
			<div class="mt-1 text-lg font-semibold text-[var(--text-primary)]">
				{current.running}<span class="text-xs text-[var(--text-tertiary)]"> / {current.containers}</span>
			</div>
		</div>
		<div class="rounded-lg border border-[var(--border-primary)] bg-[var(--surface-page)] p-3">
			<div class="text-[10px] uppercase tracking-wide text-[var(--text-tertiary)]">{$t('resources.dockerDisk')}</div>
			<div class="mt-1 text-lg font-semibold text-[var(--text-primary)]">{formatBytes(current.disk_total_bytes)}</div>
		</div>
	</div>

	<!-- History chart + window picker -->
	<div class="mt-4 rounded-lg border border-[var(--border-primary)] bg-[var(--surface-page)] p-3">
		<div class="mb-2 flex items-center justify-between gap-2">
			<span class="text-xs font-medium text-[var(--text-secondary)]">{$t('resources.workloadUtilization')}</span>
			<select
				bind:value={historyWindow}
				class="rounded border border-[var(--border-input)] bg-[var(--surface-input)] px-2 py-0.5 text-xs text-[var(--text-secondary)] focus:outline-none"
			>
				<option value="30m">{$t('resources.windowMinutes', { n: '30' })}</option>
				<option value="2h">{$t('resources.windowHours', { n: '2' })}</option>
				<option value="6h">{$t('resources.windowHours', { n: '6' })}</option>
				<option value="24h">{$t('resources.windowHours', { n: '24' })}</option>
			</select>
		</div>
		{#if history.length === 0}
			<p class="py-6 text-center text-xs text-[var(--text-tertiary)]">
				{$statsInterval > 0
					? $t('resources.noSamples', { interval: String($statsInterval) })
					: $t('resources.collectionDisabled')}
			</p>
		{:else}
			<ResourceChart option={chartOption} height="180px" ariaLabel={$t('resources.workloadUtilization')} />
		{/if}
	</div>

	<!-- Disk breakdown -->
	<div class="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-4">
		{#each [
			{ label: $t('resources.diskImages'), size: current.disk_images_bytes, reclaim: current.disk_images_reclaimable },
			{ label: $t('resources.diskContainers'), size: current.disk_containers_bytes, reclaim: current.disk_containers_reclaimable },
			{ label: $t('resources.diskVolumes'), size: current.disk_volumes_bytes, reclaim: current.disk_volumes_reclaimable },
			{ label: $t('resources.diskBuildCache'), size: current.disk_build_cache_bytes, reclaim: current.disk_build_cache_reclaimable }
		] as d}
			<div class="rounded-lg border border-[var(--border-primary)] bg-[var(--surface-page)] p-3">
				<div class="text-[10px] uppercase tracking-wide text-[var(--text-tertiary)]">{d.label}</div>
				<div class="mt-1 text-sm font-semibold text-[var(--text-primary)]">{formatBytes(d.size)}</div>
				{#if d.reclaim > 0}
					<div class="mt-0.5 text-[10px] text-amber-500">
						{$t('resources.reclaimable', { size: formatBytes(d.reclaim) })}
					</div>
				{/if}
			</div>
		{/each}
	</div>

	<!-- Top consumers -->
	<div class="mt-4 rounded-lg border border-[var(--border-primary)] bg-[var(--surface-page)] p-3">
		<div class="mb-2 flex items-center justify-between">
			<span class="text-xs font-medium text-[var(--text-secondary)]">{$t('resources.topConsumers')}</span>
			<div class="flex gap-1">
				<button
					type="button"
					class="rounded px-2 py-0.5 text-[10px] font-medium transition-colors {topBy === 'cpu'
						? 'bg-[var(--surface-card-hover)] text-[var(--text-primary)]'
						: 'text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]'}"
					onclick={() => (topBy = 'cpu')}
				>
					{$t('resources.byCpu')}
				</button>
				<button
					type="button"
					class="rounded px-2 py-0.5 text-[10px] font-medium transition-colors {topBy === 'memory'
						? 'bg-[var(--surface-card-hover)] text-[var(--text-primary)]'
						: 'text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]'}"
					onclick={() => (topBy = 'memory')}
				>
					{$t('resources.byMemory')}
				</button>
			</div>
		</div>
		{#if top.length === 0}
			<p class="py-2 text-center text-xs text-[var(--text-tertiary)]">{$t('resources.noRunning')}</p>
		{:else}
			<div class="space-y-1">
				{#each top as s}
					<div class="flex items-center justify-between gap-2 text-xs">
						<span class="truncate text-[var(--text-secondary)]">
							<span class="rounded bg-[var(--surface-card-hover)] px-1.5 py-0.5 text-[10px] text-[var(--text-tertiary)]">
								{s.owner_type === 'site' ? $t('resources.site') : $t('resources.instance')}
							</span>
							<span class="ml-2 truncate text-[var(--text-primary)]">
								{s.owner_name || (s.container_id ? s.container_id.slice(0, 12) : '')}
							</span>
						</span>
						<span class="tabular-nums text-[var(--text-primary)]">
							{#if topBy === 'cpu'}
								{s.cpu_percent.toFixed(1)}%
							{:else}
								{formatBytes(s.memory_usage)}
							{/if}
						</span>
					</div>
				{/each}
			</div>
		{/if}
	</div>
{:else if !dockerDown && !otherError}
	<p class="text-xs text-[var(--text-tertiary)]">{$t('resources.loading')}</p>
{/if}
