<script lang="ts">
    import { onMount } from 'svelte';
    import Pager from '$lib/components/Pager.svelte';
    import { fmtAgo } from '$lib/format';
    import { listAuditLog, type AuditEntry } from '$lib/api/admin';

    const LIMIT = 25;

    let rows = $state<AuditEntry[]>([]);
    let total = $state(0);
    let page = $state(1);
    let targetKind = $state('');
    let action = $state('');
    let days = $state(0);
    let loading = $state(true);
    let error = $state<string | null>(null);
    let expanded = $state<string | null>(null);
    // Cancels the in-flight request when a newer one starts, so a slow
    // response can't overwrite a fresher one (out-of-order races).
    let inflight: AbortController | null = null;

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

    async function load() {
        inflight?.abort();
        const ctrl = new AbortController();
        inflight = ctrl;
        loading = true;
        error = null;
        try {
            const resp = await listAuditLog(
                {
                    targetKind: targetKind || undefined,
                    action: action || undefined,
                    days: days || undefined,
                    limit: LIMIT,
                    offset: (page - 1) * LIMIT
                },
                { signal: ctrl.signal }
            );
            rows = resp.items;
            total = resp.page.total ?? resp.items.length;
        } catch (e) {
            if ((e as Error)?.name === 'AbortError') return; // superseded
            error = e instanceof Error ? e.message : 'Failed to load audit log.';
        } finally {
            if (inflight === ctrl) loading = false;
        }
    }

    onMount(load);

    function applyFilters() {
        page = 1;
        load();
    }
    function onPageChange(p: number) {
        page = p;
        load();
    }

    function actor(r: AuditEntry): string {
        if (r.actor_username) return r.actor_username;
        if (r.actor_user_id) return '(deleted user)';
        return 'system';
    }
    function targetLabel(r: AuditEntry): string {
        if (r.target_id) return `${r.target_kind} · ${r.target_id.slice(0, 8)}`;
        return r.target_kind;
    }
    function pretty(payload: unknown): string {
        try {
            return JSON.stringify(payload, null, 2);
        } catch {
            return String(payload);
        }
    }

    // Closed vocab of target kinds; `action` is a free-text exact filter
    // (the action set grows over time, so a text box stays future-proof).
    const TARGET_KINDS = ['', 'crawler', 'manga', 'chapter', 'page', 'settings', 'user'];
    const WINDOWS: { value: number; label: string }[] = [
        { value: 0, label: 'All time' },
        { value: 1, label: 'Last 24h' },
        { value: 7, label: 'Last 7d' },
        { value: 30, label: 'Last 30d' }
    ];
</script>

<section class="audit" data-testid="audit-table">
    <div class="toolbar">
        <select
            bind:value={targetKind}
            onchange={applyFilters}
            aria-label="Filter by target kind"
            data-testid="audit-target-kind"
        >
            {#each TARGET_KINDS as k (k)}
                <option value={k}>{k === '' ? 'All targets' : k}</option>
            {/each}
        </select>
        <select
            bind:value={days}
            onchange={applyFilters}
            aria-label="Time window"
            data-testid="audit-window"
        >
            {#each WINDOWS as w (w.value)}<option value={w.value}>{w.label}</option>{/each}
        </select>
        <input
            class="action-filter"
            type="search"
            placeholder="Filter by action (exact)…"
            bind:value={action}
            onchange={applyFilters}
            aria-label="Filter by action"
            data-testid="audit-action"
        />
    </div>

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

    {#if loading}
        <p class="muted" data-testid="audit-loading">Loading…</p>
    {:else if rows.length === 0}
        <p class="muted" data-testid="audit-empty">No audit entries match.</p>
    {:else}
        <table>
            <thead>
                <tr>
                    <th>When</th>
                    <th>Actor</th>
                    <th>Action</th>
                    <th>Target</th>
                    <th class="actions"></th>
                </tr>
            </thead>
            <tbody>
                {#each rows as r (r.id)}
                    <tr
                        class="clickable"
                        onclick={() => (expanded = expanded === r.id ? null : r.id)}
                        data-testid={`audit-row-${r.id}`}
                    >
                        <td title={new Date(r.at).toLocaleString()}>{fmtAgo(r.at)}</td>
                        <td>{actor(r)}</td>
                        <td><span class="action">{r.action}</span></td>
                        <td class="target">{targetLabel(r)}</td>
                        <td class="actions">
                            <!-- Real button so the payload toggle is keyboard-
                                 reachable; the row onclick is a mouse convenience. -->
                            <button
                                type="button"
                                class="chev"
                                aria-expanded={expanded === r.id}
                                aria-label={expanded === r.id ? 'Collapse payload' : 'Expand payload'}
                                onclick={(e) => {
                                    e.stopPropagation();
                                    expanded = expanded === r.id ? null : r.id;
                                }}
                            >
                                {expanded === r.id ? '▾' : '▸'}
                            </button>
                        </td>
                    </tr>
                    {#if expanded === r.id}
                        <tr class="payrow">
                            <td colspan="5"><pre>{pretty(r.payload)}</pre></td>
                        </tr>
                    {/if}
                {/each}
            </tbody>
        </table>
        <Pager {page} {totalPages} onChange={onPageChange} testid="audit-pager" />
    {/if}
</section>

<style>
    .toolbar {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-2);
        margin-bottom: var(--space-3);
    }
    select,
    .action-filter {
        height: 36px;
        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);
    }
    .action-filter {
        min-width: 14rem;
    }
    .muted {
        color: var(--text-muted);
    }
    table {
        width: 100%;
        border-collapse: collapse;
    }
    th,
    td {
        padding: var(--space-2);
        text-align: left;
        border-bottom: 1px solid var(--border);
        font-size: var(--font-sm);
    }
    .action {
        font-family: var(--font-mono, monospace);
        font-size: var(--font-xs);
    }
    .target {
        color: var(--text-muted);
        font-size: var(--font-xs);
    }
    .actions {
        text-align: right;
        color: var(--text-muted);
    }
    .chev {
        background: none;
        border: none;
        color: var(--text-muted);
        cursor: pointer;
        font-size: var(--font-sm);
        padding: 2px var(--space-1);
        border-radius: var(--radius-sm);
    }
    .chev:hover {
        color: var(--text);
        background: var(--surface-elevated);
    }
    tr.clickable {
        cursor: pointer;
    }
    tr.clickable:hover {
        background: var(--surface);
    }
    .payrow td {
        background: var(--surface);
    }
    .payrow pre {
        margin: 0;
        white-space: pre-wrap;
        word-break: break-word;
        font-size: var(--font-xs);
        color: var(--text-muted);
    }
    .error {
        color: var(--danger, #dc2626);
    }
</style>
