<script lang="ts">
    import { onMount, onDestroy } from 'svelte';
    import {
        getSystemStats,
        getOverviewStats,
        getCrawlerStatus,
        crawlerStatusStreamUrl,
        getAnalysisMetrics,
        analysisStatusStreamUrl,
        getCrawlerSettings,
        getAnalysisSettings,
        getHealth,
        type SystemStats,
        type OverviewStats,
        type CrawlerStatus,
        type AnalysisMetrics,
        type AnalysisEvent,
        type HealthReport
    } from '$lib/api/admin';
    import { formatBytes as fmtBytes } from '$lib/upload-validation';
    import type { LayoutData } from './$types';

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

    // System stats are loaded once by the layout gate; seed from there and
    // keep polling so the Overview's System card stays live. Capturing the
    // initial `data.stats` is intentional — we own `sys` after mount.
    // svelte-ignore state_referenced_locally
    let sys: SystemStats = $state(data.stats);
    let overview: OverviewStats | null = $state(null);
    let crawler: CrawlerStatus | null = $state(null);
    let metrics: AnalysisMetrics | null = $state(null);
    let nowAnalyzing: {
        title: string;
        chapter: number;
        page: number;
        phase: 'analyzing' | 'done' | 'failed';
    } | null = $state(null);
    // Settings strip — daemon state + a couple of effective values.
    let crawlerDaemon = $state<boolean | null>(null);
    let crawlerPassAt = $state<string | null>(null);
    let analysisEnabled = $state<boolean | null>(null);
    let analysisModel = $state<string | null>(null);
    let analysisEndpointOk = $state<boolean | null>(null);

    let liveCrawler = $state(false);
    let liveAnalysis = $state(false);

    let health = $state<HealthReport | null>(null);

    let sysTimer: ReturnType<typeof setInterval> | null = null;
    let overviewTimer: ReturnType<typeof setInterval> | null = null;
    let crawlerSource: EventSource | null = null;
    let analysisSource: EventSource | null = null;

    const coverage = $derived.by(() => {
        if (!overview) return 0;
        const a = overview.analysis;
        return a.total_pages > 0 ? (a.analyzed_pages / a.total_pages) * 100 : 0;
    });

    // Alerts rail: system thresholds plus crawler health signals.
    const extraAlerts = $derived.by(() => {
        const out: string[] = [];
        if (crawler) {
            if (crawler.queue.dead > 0) out.push(`crawler: ${crawler.queue.dead} dead job(s)`);
            if (crawler.session.expired) out.push('crawler: session expired');
        }
        return out;
    });

    async function refreshSys() {
        try {
            sys = await getSystemStats();
        } catch {
            // keep the last good snapshot; transient errors self-heal next poll
        }
    }
    async function refreshOverview() {
        try {
            overview = await getOverviewStats();
        } catch {
            // ignore; retried on the next tick
        }
    }
    async function refreshMetrics() {
        try {
            metrics = await getAnalysisMetrics(1);
        } catch {
            // ignore
        }
    }
    async function refreshHealth() {
        try {
            health = await getHealth();
        } catch {
            // keep the last good snapshot; retried next tick
        }
    }

    const healthFailing = $derived(
        health ? health.checks.filter((c) => c.status !== 'ok').length : 0
    );
    async function loadSettings() {
        try {
            const c = await getCrawlerSettings();
            crawlerDaemon = c.editable.daemon_enabled;
            crawlerPassAt = c.editable.daily_at;
        } catch {
            // strip degrades to "—"
        }
        try {
            const a = await getAnalysisSettings();
            analysisEnabled = a.editable.enabled;
            analysisModel = a.editable.model;
            analysisEndpointOk = a.env_only.api_key_configured && a.editable.endpoint.length > 0;
        } catch {
            // strip degrades to "—"
        }
    }

    function applyAnalysisEvent(ev: AnalysisEvent) {
        if (ev.kind === 'enqueued') return;
        // Cancelled (daemon shutdown / settings toggle-off mid-dispatch)
        // is a non-terminal release — the lease went back to pending.
        // Clear the banner instead of frame-flipping to a "done"/"failed"
        // misrepresentation.
        if (ev.kind === 'cancelled') {
            if (nowAnalyzing && nowAnalyzing.page === ev.page_number) {
                nowAnalyzing = null;
            }
            return;
        }
        const phase =
            ev.kind === 'started' ? 'analyzing' : ev.kind === 'completed' ? 'done' : 'failed';
        nowAnalyzing = {
            title: ev.manga_title,
            chapter: ev.chapter_number,
            page: ev.page_number,
            phase
        };
    }

    function openCrawlerStream() {
        if (crawlerSource) return;
        const es = new EventSource(crawlerStatusStreamUrl(), { withCredentials: true });
        es.addEventListener('status', (e) => {
            try {
                crawler = JSON.parse((e as MessageEvent).data) as CrawlerStatus;
                liveCrawler = true;
            } catch {
                // skip a malformed frame; the next replaces it
            }
        });
        es.onopen = () => (liveCrawler = true);
        es.onerror = () => (liveCrawler = false);
        crawlerSource = es;
    }
    function openAnalysisStream() {
        if (analysisSource) return;
        const es = new EventSource(analysisStatusStreamUrl(), { withCredentials: true });
        es.addEventListener('analysis', (e) => {
            try {
                applyAnalysisEvent(JSON.parse((e as MessageEvent).data) as AnalysisEvent);
                liveAnalysis = true;
            } catch {
                // skip malformed frame
            }
        });
        // Dropped frames — pull a fresh coverage snapshot. (The 30s poll
        // would catch up anyway; this just tightens the gap.)
        es.addEventListener('lagged', () => {
            refreshOverview();
            refreshMetrics();
        });
        es.onopen = () => (liveAnalysis = true);
        es.onerror = () => (liveAnalysis = false);
        analysisSource = es;
    }
    function closeStreams() {
        crawlerSource?.close();
        crawlerSource = null;
        analysisSource?.close();
        analysisSource = null;
        liveCrawler = false;
        liveAnalysis = false;
    }
    function onVisibilityChange() {
        if (typeof document === 'undefined') return;
        if (document.visibilityState === 'hidden') {
            closeStreams();
        } else {
            openCrawlerStream();
            openAnalysisStream();
        }
    }
    // Close the streams entering BFCache and reopen on restore, so a
    // back/forward navigation doesn't leave a stale connection behind.
    function onPageHide() {
        closeStreams();
    }
    function onPageShow() {
        openCrawlerStream();
        openAnalysisStream();
    }

    onMount(() => {
        refreshSys();
        refreshOverview();
        refreshMetrics();
        refreshHealth();
        loadSettings();
        getCrawlerStatus()
            .then((s) => (crawler = s))
            .catch(() => {});
        openCrawlerStream();
        openAnalysisStream();
        sysTimer = setInterval(refreshSys, 5000);
        // 60s: the overview aggregate is server-cached (~10s TTL) and these
        // are background freshness updates, so a slower poll halves idle DB
        // load without the dashboard feeling stale.
        overviewTimer = setInterval(() => {
            refreshOverview();
            refreshMetrics();
            refreshHealth();
        }, 60000);
        if (typeof document !== 'undefined') {
            document.addEventListener('visibilitychange', onVisibilityChange);
            window.addEventListener('pagehide', onPageHide);
            window.addEventListener('pageshow', onPageShow);
        }
    });
    onDestroy(() => {
        if (sysTimer) clearInterval(sysTimer);
        if (overviewTimer) clearInterval(overviewTimer);
        closeStreams();
        if (typeof document !== 'undefined') {
            document.removeEventListener('visibilitychange', onVisibilityChange);
            window.removeEventListener('pagehide', onPageHide);
            window.removeEventListener('pageshow', onPageShow);
        }
    });

    function fmtPercent(pct: number): string {
        return `${pct.toFixed(0)}%`;
    }
    function fmtInt(n: number): string {
        return n.toLocaleString();
    }
    function ago(iso: string | null): string {
        if (!iso) return '';
        const s = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000);
        if (s < 60) return `${Math.floor(s)}s ago`;
        const mins = s / 60;
        if (mins < 60) return `${Math.floor(mins)}m ago`;
        const hrs = mins / 60;
        if (hrs < 24) return `${Math.floor(hrs)}h ago`;
        return `${Math.floor(hrs / 24)}d ago`;
    }
    function secs(ms: number | null): string {
        return ms === null ? '—' : `${(ms / 1000).toFixed(1)}s`;
    }
