<script lang="ts">
    import { onMount } from 'svelte';
    import Pager from '$lib/components/Pager.svelte';
    import { fmtDuration } from '$lib/format';
    import TrendChart from '$lib/components/charts/TrendChart.svelte';
    import { buildSeries, type TrendSeries } from '$lib/admin/series';
    import {
        getCrawlerMetrics,
        getCrawlerMetricsSeries,
        listCrawlerOps,
        type OpSummary,
        type OpRow,
        type CrawlOp
    } from '$lib/api/admin';

    const LIMIT = 25;

    let days = $state(7);
    let summary = $state<OpSummary[]>([]);
    let summaryLoading = $state(true);
    let series = $state<TrendSeries | null>(null);
    let seriesCtrl: AbortController | null = null;

    let rows = $state<OpRow[]>([]);
    let total = $state(0);
    let page = $state(1);
    let opFilter = $state<'' | CrawlOp>('');
    let outcomeFilter = $state<'' | 'ok' | 'failed'>('');
    let opsLoading = $state(true);
    let error = $state<string | null>(null);
    let expanded = $state<string | null>(null);

    const totalPages = $derived(Math.max(1, Math.ceil(total / LIMIT)));

    // Human labels + canonical order for the summary table.
    const OP_LABELS: Record<CrawlOp, string> = {
        manga_list: 'manga list walk',
        manga_detail: 'manga detail',
        manga_cover: 'manga cover',
        chapter: 'whole chapter'
    };
    const OP_ORDER: CrawlOp[] = ['manga_list', 'manga_detail', 'manga_cover', 'chapter'];
    const orderedSummary = $derived(
        OP_ORDER.map((op) => summary.find((s) => s.op === op)).filter(
            (s): s is OpSummary => s != null
        )
    );
    // Derived per-page crawl time from the chapter roll-up (Σms ÷ Σpages ≈
    // avg_ms ÷ avg_items). Shown as an indented sub-row under "whole chapter".
    const chapter = $derived(summary.find((s) => s.op === 'chapter'));
    const perPageMs = $derived(
        chapter && chapter.avg_ms != null && chapter.avg_items && chapter.avg_items > 0
            ? chapter.avg_ms / chapter.avg_items
            : null
    );

    function successPct(s: OpSummary): string {
        if (s.n === 0) return '—';
        return `${Math.round((s.ok / s.n) * 100)}%`;
    }

    async function loadSummary() {
        summaryLoading = true;
        try {
            summary = (await getCrawlerMetrics(days)).summary;
        } catch (e) {
            error = e instanceof Error ? e.message : 'Failed to load metrics.';
        } finally {
            summaryLoading = false;
        }
    }

    async function loadSeries() {
        seriesCtrl?.abort();
        const ctrl = new AbortController();
        seriesCtrl = ctrl;
        try {
            const resp = await getCrawlerMetricsSeries(days, { signal: ctrl.signal });
            series = buildSeries(resp.buckets, days);
        } catch (e) {
            if ((e as Error)?.name === 'AbortError') return;
            // A chart failure shouldn't blank the whole panel; leave prior series.
        }
    }

    async function loadOps() {
        opsLoading = true;
        try {
            const resp = await listCrawlerOps({
                op: opFilter || undefined,
                outcome: outcomeFilter || undefined,
                days,
                limit: LIMIT,
                offset: (page - 1) * LIMIT
            });
            rows = resp.items;
            total = resp.page.total ?? resp.items.length;
        } catch (e) {
            error = e instanceof Error ? e.message : 'Failed to load operations.';
        } finally {
            opsLoading = false;
        }
    }

    onMount(() => {
        loadSummary();
        loadSeries();
        loadOps();
    });

    function onWindowChange() {
        page = 1;
        loadSummary();
        loadSeries();
        loadOps();
    }
    function onOpsFilter() {
        page = 1;
        loadOps();
    }
    function onPageChange(p: number) {
        page = p;
        loadOps();
    }

    function opLabel(op: CrawlOp): string {
        return OP_LABELS[op] ?? op;
    }
    function fmtAgo(iso: string): string {
        const secs = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
        if (secs < 45) return 'just now';
        const mins = Math.round(secs / 60);
        if (mins < 60) return `${mins}m ago`;
        const hrs = Math.round(mins / 60);
        if (hrs < 24) return `${hrs}h ago`;
        return new Date(iso).toLocaleDateString();
    }
</script>

