/**
 * Protera — header live search dropdown.
 *
 * Alpine 3 component that owns the dropdown state for the header search
 * input. While the user types (debounced 200ms), this component fetches
 * `/paieska/suggest?q=...` and renders a grouped dropdown with two
 * sections: "KATEGORIJOS" (NewsCategory matches) and "PREKĖS" (Pages,
 * News, future Products).
 *
 * Server-side response shape (see app/Http/Controllers/Web/SuggestController):
 *
 *   {
 *     q: "...",
 *     groups: [{ id, label, items: [{ type, id, title, subtitle, url,
 *                                     icon, thumbnail, meta_right,
 *                                     highlight_ranges: [[start, end], ...] }] }],
 *     total: int
 *   }
 *
 * Behaviour contract (mirrors admin Ctrl+K dropdown's UX):
 *   - Min 2 chars before fetch; <2 → dropdown hidden.
 *   - 200ms input debounce wired in the blade via @input.debounce.200ms.
 *   - Race-condition guard via monotonic requestId — out-of-order
 *     responses are dropped silently.
 *   - Keyboard: ArrowDown/ArrowUp navigate, Enter opens the active row
 *     (or submits the form for full-page results when none highlighted),
 *     Esc closes the dropdown.
 *   - Disabled rows (item.url === null/empty) are skipped from keyboard
 *     navigation entirely. Server filters these out, so this is mostly
 *     defensive.
 *   - Highlights computed server-side (diacritic-aware) — client splices
 *     the title at the provided ranges and wraps each match in <mark>.
 *     We construct DOM nodes via createElement (NOT innerHTML) so that
 *     hostile titles can't ever turn into XSS — the server returns plain
 *     text and the only HTML we add is the <mark> wrapper.
 *
 * Wired by themes/Web/Protera/Resources/views/components/site-header.blade.php
 * (input bindings) + components/search-suggest.blade.php (dropdown markup).
 *
 * Imports the matching CSS so Vite produces a CSS sidecar of web.js, which
 * the layout's @vite() pulls in automatically via the manifest. The CSS
 * file is also auto-discovered as its own entry (every css/*.css becomes
 * an entry per vite.config.js); that duplicate bundle is harmless because
 * the layout never references it.
 */

import '../css/search-suggest.css';