</script>

<h1>Overview</h1>

{#if health}
    <a
        class="health-summary status-{health.status}"
        href="/admin/health"
        data-testid="overview-health"
    >
        <span class="glyph" aria-hidden="true"
            >{health.status === 'ok' ? '✓' : health.status === 'warn' ? '⚠' : '✗'}</span
        >
        <span class="hs-label">
            {#if health.status === 'ok'}
                All health checks passing
            {:else}
                {healthFailing} health check{healthFailing === 1 ? '' : 's'} need attention
            {/if}
        </span>
        <span class="hs-view">View →</span>
    </a>
{/if}

{#if sys.alerts.length > 0 || extraAlerts.length > 0}
    <section class="alerts" data-testid="admin-alerts">
        {#each sys.alerts as a (a.message)}
            <div class="alert" data-level={a.level}>{a.message}</div>
        {/each}
        {#each extraAlerts as msg (msg)}
            <div class="alert" data-level="warning">{msg}</div>
        {/each}
    </section>
{/if}

<div class="sections">
    <!-- System -->
    <section class="card">
        <header>
            <h2>System</h2>
            <a class="view" href="/admin/system">View →</a>
        </header>
        {#if sys.disk}
            {@render MeterRow(
                'Disk',
                sys.disk.percent_used,
                `${fmtBytes(sys.disk.used_bytes)} / ${fmtBytes(sys.disk.total_bytes)}`
            )}
        {:else}
            {@render MeterRow('Disk', 0, 'n/a — non-local storage')}
        {/if}
        {@render MeterRow(
            'Memory',
            sys.memory.percent_used,
            `${fmtBytes(sys.memory.used_bytes)} / ${fmtBytes(sys.memory.total_bytes)}`
        )}
        {@render MeterRow('CPU', sys.cpu.percent_used, `load ${sys.cpu.load_avg.one.toFixed(2)}`)}
    </section>

    <!-- Crawler -->
    <section class="card">
        <header>
            <h2>Crawler</h2>
            <span
                class="live"
                class:on={liveCrawler}
                title={liveCrawler ? 'live' : 'reconnecting'}
            ></span>
            <a class="view" href="/admin/crawler">View →</a>
        </header>
        {#if crawler}
            <p class="line">
                <span class="dot" class:ok={crawler.daemon === 'running'}></span>
                {crawler.daemon} · phase {crawler.phase?.state ?? 'idle'} · {crawler.worker_count} workers
            </p>
            <p class="line">
                Queue pending {fmtInt(crawler.queue.pending)} · running {fmtInt(crawler.queue.running)}
                · <span class:warn={crawler.queue.dead > 0}>dead {fmtInt(crawler.queue.dead)}</span>
                · covers {fmtInt(crawler.covers_queued)}
            </p>
            <p class="line muted">
                browser {crawler.browser} · session {crawler.session.configured ? '✓' : '—'} · last
                pass +{fmtInt(crawler.last_pass.upserted)} / {fmtInt(crawler.last_pass.discovered)} seen
                / {fmtInt(crawler.last_pass.mangas_failed)} failed
            </p>
        {:else}
            <p class="muted">Loading…</p>
        {/if}
    </section>

    <!-- Analysis -->
    <section class="card">
        <header>
            <h2>Analysis</h2>
            <span
                class="live"
                class:on={liveAnalysis}
                title={liveAnalysis ? 'live' : 'reconnecting'}
            ></span>
            <a class="view" href="/admin/analysis">View →</a>
        </header>
        {#if overview}
            {@render MeterRow(
                'Coverage',
                coverage,
                `${fmtInt(overview.analysis.analyzed_pages)} / ${fmtInt(overview.analysis.total_pages)} pages`
            )}
        {/if}
        {#if nowAnalyzing}
            <p class="line">
                {nowAnalyzing.phase === 'analyzing' ? 'now' : 'last'}:
                <strong>{nowAnalyzing.title}</strong> ch.{nowAnalyzing.chapter} p.{nowAnalyzing.page}
                · {nowAnalyzing.phase}
            </p>
        {/if}
        {#if metrics}
            <p class="line muted">
                last 24h {fmtInt(metrics.ok)} ok · {fmtInt(metrics.failed)} failed · ⌀ {secs(
                    metrics.avg_ms
                )}/page
            </p>
        {/if}
    </section>

    <!-- Mangas -->
    <section class="card">
        <header>
            <h2>Mangas</h2>
            <a class="view" href="/admin/mangas">View →</a>
        </header>
        {#if overview}
            <p class="line big">
                {fmtInt(overview.mangas.total)} mangas · {fmtInt(overview.mangas.total_chapters)} chapters
                · {fmtInt(overview.mangas.total_pages)} pages
            </p>
            <p class="line">
                synced {fmtInt(overview.mangas.synced)} · in progress {fmtInt(
                    overview.mangas.in_progress
                )} · dropped {fmtInt(overview.mangas.dropped)}
            </p>
            {#if overview.mangas.newest_title}
                <p class="line muted">
                    newest: {overview.mangas.newest_title}{overview.mangas.newest_seen_at
                        ? ` (seen ${ago(overview.mangas.newest_seen_at)})`
                        : ''}
                </p>
            {/if}
        {:else}
            <p class="muted">Loading…</p>
        {/if}
    </section>

    <!-- Users -->
    <section class="card">
        <header>
            <h2>Users</h2>
            <a class="view" href="/admin/users">View →</a>
        </header>
        {#if overview}
            <p class="line big">
                {fmtInt(overview.users.total)} users · {fmtInt(overview.users.admins)} admins
            </p>
            {#if overview.users.newest_username}
                <p class="line muted">
                    newest: {overview.users.newest_username}{overview.users.newest_created_at
                        ? ` (created ${ago(overview.users.newest_created_at)})`
                        : ''}
                </p>
            {/if}
        {:else}
            <p class="muted">Loading…</p>
        {/if}
    </section>

    <!-- Settings (thin daemon-status strip) -->
    <section class="card strip">
        <div class="strip-body">
            <span>
                <span class="dot" class:ok={crawlerDaemon === true}></span>
                Crawler {crawlerDaemon === null ? '—' : crawlerDaemon ? 'enabled' : 'disabled'}
                {#if crawlerPassAt}· pass {crawlerPassAt}{/if}
            </span>
            <span class="sep">|</span>
            <span>
                <span class="dot" class:ok={analysisEnabled === true}></span>
                Analysis {analysisEnabled === null ? '—' : analysisEnabled ? 'enabled' : 'disabled'}
                {#if analysisModel}· {analysisModel}{/if}
                {#if analysisEndpointOk !== null}· endpoint {analysisEndpointOk ? '✓' : '—'}{/if}
            </span>
        </div>
        <a class="view" href="/admin/settings">View →</a>
    </section>
</div>

{#snippet MeterRow(label: string, percent: number, sub: string)}
    <div class="meter">
        <span class="meter-label">{label}</span>
        <div
            class="bar"
            role="progressbar"
            aria-valuenow={percent}
            aria-valuemin="0"
            aria-valuemax="100"
            aria-label="{label} {fmtPercent(percent)}"
        >
            <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="bar-label">{fmtPercent(percent)}</span>
        </div>
        <span class="meter-sub">{sub}</span>
    </div>
{/snippet}

<style>
    h1 {
        margin: 0 0 var(--space-4) 0;
    }
    .health-summary {
        display: flex;
        align-items: center;
        gap: var(--space-2);
        padding: var(--space-2) var(--space-3);
        margin-bottom: var(--space-3);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        background: var(--surface);
        color: var(--text);
        font-size: var(--font-sm);
    }
    .health-summary:hover {
        background: var(--surface-elevated);
        text-decoration: none;
    }
    .health-summary .glyph {
        font-weight: var(--weight-semibold);
    }
    .health-summary .hs-view {
        margin-left: auto;
        color: var(--text-muted);
    }
    .health-summary.status-ok .glyph {
        color: var(--success, #16a34a);
    }
    .health-summary.status-warn {
        border-left: 4px solid var(--warning, #d97706);
    }
    .health-summary.status-warn .glyph {
        color: var(--warning, #d97706);
    }
    .health-summary.status-critical {
        border-left: 4px solid var(--danger, #dc2626);
    }
    .health-summary.status-critical .glyph {
        color: var(--danger, #dc2626);
    }
    .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;
    }
    .sections {
        display: flex;
        flex-direction: column;
        gap: var(--space-3);
    }
    .card {
        padding: var(--space-3) var(--space-4);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        background: var(--surface);
    }
    .card header {
        display: flex;
        align-items: center;
        gap: var(--space-2);
        margin-bottom: var(--space-3);
    }
    .card header h2 {
        margin: 0;
        font-size: var(--font-sm);
        color: var(--text-muted);
        text-transform: uppercase;
        letter-spacing: 0.04em;
    }
    .view {
        margin-left: auto;
        font-size: var(--font-sm);
        color: var(--text-muted);
        text-decoration: none;
    }
    .view:hover {
        color: var(--text);
    }
    .live {
        width: 0.5rem;
        height: 0.5rem;
        border-radius: 50%;
        background: var(--text-muted);
        opacity: 0.4;
    }
    .live.on {
        background: #22c55e;
        opacity: 1;
    }
    .meter {
        display: grid;
        grid-template-columns: 5rem 1fr minmax(8rem, max-content);
        align-items: center;
        gap: var(--space-3);
        margin-bottom: var(--space-2);
    }
    .meter:last-child {
        margin-bottom: 0;
    }
    .meter-label {
        font-size: var(--font-sm);
        color: var(--text-muted);
    }
    .meter-sub {
        font-size: var(--font-sm);
        color: var(--text-muted);
        font-family: var(--font-mono, monospace);
        text-align: right;
    }
    .bar {
        position: relative;
        background: var(--surface-elevated);
        border-radius: var(--radius-sm, 4px);
        height: 1.25rem;
        overflow: hidden;
    }
    .fill {
        height: 100%;
        background: #22c55e;
        transition: width 0.3s ease, background 0.3s ease;
    }
    .fill.mid {
        background: #f59e0b;
    }
    .fill.high {
        background: #dc2626;
    }
    .bar-label {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        font-size: var(--font-xs, 0.75rem);
        font-weight: var(--weight-semibold);
        color: var(--text);
    }
    .line {
        margin: 0 0 var(--space-2) 0;
        font-size: var(--font-sm);
    }
    .line:last-child {
        margin-bottom: 0;
    }
    .line.big {
        font-size: var(--font-md, 1rem);
        font-weight: var(--weight-semibold);
    }
    .muted {
        color: var(--text-muted);
    }
    .warn {
        color: #f59e0b;
        font-weight: var(--weight-semibold);
    }
    .dot {
        display: inline-block;
        width: 0.5rem;
        height: 0.5rem;
        border-radius: 50%;
        background: var(--text-muted);
        margin-right: var(--space-1);
    }
    .dot.ok {
        background: #22c55e;
    }
    .strip {
        display: flex;
        align-items: center;
        gap: var(--space-3);
    }
    .strip-body {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-2) var(--space-3);
        font-size: var(--font-sm);
    }
    .strip .sep {
        color: var(--border);
    }
</style>
