<script lang="ts">
    import { onMount, onDestroy } from 'svelte';
    import {
        getSystemStats,
        getStorageStats,
        backfillStorage,
        type SystemStats,
        type StorageStats
    } from '$lib/api/admin';
    import { formatBytes as fmtBytes } from '$lib/upload-validation';
    import { pickCpuTemp } from '$lib/admin/temps';

    let stats: SystemStats | null = $state(null);

    // The component list is noisy (every core, chipset, NVMe…). Surface the
    // CPU die/package reading and tuck the rest behind a disclosure.
    const cpuTemp = $derived.by(() => (stats ? pickCpuTemp(stats.temperatures) : null));
    const otherTemps = $derived.by(() =>
        stats ? stats.temperatures.filter((t) => t !== cpuTemp) : []
    );
    let error: string | null = $state(null);
    let timer: ReturnType<typeof setInterval> | null = null;
    let storageTimer: ReturnType<typeof setInterval> | null = null;

    // Storage usage is a cheap DB aggregate — refreshed alongside the
    // system poll so the totals/leaderboards don't go stale while crawls
    // and uploads land elsewhere.
    let storage: StorageStats | null = $state(null);
    let storageError: string | null = $state(null);
    let backfilling = $state(false);
    let backfillNote: string | null = $state(null);

    async function refresh() {
        try {
            stats = await getSystemStats();
            error = null;
        } catch (e) {
            error = e instanceof Error ? e.message : 'refresh failed';
        }
    }

    async function refreshStorage() {
        try {
            storage = await getStorageStats();
            storageError = null;
        } catch (e) {
            storageError = e instanceof Error ? e.message : 'failed to load storage usage';
        }
    }

    async function runBackfill() {
        backfilling = true;
        backfillNote = null;
        try {
            const r = await backfillStorage();
            const extra = [
                r.missing > 0 ? `${r.missing} missing` : '',
                r.errored > 0 ? `${r.errored} errored` : ''
            ].filter(Boolean);
            backfillNote =
                `Backfilled ${r.pages} page(s), ${r.covers} cover(s)${
                    extra.length ? ` (${extra.join(', ')})` : ''
                }.` + (r.more_remaining ? ' More remain — run again to finish.' : '');
            await refreshStorage();
        } catch (e) {
            backfillNote = e instanceof Error ? e.message : 'backfill failed';
        } finally {
            backfilling = false;
        }
    }

    onMount(() => {
        refresh();
        refreshStorage();
        // System metrics (cheap) poll fast; storage stats run library-scaling
        // aggregations, so they refresh on a slower cadence — fresh enough for
        // an admin view without re-running the leaderboard queries every 5 s.
        timer = setInterval(refresh, 5000);
        storageTimer = setInterval(refreshStorage, 30000);
    });
    onDestroy(() => {
        if (timer) clearInterval(timer);
        if (storageTimer) clearInterval(storageTimer);
    });

    function fmtPercent(ratio: number): string {
        return `${(ratio * 100).toFixed(1)}%`;
    }
    // Distinct from the shared chapterLabel() (which renders just the
    // title): the leaderboard needs the chapter number too.
    function chapterRef(c: { number: number; title: string | null }): string {
        return c.title ? `Ch. ${c.number} — ${c.title}` : `Ch. ${c.number}`;
    }
</script>

<h1>System</h1>