<section data-testid="crawler-metrics">
    <div class="winrow">
        <label>
            Window
            <select
                bind:value={days}
                onchange={onWindowChange}
                data-testid="crawler-metrics-window"
            >
                <option value={1}>24 hours</option>
                <option value={7}>7 days</option>
                <option value={30}>30 days</option>
                <option value={0}>All time</option>
            </select>
        </label>
    </div>

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

    {#if series}
        <div class="charts" data-testid="crawler-metrics-charts">
            <TrendChart points={series.throughput} label="Throughput (ops/bucket)" />
            <TrendChart
                points={series.success}
                label="Success rate"
                format={(v) => `${v.toFixed(0)}%`}
                accent="var(--success, #16a34a)"
            />
            <TrendChart
                points={series.duration}
                label="Avg duration"
                format={(v) => fmtDuration(v)}
                accent="var(--warning, #d97706)"
            />
        </div>
    {/if}

    <h2>Average durations by type</h2>
    {#if summaryLoading}
        <p class="muted">Loading…</p>
    {:else if orderedSummary.length === 0}
        <p class="muted" data-testid="crawler-metrics-empty">No operations recorded yet.</p>
    {:else}
        <table>
            <thead>
                <tr>
                    <th>Type</th>
                    <th class="num">Avg</th>
                    <th class="num">N</th>
                    <th class="num">OK / Fail</th>
                    <th class="num">Success</th>
                </tr>
            </thead>
            <tbody>
                {#each orderedSummary as s (s.op)}
                    <tr data-testid={`crawler-metrics-row-${s.op}`}>
                        <td>{opLabel(s.op)}</td>
                        <td class="num">{fmtDuration(s.avg_ms)}</td>
                        <td class="num">{s.n}</td>
                        <td class="num">{s.ok} / {s.failed}</td>
                        <td class="num">{successPct(s)}</td>
                    </tr>
                    {#if s.op === 'chapter' && perPageMs != null}
                        <tr class="derived" data-testid="crawler-metrics-perpage">
                            <td>└ per page (≈)</td>
                            <td class="num">{fmtDuration(perPageMs)}</td>
                            <td class="num" colspan="3"
                                >≈{Math.round(chapter?.avg_items ?? 0)} pages/chapter</td
                            >
                        </tr>
                    {/if}
                {/each}
            </tbody>
        </table>
    {/if}

    <h2>Recent operations</h2>
    <div class="toolbar">
        <select bind:value={opFilter} onchange={onOpsFilter} aria-label="Filter by type">
            <option value="">All types</option>
            {#each OP_ORDER as op (op)}<option value={op}>{opLabel(op)}</option>{/each}
        </select>
        <select
            bind:value={outcomeFilter}
            onchange={onOpsFilter}
            aria-label="Filter by outcome"
        >
            <option value="">All outcomes</option>
            <option value="ok">ok</option>
            <option value="failed">failed</option>
        </select>
    </div>

    {#if opsLoading}
        <p class="muted">Loading…</p>
    {:else if rows.length === 0}
        <p class="muted" data-testid="crawler-metrics-ops-empty">No operations match.</p>
    {:else}
        <table>
            <thead>
                <tr>
                    <th>Type</th>
                    <th>Target</th>
                    <th class="num">Dur</th>
                    <th>Outcome</th>
                    <th>When</th>
                </tr>
            </thead>
            <tbody>
                {#each rows as r (r.id)}
                    <tr
                        class:clickable={!!r.error}
                        onclick={() => r.error && (expanded = expanded === r.id ? null : r.id)}
                        data-testid={`crawler-op-${r.id}`}
                    >
                        <td>{opLabel(r.op)}</td>
                        <td>
                            {r.manga_title
                                ? `${r.manga_title}${r.chapter_number != null ? ` · Ch ${r.chapter_number}` : ''}`
                                : '—'}
                        </td>
                        <td class="num">{fmtDuration(r.duration_ms)}</td>
                        <td>
                            <span class="dot {r.outcome}"></span>{r.outcome}
                        </td>
                        <td title={new Date(r.finished_at).toLocaleString()}
                            >{fmtAgo(r.finished_at)}</td
                        >
                    </tr>
                    {#if expanded === r.id && r.error}
                        <tr class="errrow"><td colspan="5"><code>{r.error}</code></td></tr>
                    {/if}
                {/each}
            </tbody>
        </table>
        <Pager {page} {totalPages} onChange={onPageChange} testid="crawler-metrics-pager" />
    {/if}
</section>

<style>
    .winrow {
        display: flex;
        justify-content: flex-end;
        margin-bottom: var(--space-2);
    }
    .charts {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
        gap: var(--space-3) var(--space-4);
        margin-bottom: var(--space-4);
        max-width: 60rem;
    }
    label {
        font-size: var(--font-sm);
        color: var(--text-muted);
    }
    select {
        height: 32px;
        margin-left: var(--space-1);
        padding: 0 var(--space-2);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        background: var(--surface);
        color: var(--text);
        font-size: var(--font-sm);
    }
    h2 {
        margin: var(--space-4) 0 var(--space-2);
        font-size: var(--font-sm);
        color: var(--text-muted);
        text-transform: uppercase;
        letter-spacing: 0.04em;
    }
    .toolbar {
        display: flex;
        gap: var(--space-2);
        margin-bottom: var(--space-2);
    }
    table {
        width: 100%;
        border-collapse: collapse;
        max-width: 48rem;
    }
    th,
    td {
        padding: var(--space-2);
        text-align: left;
        border-bottom: 1px solid var(--border);
        font-size: var(--font-sm);
    }
    .num {
        text-align: right;
        font-variant-numeric: tabular-nums;
    }
    .derived td {
        color: var(--text-muted);
        font-size: var(--font-xs);
    }
    tr.clickable {
        cursor: pointer;
    }
    tr.clickable:hover {
        background: var(--surface);
    }
    .errrow td {
        background: var(--surface);
    }
    .errrow code {
        white-space: pre-wrap;
        word-break: break-word;
        font-size: var(--font-xs);
        color: var(--text-muted);
    }
    .dot {
        display: inline-block;
        width: 8px;
        height: 8px;
        border-radius: 50%;
        margin-right: 6px;
        vertical-align: middle;
    }
    .dot.ok {
        background: #2e7d32;
    }
    .dot.failed {
        background: var(--danger);
    }
    .muted {
        color: var(--text-muted);
    }
    .error {
        color: var(--danger);
    }
</style>
