<script lang="ts">
    import { onMount } from 'svelte';
    import { browser } from '$app/environment';
    import { goto } from '$app/navigation';
    import { page } from '$app/stores';
    import {
        listMangas,
        type MangaCard as MangaCardData,
        type MangaSort,
        type MangaStatus
    } from '$lib/api/mangas';
    import { listGenres, type Genre } from '$lib/api/genres';
    import { listTags, type Tag } from '$lib/api/tags';
    import Chip from '$lib/components/Chip.svelte';
    import MangaCard from '$lib/components/MangaCard.svelte';
    import Search from '@lucide/svelte/icons/search';
    import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal';
    import Plus from '@lucide/svelte/icons/plus';

    let mangas: MangaCardData[] = $state([]);
    let search = $state('');
    let sort: MangaSort = $state('recent');
    let statusFilter = $state<'' | MangaStatus>('');
    let selectedGenres = $state<Genre[]>([]);
    let selectedTags = $state<Tag[]>([]);
    let allGenres = $state<Genre[]>([]);
    let tagDraft = $state('');
    let tagSuggestions = $state<Tag[]>([]);
    let tagSuggestHighlight = $state(-1);
    let suggestTimer: ReturnType<typeof setTimeout> | null = null;
    // Monotonic counter — discards stale fetch results so a fast typist
    // can't see an earlier query's results overwrite the current one.
    let suggestSeq = 0;
    const tagSuggestListId = 'tag-filter-suggest-list';
    let filtersOpen = $state(false);
    let total: number | null = $state(null);
    let loading = $state(true);
    let error: string | null = $state(null);

    const activeFilterCount = $derived(
        (statusFilter ? 1 : 0) + selectedGenres.length + selectedTags.length
    );

    async function load() {
        loading = true;
        error = null;
        try {
            const result = await listMangas({
                search: search.trim() || undefined,
                status: statusFilter || undefined,
                genreIds: selectedGenres.map((g) => g.id),
                tagIds: selectedTags.map((t) => t.id),
                sort
            });
            mangas = result.items;
            total = result.page.total;
        } catch (e) {
            error = (e as Error).message;
        } finally {
            loading = false;
        }
    }

    function syncUrl() {
        if (!browser) return;
        const params = new URLSearchParams();
        if (search.trim()) params.set('q', search.trim());
        if (sort !== 'recent') params.set('sort', sort);
        if (statusFilter) params.set('status', statusFilter);
        if (selectedGenres.length)
            params.set('genres', selectedGenres.map((g) => g.id).join(','));
        if (selectedTags.length)
            params.set('tags', selectedTags.map((t) => t.id).join(','));
        const qs = params.toString();
        const url = qs ? `/?${qs}` : '/';
        goto(url, { replaceState: true, keepFocus: true, noScroll: true });
    }

    async function hydrateFromUrl() {
        // Parse the query and resolve the supplied ids back to full Tag /
        // Genre objects so the chip rows render real labels.
        const url = new URL($page.url);
        search = url.searchParams.get('q') ?? '';
        const s = url.searchParams.get('sort');
        if (s === 'title' || s === 'recent') sort = s;
        const st = url.searchParams.get('status');
        statusFilter = st === 'ongoing' || st === 'completed' ? st : '';
        const genreIds = (url.searchParams.get('genres') ?? '')
            .split(',')
            .filter(Boolean);
        if (genreIds.length) {
            selectedGenres = allGenres.filter((g) => genreIds.includes(g.id));
        }
        const tagIds = (url.searchParams.get('tags') ?? '')
            .split(',')
            .filter(Boolean);
        if (tagIds.length) {
            // listTags doesn't take ids; fetch a generous page and filter.
            // Tag count is small in the near term, so this is fine.
            const tags = await listTags({ limit: 50 });
            selectedTags = tags.filter((t) => tagIds.includes(t.id));
        }
        // Open the filters panel if anything is active so the user can see why.
        if (statusFilter || selectedGenres.length || selectedTags.length) {
            filtersOpen = true;
        }
    }

    async function onSubmit(e: SubmitEvent) {
        e.preventDefault();
        syncUrl();
        await load();
    }

    function onSortChange() {
        syncUrl();
        load();
    }

    function onStatusChange() {
        syncUrl();
        load();
    }

    function toggleGenre(g: Genre) {
        selectedGenres = selectedGenres.some((x) => x.id === g.id)
            ? selectedGenres.filter((x) => x.id !== g.id)
            : [...selectedGenres, g];
        syncUrl();
        load();
    }

    function removeTag(t: Tag) {
        selectedTags = selectedTags.filter((x) => x.id !== t.id);
        syncUrl();
        load();
    }

    function pickTag(t: Tag) {
        if (!selectedTags.some((x) => x.id === t.id)) {
            selectedTags = [...selectedTags, t];
        }
        tagDraft = '';
        tagSuggestions = [];
        tagSuggestHighlight = -1;
        syncUrl();
        load();
    }

    function onTagDraftInput() {
        tagSuggestHighlight = -1;
        if (suggestTimer) clearTimeout(suggestTimer);
        const q = tagDraft.trim();
        if (q.length === 0) {
            tagSuggestions = [];
            suggestSeq++;
            return;
        }
        const seq = ++suggestSeq;
        suggestTimer = setTimeout(async () => {
            try {
                const matched = await listTags({ search: q, limit: 6 });
                if (seq !== suggestSeq) return;
                const chosen = new Set(selectedTags.map((t) => t.id));
                tagSuggestions = matched.filter((m) => !chosen.has(m.id));
            } catch {
                if (seq === suggestSeq) tagSuggestions = [];
            }
        }, 180);
    }

    function onTagFilterKeydown(e: KeyboardEvent) {
        if (e.key === 'ArrowDown' && tagSuggestions.length > 0) {
            e.preventDefault();
            tagSuggestHighlight = (tagSuggestHighlight + 1) % tagSuggestions.length;
        } else if (e.key === 'ArrowUp' && tagSuggestions.length > 0) {
            e.preventDefault();
            tagSuggestHighlight =
                tagSuggestHighlight <= 0
                    ? tagSuggestions.length - 1
                    : tagSuggestHighlight - 1;
        } else if (e.key === 'Enter' && tagSuggestHighlight >= 0) {
            e.preventDefault();
            pickTag(tagSuggestions[tagSuggestHighlight]);
        } else if (e.key === 'Escape') {
            tagSuggestions = [];
            tagSuggestHighlight = -1;
        }
    }

    function clearFilters() {
        statusFilter = '';
        selectedGenres = [];
        selectedTags = [];
        syncUrl();
        load();
    }

    onMount(async () => {
        try {
            allGenres = await listGenres();
        } catch {
            // Filter UI still loads with an empty genre list rather than blocking.
        }
        await hydrateFromUrl();
        await load();
    });