window.searchSuggest = function () {
    return {
        // ----- State -----

        /** Current input value (bound via x-model on the input). */
        q: '',
        /** Whether dropdown is currently shown. */
        open: false,
        /** Async indicator (shown inside dropdown while a fetch is pending). */
        loading: false,
        /**
         * Server response groups.
         * @type {Array<{id:string,label:string,items:Array<Item>}>}
         */
        groups: [],
        /** Total items across all groups (mirrors response.total). */
        total: 0,
        /**
         * Current keyboard-highlighted row, or null when nothing is
         * highlighted (Enter falls back to full-page submit).
         * @type {?{groupIndex:number,itemIndex:number}}
         */
        highlighted: null,
        /** Monotonic guard — increments on every fetch; stale responses ignored. */
        requestId: 0,
        /** URL of the suggest endpoint (locale-aware; resolved in blade). */
        endpoint: '/paieska/suggest',
        /** i18n strings injected from blade. */
        labels: {
            noResults: '',
            loading: '',
            viewAll: '',
        },
        /** Min query length before we hit the network. */
        minLength: 2,

        // ----- Lifecycle -----

        /**
         * Pulls config from x-init in blade — endpoint URL + label strings.
         * Falls back to defaults if anything is missing.
         */
        init({ endpoint, labels, minLength } = {}) {
            if (typeof endpoint === 'string' && endpoint !== '') {
                this.endpoint = endpoint;
            }
            if (labels && typeof labels === 'object') {
                this.labels = {
                    noResults: labels.noResults || '',
                    loading: labels.loading || '',
                    viewAll: labels.viewAll || '',
                };
            }
            if (typeof minLength === 'number' && minLength > 0) {
                this.minLength = minLength;
            }
        },

        // ----- Computed -----

        get hasResults() {
            return this.total > 0;
        },

        /**
         * Build a flat ordered list of focusable rows for keyboard nav.
         * Each entry stores its (groupIndex, itemIndex, url).
         */
        get navItems() {
            const items = [];
            this.groups.forEach((group, gi) => {
                group.items.forEach((item, ii) => {
                    if (item.url) {
                        items.push({ groupIndex: gi, itemIndex: ii, url: item.url });
                    }
                });
            });
            return items;
        },

        // ----- Event handlers -----

        /**
         * Triggered on input change (debounced via Alpine modifier).
         * Empty / too-short queries close the dropdown without firing
         * a request.
         */
        async onInput() {
            const trimmed = (this.q || '').trim();
            this.highlighted = null;

            if (trimmed.length < this.minLength) {
                this.open = false;
                this.loading = false;
                this.groups = [];
                this.total = 0;
                return;
            }

            this.open = true;
            await this.fetchSuggestions(trimmed);
        },

        /**
         * Re-open the dropdown when the input regains focus and we have
         * stale results to show. No new fetch — the user already typed
         * the query.
         */
        onFocus() {
            const trimmed = (this.q || '').trim();
            if (trimmed.length >= this.minLength && this.hasResults) {
                this.open = true;
            }
        },

        /**
         * Esc closes the dropdown without clearing the query.
         */
        onEscape() {
            this.open = false;
            this.highlighted = null;
        },

        /**
         * ArrowDown moves the highlight one row forward (or onto the
         * first row if nothing is highlighted yet).
         */
        moveDown() {
            const items = this.navItems;
            if (items.length === 0) return;
            this.open = true;
            const current = this.currentNavIndex();
            const next = Math.min(items.length - 1, current + 1);
            const row = items[next];
            this.highlighted = { groupIndex: row.groupIndex, itemIndex: row.itemIndex };
        },

        /**
         * ArrowUp moves the highlight one row backward; from the first
         * row it clears the highlight (input keeps focus, Enter then
         * submits the form for full-page results).
         */
        moveUp() {
            const items = this.navItems;
            if (items.length === 0) return;
            const current = this.currentNavIndex();
            if (current <= 0) {
                this.highlighted = null;
                return;
            }
            const row = items[current - 1];
            this.highlighted = { groupIndex: row.groupIndex, itemIndex: row.itemIndex };
        },

        /**
         * Enter handler. Two distinct outcomes:
         *
         *   1. A row is highlighted → navigate to that row's URL.
         *   2. No row highlighted   → return false so the form's native
         *                              submit fires (full results page at
         *                              /paieska?q=...).
         */
        onEnter(event) {
            if (this.highlighted === null) {
                // Let the form submit naturally — full results page.
                return true;
            }
            event.preventDefault();
            event.stopPropagation();
            const { groupIndex, itemIndex } = this.highlighted;
            const item = this.groups?.[groupIndex]?.items?.[itemIndex];
            if (item && item.url) {
                window.location.href = item.url;
            }
            return false;
        },

        /**
         * Click on dropdown row — same behaviour as Enter on highlighted.
         */
        selectItem(item) {
            if (item && item.url) {
                window.location.href = item.url;
            }
        },

        /**
         * Move highlight on hover so keyboard + mouse don't fight each
         * other (mouse over a row "selects" it for Enter).
         */
        setHighlight(groupIndex, itemIndex) {
            this.highlighted = { groupIndex, itemIndex };
        },

        /**
         * Click-outside / focus-outside — close the dropdown.
         */
        close() {
            this.open = false;
        },

        // ----- Helpers -----

        currentNavIndex() {
            if (this.highlighted === null) return -1;
            return this.navItems.findIndex(
                (it) =>
                    it.groupIndex === this.highlighted.groupIndex &&
                    it.itemIndex === this.highlighted.itemIndex,
            );
        },

        isHighlighted(groupIndex, itemIndex) {
            return (
                this.highlighted !== null &&
                this.highlighted.groupIndex === groupIndex &&
                this.highlighted.itemIndex === itemIndex
            );
        },

        // ----- Network -----

        async fetchSuggestions(q) {
            this.loading = true;
            const myId = ++this.requestId;

            try {
                const url = `${this.endpoint}?q=${encodeURIComponent(q)}`;
                const res = await fetch(url, {
                    method: 'GET',
                    credentials: 'same-origin',
                    headers: {
                        Accept: 'application/json',
                        'X-Requested-With': 'XMLHttpRequest',
                    },
                });

                // Drop stale responses — newer query already in flight.
                if (myId !== this.requestId) return;

                if (!res.ok) {
                    this.groups = [];
                    this.total = 0;
                    return;
                }

                const data = await res.json();
                if (myId !== this.requestId) return;

                this.groups = Array.isArray(data.groups) ? data.groups : [];
                this.total = typeof data.total === 'number' ? data.total : 0;
                this.highlighted = null;
            } catch (err) {
                if (myId !== this.requestId) return;
                // eslint-disable-next-line no-console
                console.warn('[searchSuggest] fetch failed', err);
                this.groups = [];
                this.total = 0;
            } finally {
                if (myId === this.requestId) {
                    this.loading = false;
                }
            }
        },

        // ----- Title rendering with <mark> -----

        /**
         * Wrap matched ranges in <mark> elements and append the result
         * to a target node. Uses createElement (no innerHTML) — server
         * returns plain text, the only HTML we add is the wrapper.
         *
         * Called from x-init on the title span so the DOM nodes are
         * built at render time and re-built whenever Alpine re-renders
         * the row.
         *
         * @param {HTMLElement} el     Span container into which we
         *                             insert the title nodes.
         * @param {string}      title  Original title text.
         * @param {Array<Array<number>>} ranges  Highlight ranges, sorted
         *                                       and non-overlapping.
         */
        renderTitle(el, title, ranges) {
            // Defensive — if anything is missing, fall back to plain text.
            if (!el) return;
            if (!title) {
                el.textContent = '';
                return;
            }

            // Clear previous children before re-rendering.
            while (el.firstChild) el.removeChild(el.firstChild);

            if (!Array.isArray(ranges) || ranges.length === 0) {
                el.textContent = title;
                return;
            }

            let cursor = 0;
            for (const range of ranges) {
                if (!Array.isArray(range) || range.length < 2) continue;
                const [start, end] = range;
                if (typeof start !== 'number' || typeof end !== 'number') continue;
                if (start < cursor || end <= start || end > title.length) continue;

                if (start > cursor) {
                    el.appendChild(document.createTextNode(title.slice(cursor, start)));
                }
                const mark = document.createElement('mark');
                mark.className = 'search-suggest-mark';
                mark.textContent = title.slice(start, end);
                el.appendChild(mark);
                cursor = end;
            }

            if (cursor < title.length) {
                el.appendChild(document.createTextNode(title.slice(cursor)));
            }
        },
    };
};