{#if error}
    <p class="error" role="alert">{error}</p>
{/if}

{#if stats}
    {#if stats.alerts.length > 0}
        <section class="alerts">
            {#each stats.alerts as a (a.message)}
                <div class="alert">{a.message}</div>
            {/each}
        </section>
    {/if}

    <section class="grid">
        <article>
            <h2>Disk (storage_dir)</h2>
            {#if stats.disk}
                {@render Bar({ percent: stats.disk.percent_used })}
                <dl>
                    <dt>Total</dt>
                    <dd>{fmtBytes(stats.disk.total_bytes)}</dd>
                    <dt>Used</dt>
                    <dd>{fmtBytes(stats.disk.used_bytes)}</dd>
                    <dt>Free</dt>
                    <dd>{fmtBytes(stats.disk.free_bytes)}</dd>
                </dl>
            {:else}
                <p class="muted">n/a — non-local storage backend</p>
            {/if}
        </article>

        <article>
            <h2>Memory</h2>
            {@render Bar({ percent: stats.memory.percent_used })}
            <dl>
                <dt>Total</dt>
                <dd>{fmtBytes(stats.memory.total_bytes)}</dd>
                <dt>Used</dt>
                <dd>{fmtBytes(stats.memory.used_bytes)}</dd>
            </dl>
        </article>

        <article>
            <h2>CPU</h2>
            {@render Bar({ percent: stats.cpu.percent_used })}
            <dl>
                <dt>Load avg</dt>
                <dd>
                    {stats.cpu.load_avg.one.toFixed(2)} / {stats.cpu.load_avg.five.toFixed(2)} / {stats.cpu.load_avg.fifteen.toFixed(
                        2
                    )}
                </dd>
            </dl>
            {#if stats.cpu.per_core.length > 0}
                <div class="cores" aria-label="per-core usage">
                    {#each stats.cpu.per_core as core, i (i)}
                        <div
                            class="core"
                            title="core {i}: {core.toFixed(0)}%"
                            role="progressbar"
                            aria-valuenow={core}
                            aria-valuemin="0"
                            aria-valuemax="100"
                            aria-label="core {i} {core.toFixed(0)}%"
                        >
                            <div
                                class="core-fill"
                                class:high={core >= 90}
                                class:mid={core >= 70 && core < 90}
                                style:height="{Math.min(100, Math.max(0, core))}%"
                            ></div>
                        </div>
                    {/each}
                </div>
            {/if}
        </article>

        <article>
            <h2>Temperatures</h2>
            {#if stats.temperatures.length === 0}
                <p class="muted">Sensor data unavailable (not exposed in this environment).</p>
            {:else if cpuTemp}
                {@render Temp(cpuTemp)}
                {#if otherTemps.length > 0}
                    <details class="more-temps">
                        <summary>{otherTemps.length} other sensor{otherTemps.length === 1 ? '' : 's'}</summary>
                        {#each otherTemps as t (t.label)}
                            {@render Temp(t)}
                        {/each}
                    </details>
                {/if}
            {:else}
                {#each stats.temperatures as t (t.label)}
                    {@render Temp(t)}
                {/each}
            {/if}
        </article>
    </section>

    <p class="hint">refreshing every 5 s</p>
{:else}
    <p>Loading…</p>
{/if}

<section class="storage" aria-label="storage usage">
    <header class="storage-head">
        <h2>Storage</h2>
        <button
            type="button"
            onclick={runBackfill}
            disabled={backfilling}
            aria-busy={backfilling}
        >
            {backfilling ? 'Backfilling…' : 'Backfill sizes'}
        </button>
    </header>

    {#if storageError}
        <p class="error" role="alert">{storageError}</p>
    {/if}
    {#if backfillNote}
        <p class="note" role="status">{backfillNote}</p>
    {/if}
    {#if storage && (storage.unmeasured_pages > 0 || storage.unmeasured_covers > 0)}
        <p class="unmeasured" role="status">
            {storage.unmeasured_pages + storage.unmeasured_covers} item(s) not yet measured —
            figures below are partial and leaderboards omit them. Run “Backfill sizes” to complete.
        </p>
    {/if}

    {#if storage}
        <div class="cards">
            <article class="card">
                <h3>Total content</h3>
                <p class="metric">{fmtBytes(storage.total_bytes)}</p>
                {#if storage.ratio_of_disk !== null}
                    <p class="sub">{fmtPercent(storage.ratio_of_disk)} of disk</p>
                {/if}
            </article>
            <article class="card">
                <h3>Covers</h3>
                <p class="metric">{fmtBytes(storage.covers_bytes)}</p>
                {#if storage.avg_cover_bytes !== null}
                    <p class="sub">avg {fmtBytes(storage.avg_cover_bytes)}</p>
                {/if}
            </article>
            <article class="card">
                <h3>Chapters</h3>
                <p class="metric">{fmtBytes(storage.chapters_bytes)}</p>
                {#if storage.avg_chapter_page_bytes !== null}
                    <p class="sub">avg {fmtBytes(storage.avg_chapter_page_bytes)}/page</p>
                {/if}
            </article>
        </div>
        {#if storage.avg_image_bytes !== null}
            <p class="hint">avg image (all): {fmtBytes(storage.avg_image_bytes)}</p>
        {/if}

        <div class="boards">
            <article class="board">
                <h3>Largest mangas</h3>
                {#if storage.top_mangas.length === 0}
                    <p class="muted">No content yet.</p>
                {:else}
                    <ol>
                        {#each storage.top_mangas as m (m.id)}
                            <li>
                                <a href="/manga/{m.id}">{m.title}</a>
                                <span class="bytes">{fmtBytes(m.total_bytes)}</span>
                            </li>
                        {/each}
                    </ol>
                {/if}
            </article>
            <article class="board">
                <h3>Largest chapters</h3>
                {#if storage.top_chapters.length === 0}
                    <p class="muted">No content yet.</p>
                {:else}
                    <ol>
                        {#each storage.top_chapters as c (c.id)}
                            <li>
                                <a href="/manga/{c.manga_id}/chapter/{c.id}">{chapterRef(c)}</a>
                                <span class="bytes">{fmtBytes(c.total_bytes)}</span>
                            </li>
                        {/each}
                    </ol>
                {/if}
            </article>
        </div>
    {:else if !storageError}
        <p>Loading…</p>
    {/if}
</section>

{#snippet Temp(t: { label: string; celsius: number; max_celsius: number | null; critical_celsius: number | null })}
    {@const limit = t.critical_celsius ?? 100}
    {@const pct = Math.min(100, Math.max(0, (t.celsius / limit) * 100))}
    <div class="temp">
        <div class="temp-head">
            <span class="temp-label">{t.label}</span>
            <span class="temp-val">{t.celsius.toFixed(0)}°C</span>
        </div>
        <div
            class="bar"
            role="progressbar"
            aria-valuenow={t.celsius}
            aria-valuemin="0"
            aria-valuemax={limit}
            aria-label="{t.label} {t.celsius.toFixed(0)}°C"
        >
            <div
                class="fill"
                class:high={pct >= 90}
                class:mid={pct >= 70 && pct < 90}
                style:width="{pct}%"
            ></div>
        </div>
        <p class="sub">
            {#if t.max_celsius !== null}max {t.max_celsius.toFixed(0)}°C{/if}
            {#if t.critical_celsius !== null}
                · crit {t.critical_celsius.toFixed(0)}°C{/if}
        </p>
    </div>
{/snippet}

{#snippet Bar({ percent }: { percent: number })}
    <div
        class="bar"
        role="progressbar"
        aria-valuenow={percent}
        aria-valuemin="0"
        aria-valuemax="100"
        aria-label="{percent.toFixed(1)}% used"
    >
        <div
            class="fill"
            class:high={percent >= 90}
            class:mid={percent >= 70 && percent < 90}
            style:width="{Math.min(100, Math.max(0, percent))}%"
        ></div>
        <span class="label">{percent.toFixed(1)}%</span>
    </div>
{/snippet}

<style>
    h1 {
        margin: 0 0 var(--space-4) 0;
    }
    .grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
        gap: var(--space-3);
    }
    article {
        padding: var(--space-3);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        background: var(--surface);
    }
    article h2 {
        margin: 0 0 var(--space-3) 0;
        font-size: var(--font-sm);
        color: var(--text-muted);
        text-transform: uppercase;
        letter-spacing: 0.04em;
    }
    .bar {
        position: relative;
        background: var(--surface-elevated);
        border-radius: var(--radius-sm, 4px);
        height: 1.5rem;
        margin-bottom: var(--space-2);
        overflow: hidden;
    }
    .fill {
        height: 100%;
        background: #22c55e;
        transition: width 0.3s ease, background 0.3s ease;
    }
    .fill.mid {
        background: #f59e0b;
    }
    .fill.high {
        background: #dc2626;
    }
    .label {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        font-size: var(--font-sm);
        font-weight: var(--weight-semibold);
        color: var(--text);
    }
    .cores {
        display: flex;
        align-items: flex-end;
        gap: 3px;
        height: 2.5rem;
        margin-top: var(--space-2);
    }
    .core {
        position: relative;
        flex: 1 1 0;
        min-width: 4px;
        height: 100%;
        background: var(--surface-elevated);
        border-radius: var(--radius-sm, 4px);
        overflow: hidden;
        display: flex;
        align-items: flex-end;
    }
    .core-fill {
        width: 100%;
        background: #22c55e;
        transition: height 0.3s ease, background 0.3s ease;
    }
    .core-fill.mid {
        background: #f59e0b;
    }
    .core-fill.high {
        background: #dc2626;
    }
    .temp + .temp {
        margin-top: var(--space-3);
    }
    .more-temps {
        margin-top: var(--space-3);
    }
    .more-temps summary {
        cursor: pointer;
        font-size: var(--font-sm);
        color: var(--text-muted);
    }
    .more-temps .temp:first-of-type {
        margin-top: var(--space-3);
    }
    .temp-head {
        display: flex;
        justify-content: space-between;
        gap: var(--space-3);
        margin-bottom: var(--space-1);
        font-size: var(--font-sm);
    }
    .temp-label {
        color: var(--text-muted);
    }
    .temp-val {
        font-family: var(--font-mono, monospace);
        font-weight: var(--weight-semibold);
    }
    dl {
        display: grid;
        grid-template-columns: max-content 1fr;
        gap: var(--space-1) var(--space-3);
        margin: 0;
        font-size: var(--font-sm);
    }
    dt {
        color: var(--text-muted);
    }
    dd {
        margin: 0;
        font-family: var(--font-mono, monospace);
    }
    .alerts {
        display: flex;
        flex-direction: column;
        gap: var(--space-2);
        margin-bottom: var(--space-4);
    }
    .alert {
        padding: var(--space-3);
        border-radius: var(--radius-md);
        background: var(--surface-elevated);
        border-left: 4px solid #f59e0b;
    }
    .hint {
        color: var(--text-muted);
        font-size: var(--font-sm);
        margin-top: var(--space-3);
    }
    .muted {
        color: var(--text-muted);
    }
    .error {
        color: var(--danger, #dc2626);
        padding: var(--space-2) var(--space-3);
        border: 1px solid var(--danger, #dc2626);
        border-radius: var(--radius-md);
        margin-bottom: var(--space-3);
    }
    .storage {
        margin-top: var(--space-5, 2rem);
    }
    .storage-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-3);
        margin-bottom: var(--space-3);
    }
    .storage-head h2 {
        margin: 0;
    }
    .storage-head button {
        padding: var(--space-2) var(--space-3);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        background: var(--surface);
        cursor: pointer;
        font-size: var(--font-sm);
    }
    .storage-head button:disabled {
        opacity: 0.6;
        cursor: default;
    }
    .note {
        color: var(--text-muted);
        font-size: var(--font-sm);
        margin: 0 0 var(--space-3) 0;
    }
    .unmeasured {
        padding: var(--space-2) var(--space-3);
        margin: 0 0 var(--space-3) 0;
        font-size: var(--font-sm);
        border-radius: var(--radius-md);
        background: var(--surface-elevated);
        border-left: 4px solid #f59e0b;
    }
    .cards {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
        gap: var(--space-3);
    }
    .card {
        padding: var(--space-3);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        background: var(--surface);
    }
    .card h3 {
        margin: 0 0 var(--space-2) 0;
        font-size: var(--font-sm);
        color: var(--text-muted);
        text-transform: uppercase;
        letter-spacing: 0.04em;
    }
    .metric {
        margin: 0;
        font-size: var(--font-xl, 1.5rem);
        font-weight: var(--weight-semibold);
    }
    .sub {
        margin: var(--space-1) 0 0 0;
        font-size: var(--font-sm);
        color: var(--text-muted);
    }
    .boards {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
        gap: var(--space-3);
        margin-top: var(--space-3);
    }
    .board {
        padding: var(--space-3);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        background: var(--surface);
    }
    .board h3 {
        margin: 0 0 var(--space-2) 0;
        font-size: var(--font-sm);
        color: var(--text-muted);
        text-transform: uppercase;
        letter-spacing: 0.04em;
    }
    .board ol {
        margin: 0;
        padding-left: var(--space-4);
        display: flex;
        flex-direction: column;
        gap: var(--space-1);
    }
    .board li {
        display: flex;
        justify-content: space-between;
        gap: var(--space-3);
        font-size: var(--font-sm);
    }
    .board .bytes {
        font-family: var(--font-mono, monospace);
        color: var(--text-muted);
        white-space: nowrap;
    }
</style>