</script>

<h1>Mangas</h1>

<form
    onsubmit={onSubmit}
    action="javascript:void(0)"
    class="controls"
>
    <div class="search-row">
        <input
            class="search"
            type="search"
            bind:value={search}
            placeholder="Search by title or author"
            data-testid="search-input"
        />
        <button class="icon-btn primary" type="submit" aria-label="Search" title="Search">
            <Search size={18} aria-hidden="true" />
        </button>
        <button
            type="button"
            class="filters-toggle"
            class:active={filtersOpen}
            onclick={() => (filtersOpen = !filtersOpen)}
            aria-expanded={filtersOpen}
            aria-controls="filters-panel"
            data-testid="filters-toggle"
        >
            <SlidersHorizontal size={16} aria-hidden="true" />
            <span>Filters</span>
            {#if activeFilterCount > 0}
                <span class="filter-count">{activeFilterCount}</span>
            {/if}
        </button>
    </div>
    {#if filtersOpen}
        <div class="filters-panel" id="filters-panel" data-testid="filters-panel">
            <div class="filter-group">
                <span class="filter-label">Status</span>
                <div class="status-row">
                    <label>
                        <input
                            type="radio"
                            name="status"
                            value=""
                            bind:group={statusFilter}
                            onchange={onStatusChange}
                        />
                        <span>Any</span>
                    </label>
                    <label>
                        <input
                            type="radio"
                            name="status"
                            value="ongoing"
                            bind:group={statusFilter}
                            onchange={onStatusChange}
                        />
                        <span>Ongoing</span>
                    </label>
                    <label>
                        <input
                            type="radio"
                            name="status"
                            value="completed"
                            bind:group={statusFilter}
                            onchange={onStatusChange}
                        />
                        <span>Completed</span>
                    </label>
                </div>
            </div>

            <div class="filter-group">
                <span class="filter-label">Genres (all must match)</span>
                <div class="filter-chip-row">
                    {#each allGenres as g (g.id)}
                        {@const on = selectedGenres.some((x) => x.id === g.id)}
                        <button
                            type="button"
                            class="genre-pill"
                            class:active={on}
                            onclick={() => toggleGenre(g)}
                            data-testid={`genre-filter-${g.name}`}
                        >
                            {g.name}
                        </button>
                    {/each}
                </div>
            </div>

            <div class="filter-group">
                <span class="filter-label">Tags (all must match)</span>
                {#if selectedTags.length > 0}
                    <div class="filter-chip-row">
                        {#each selectedTags as t (t.id)}
                            <Chip
                                label={t.name}
                                variant="primary"
                                onRemove={() => removeTag(t)}
                                removeLabel={`Remove tag ${t.name}`}
                                testid={`tag-filter-chip-${t.name}`}
                            />
                        {/each}
                    </div>
                {/if}
                <div class="tag-search">
                    <input
                        type="text"
                        role="combobox"
                        bind:value={tagDraft}
                        oninput={onTagDraftInput}
                        onkeydown={onTagFilterKeydown}
                        placeholder="Type to find a tag"
                        maxlength="64"
                        aria-label="Find a tag"
                        aria-controls={tagSuggestListId}
                        aria-expanded={tagSuggestions.length > 0}
                        aria-autocomplete="list"
                        aria-activedescendant={tagSuggestHighlight >= 0
                            ? `${tagSuggestListId}-opt-${tagSuggestHighlight}`
                            : undefined}
                        data-testid="tag-filter-input"
                    />
                    {#if tagSuggestions.length > 0}
                        <ul class="tag-suggestions" role="listbox" id={tagSuggestListId}>
                            {#each tagSuggestions as s, i (s.id)}
                                <li
                                    id={`${tagSuggestListId}-opt-${i}`}
                                    role="option"
                                    aria-selected={i === tagSuggestHighlight}
                                    class:active={i === tagSuggestHighlight}
                                >
                                    <button
                                        type="button"
                                        tabindex="-1"
                                        onmouseenter={() => (tagSuggestHighlight = i)}
                                        onclick={() => pickTag(s)}
                                        data-testid={`tag-filter-suggestion-${s.name}`}
                                    >
                                        <Plus size={12} aria-hidden="true" />
                                        {s.name}
                                    </button>
                                </li>
                            {/each}
                        </ul>
                    {/if}
                </div>
            </div>

            {#if activeFilterCount > 0}
                <button type="button" class="clear" onclick={clearFilters}>
                    Clear filters
                </button>
            {/if}
        </div>
    {/if}
    <div class="config-row">
        <label class="sort">
            <span>Sort</span>
            <select bind:value={sort} onchange={onSortChange} data-testid="sort-select">
                <option value="recent">Recent</option>
                <option value="title">Title (A→Z)</option>
            </select>
        </label>
    </div>
</form>

{#if loading}
    <p class="status" data-testid="loading">Loading…</p>
{:else if error}
    <p class="error" data-testid="error" role="alert">{error}</p>
{:else if mangas.length === 0}
    <p class="status" data-testid="empty">No mangas yet. <a href="/upload">Upload one</a>.</p>
{:else}
    {#if total !== null}
        <p class="count" data-testid="manga-total">
            Showing {mangas.length} of {total}
        </p>
    {/if}
    <ul class="manga-grid" data-testid="manga-list">
        {#each mangas as m (m.id)}
            <MangaCard manga={m} authors={m.authors} genres={m.genres} />
        {/each}
    </ul>
{/if}

<style>
    .controls {
        display: flex;
        flex-direction: column;
        gap: var(--space-3);
        margin-bottom: var(--space-4);
    }

    .search-row {
        display: flex;
        gap: var(--space-2);
        align-items: center;
        flex-wrap: wrap;
    }

    .search {
        flex: 1;
        min-width: 0;
        max-width: 28rem;
    }

    .filters-toggle {
        display: inline-flex;
        align-items: center;
        gap: var(--space-2);
        padding: 0 var(--space-3);
        height: 36px;
        background: var(--surface);
        border: 1px solid var(--border-strong);
        color: var(--text);
        cursor: pointer;
    }

    .filters-toggle:hover,
    .filters-toggle.active {
        background: var(--surface-elevated);
        border-color: var(--primary);
    }

    .filter-count {
        background: var(--primary);
        color: var(--primary-contrast);
        border-radius: var(--radius-pill);
        font-size: var(--font-xs);
        padding: 0 6px;
        min-width: 18px;
        text-align: center;
    }

    .filters-panel {
        display: flex;
        flex-direction: column;
        gap: var(--space-3);
        padding: var(--space-3);
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
    }

    .filter-group {
        display: flex;
        flex-direction: column;
        gap: var(--space-2);
    }

    .filter-label {
        color: var(--text-muted);
        font-size: var(--font-sm);
        font-weight: var(--weight-medium);
    }

    .status-row {
        display: flex;
        gap: var(--space-3);
        flex-wrap: wrap;
    }

    .status-row label {
        display: inline-flex;
        align-items: center;
        gap: var(--space-1);
        color: var(--text);
        font-size: var(--font-sm);
        cursor: pointer;
    }

    .filter-chip-row {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-2);
    }

    .genre-pill {
        display: inline-flex;
        align-items: center;
        padding: 4px var(--space-2);
        background: var(--surface-elevated);
        border: 1px solid var(--border);
        color: var(--text);
        border-radius: var(--radius-pill);
        font-size: var(--font-xs);
        cursor: pointer;
    }

    .genre-pill:hover {
        border-color: var(--primary);
    }

    .genre-pill.active {
        background: var(--primary-soft-bg);
        border-color: var(--primary);
        color: var(--primary);
    }

    .tag-search {
        position: relative;
        max-width: 20rem;
    }

    .tag-search input {
        width: 100%;
    }

    .tag-suggestions {
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        margin: var(--space-1) 0 0;
        padding: var(--space-1) 0;
        list-style: none;
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
        z-index: var(--z-dropdown);
    }

    .tag-suggestions button {
        width: 100%;
        background: transparent;
        border: 0;
        text-align: left;
        display: inline-flex;
        align-items: center;
        gap: var(--space-2);
        padding: var(--space-1) var(--space-3);
        color: var(--text);
        cursor: pointer;
        font-size: var(--font-sm);
    }

    .tag-suggestions li.active button,
    .tag-suggestions button:hover {
        background: var(--primary-soft-bg);
    }

    .clear {
        align-self: flex-start;
        background: transparent;
        border: 1px solid var(--border);
        color: var(--text-muted);
        font-size: var(--font-sm);
    }

    .config-row {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-3);
        align-items: center;
    }

    .sort {
        display: flex;
        align-items: center;
        gap: var(--space-2);
        color: var(--text-muted);
        font-size: var(--font-sm);
    }

    .sort select {
        width: auto;
    }

    .icon-btn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 36px;
        height: 36px;
        padding: 0;
    }

    .icon-btn.primary {
        background: var(--primary);
        color: var(--primary-contrast);
        border: 1px solid var(--primary);
    }

    .icon-btn.primary:hover:not(:disabled) {
        background: var(--primary-hover);
        border-color: var(--primary-hover);
    }

    .status {
        color: var(--text-muted);
    }

    .error {
        color: var(--danger);
    }

    .count {
        color: var(--text-muted);
        font-size: var(--font-sm);
        margin: var(--space-2) 0;
    }

    .manga-grid {
        list-style: none;
        padding: 0;
        margin: 0;
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
        gap: var(--space-4);
    }
</style>
