/**
 * typeahead-multiselect.js
 *
 * Reusable typeahead + multi-select checkbox component for WHP admin.
 * Vanilla JS, no jQuery, no third-party deps. Targets ES2017+ (matches the
 * rest of bootstrap5-native.js usage).
 *
 * Implementation note: this file deliberately avoids assigning attacker-
 * influenced strings (item labels/values) into innerHTML. Everything
 * user-supplied goes through textContent or createElement. The only
 * innerHTML assignment is the initial shell built from a static string
 * literal in _build().
 *
 * UX contract:
 *   - Search input filters the item list as the operator types (debounced).
 *   - Match is a case-insensitive substring match anywhere in the label.
 *   - Filtered matches show the matched substring wrapped in a <mark> tag.
 *   - Already-checked items remain visible at the top of the list even if
 *     they no longer match the current filter (so the operator can see what
 *     they have selected).
 *   - Items can be flagged disabled (e.g. the site's primary FQDN); they
 *     render greyed-out and are not selectable.
 *
 * Theming: pulls from --whp-* tokens declared in bootstrap5-whp-theme.css.
 * Class prefix is .whp-tams-* so it does not collide with Bootstrap or the
 * legacy Choices.js styling.
 *
 * Public API:
 *
 *   const tams = new TypeaheadMultiselect(rootEl, {
 *       items: [{ value: 'foo.example.com', label: 'foo.example.com', meta: {...} }, ...],
 *       selected: ['existing.example.com'],         // optional pre-selected values
 *       disabledValues: ['primary.example.com'],    // optional non-selectable values
 *       placeholder: 'Search domains...',           // optional search placeholder
 *       emptyMessage: 'No domains match.',          // optional empty-state text
 *       noItemsMessage: 'No domains available.',    // optional zero-items text
 *       debounceMs: 250,                            // optional, default 250
 *       maxVisible: 200,                            // optional cap, default 200
 *       onChange: (selectedValues) => {},           // optional change callback
 *   });
 *
 *   tams.getSelected()              // -> string[] of currently-checked values
 *   tams.setSelected(values)        // replace selection
 *   tams.setItems(items)            // replace the item pool (preserves selection where possible)
 *   tams.setDisabled(values)        // replace the disabled-value set
 *   tams.clear()                    // clear selection + search
 *   tams.focus()                    // focus the search input
 *   tams.destroy()                  // remove DOM + listeners
 *
 * Constructor also stashes the instance on rootEl.__whpTypeaheadMultiselect
 * so other code can find it via the host element.
 *
 * Static helper:
 *   TypeaheadMultiselect.fromSelect(rootEl, sourceSelectEl, opts)
 *     Convenience constructor that bootstraps items + selected from a
 *     legacy <select multiple> element. The source select is detached from
 *     the form so it won't double-submit.
 */
(function (global) {
    'use strict';

    function debounce(fn, ms) {
        let t = null;
        return function () {
            const args = arguments;
            const self = this;
            if (t) clearTimeout(t);
            t = setTimeout(function () { fn.apply(self, args); }, ms);
        };
    }

    /**
     * Append the `label` text to `parent`, wrapping any case-insensitive
     * occurrence of `query` in a <mark class="whp-tams-mark"> element. All
     * inserts go through document.createTextNode / createElement, so the
     * label content is rendered as text, not parsed as HTML.
     */
    function appendHighlightedLabel(parent, label, query) {
        const text = String(label == null ? '' : label);
        if (!query) {
            parent.appendChild(document.createTextNode(text));
            return;
        }
        const q = String(query).toLowerCase();
        if (!q.length) {
            parent.appendChild(document.createTextNode(text));
            return;
        }
        const lower = text.toLowerCase();
        let i = 0;
        while (i < text.length) {
            const hit = lower.indexOf(q, i);
            if (hit === -1) {
                parent.appendChild(document.createTextNode(text.slice(i)));
                break;
            }
            if (hit > i) {
                parent.appendChild(document.createTextNode(text.slice(i, hit)));
            }
            const mark = document.createElement('mark');
            mark.className = 'whp-tams-mark';
            mark.appendChild(document.createTextNode(text.slice(hit, hit + q.length)));
            parent.appendChild(mark);
            i = hit + q.length;
        }
    }

    function TypeaheadMultiselect(root, opts) {
        if (!root || root.nodeType !== 1) {
            throw new Error('TypeaheadMultiselect: root element is required');
        }
        opts = opts || {};
        this.root = root;
        this.items = Array.isArray(opts.items) ? opts.items.slice() : [];
        this.selected = new Set(Array.isArray(opts.selected) ? opts.selected : []);
        this.disabled = new Set(Array.isArray(opts.disabledValues) ? opts.disabledValues : []);
        this.placeholder = opts.placeholder || 'Search...';
        this.emptyMessage = opts.emptyMessage || 'No items match your search.';
        this.noItemsMessage = opts.noItemsMessage || 'No items available.';
        this.debounceMs = typeof opts.debounceMs === 'number' ? opts.debounceMs : 250;
        this.maxVisible = typeof opts.maxVisible === 'number' ? opts.maxVisible : 200;
        this.onChange = typeof opts.onChange === 'function' ? opts.onChange : null;
        this.query = '';
        this._idPrefix = 'whp-tams-' + Math.random().toString(36).slice(2, 10);

        this._build();
        this._bind();
        this._render();

        root.__whpTypeaheadMultiselect = this;
    }

    TypeaheadMultiselect.prototype._build = function () {
        // Static shell. All dynamic content is later set via textContent or
        // createElement; nothing user-supplied lands in innerHTML.
        this.root.classList.add('whp-tams');
        const wrap = document.createElement('div');
        wrap.className = 'whp-tams-search-wrap';
        const input = document.createElement('input');
        input.type = 'text';
        input.className = 'form-control whp-tams-search';
        input.placeholder = this.placeholder;
        input.autocomplete = 'off';
        input.spellcheck = false;
        wrap.appendChild(input);
        const count = document.createElement('span');
        count.className = 'whp-tams-count';
        count.setAttribute('aria-hidden', 'true');
        wrap.appendChild(count);

        const list = document.createElement('div');
        list.className = 'whp-tams-list';
        list.setAttribute('role', 'listbox');
        list.setAttribute('aria-multiselectable', 'true');

        // Clear root, then attach.
        while (this.root.firstChild) this.root.removeChild(this.root.firstChild);
        this.root.appendChild(wrap);
        this.root.appendChild(list);

        this.searchEl = input;
        this.listEl = list;
        this.countEl = count;
    };

    TypeaheadMultiselect.prototype._bind = function () {
        const self = this;
        const debounced = debounce(function (val) {
            self.query = val;
            self._render();
        }, this.debounceMs);
        this.searchEl.addEventListener('input', function () {
            debounced(self.searchEl.value.trim());
        });
        // Toggle handler is delegated.
        this.listEl.addEventListener('change', function (ev) {
            const cb = ev.target;
            if (!cb || cb.tagName !== 'INPUT' || cb.type !== 'checkbox') return;
            const value = cb.value;
            if (self.disabled.has(value)) {
                cb.checked = false;
                return;
            }
            if (cb.checked) self.selected.add(value);
            else self.selected.delete(value);
            self._updateCount();
            if (self.onChange) self.onChange(self.getSelected());
        });
    };

    TypeaheadMultiselect.prototype._filtered = function () {
        const q = this.query.toLowerCase();
        const selected = [];
        const matched = [];
        for (let i = 0; i < this.items.length; i++) {
            const it = this.items[i];
            const label = String(it.label != null ? it.label : it.value);
            const isSelected = this.selected.has(it.value);
            const isMatch = q === '' || label.toLowerCase().indexOf(q) !== -1;
            if (isSelected) {
                selected.push(it);
            } else if (isMatch) {
                matched.push(it);
            }
        }
        // Order: selected first (always shown), then filter-matches.
        const visible = selected.concat(matched);
        const truncated = visible.length > this.maxVisible
            ? visible.length - this.maxVisible
            : 0;
        return {
            visible: visible.slice(0, this.maxVisible),
            truncated: truncated,
        };
    };

    TypeaheadMultiselect.prototype._render = function () {
        // Drop existing rows.
        while (this.listEl.firstChild) this.listEl.removeChild(this.listEl.firstChild);

        if (this.items.length === 0) {
            const empty = document.createElement('div');
            empty.className = 'whp-tams-empty';
            empty.textContent = this.noItemsMessage;
            this.listEl.appendChild(empty);
            this._updateCount();
            return;
        }

        const f = this._filtered();
        if (f.visible.length === 0) {
            const empty = document.createElement('div');
            empty.className = 'whp-tams-empty';
            empty.textContent = this.emptyMessage;
            this.listEl.appendChild(empty);
            this._updateCount();
            return;
        }

        const frag = document.createDocumentFragment();
        for (let i = 0; i < f.visible.length; i++) {
            const it = f.visible[i];
            const id = this._idPrefix + '-' + i;
            const label = String(it.label != null ? it.label : it.value);
            const isChecked = this.selected.has(it.value);
            const isDisabled = this.disabled.has(it.value);

            const row = document.createElement('div');
            row.className = 'whp-tams-row form-check';
            if (isChecked) row.classList.add('whp-tams-row-checked');
            if (isDisabled) row.classList.add('whp-tams-row-disabled');

            const cb = document.createElement('input');
            cb.className = 'form-check-input whp-tams-cb';
            cb.type = 'checkbox';
            cb.id = id;
            cb.value = it.value;
            cb.checked = isChecked;
            cb.disabled = isDisabled;
            row.appendChild(cb);

            const lab = document.createElement('label');
            lab.className = 'form-check-label whp-tams-label';
            lab.htmlFor = id;
            appendHighlightedLabel(lab, label, this.query);
            if (isDisabled && it.disabledReason) {
                const reason = document.createElement('span');
                reason.className = 'whp-tams-reason text-muted';
                reason.textContent = ' (' + String(it.disabledReason) + ')';
                lab.appendChild(reason);
            }
            row.appendChild(lab);

            frag.appendChild(row);
        }
        if (f.truncated > 0) {
            const trunc = document.createElement('div');
            trunc.className = 'whp-tams-truncated text-muted';
            trunc.textContent = '... ' + f.truncated + ' more match' +
                (f.truncated === 1 ? '' : 'es') +
                '. Narrow your search to see them.';
            frag.appendChild(trunc);
        }
        this.listEl.appendChild(frag);
        this._updateCount();
    };

    TypeaheadMultiselect.prototype._updateCount = function () {
        if (!this.countEl) return;
        const n = this.selected.size;
        if (n === 0) {
            this.countEl.textContent = '';
            this.countEl.classList.remove('has-selection');
        } else {
            this.countEl.textContent = n + ' selected';
            this.countEl.classList.add('has-selection');
        }
    };

    TypeaheadMultiselect.prototype.getSelected = function () {
        return Array.from(this.selected);
    };

    TypeaheadMultiselect.prototype.setSelected = function (values) {
        this.selected = new Set(Array.isArray(values) ? values : []);
        this._render();
        if (this.onChange) this.onChange(this.getSelected());
    };

    TypeaheadMultiselect.prototype.setItems = function (items) {
        this.items = Array.isArray(items) ? items.slice() : [];
        // Drop selections for values no longer in the pool.
        const pool = new Set(this.items.map(function (i) { return i.value; }));
        const nextSel = new Set();
        this.selected.forEach(function (v) { if (pool.has(v)) nextSel.add(v); });
        this.selected = nextSel;
        this._render();
    };

    TypeaheadMultiselect.prototype.setDisabled = function (values) {
        this.disabled = new Set(Array.isArray(values) ? values : []);
        // Uncheck anything that just became disabled.
        const self = this;
        this.disabled.forEach(function (v) { self.selected.delete(v); });
        this._render();
    };

    TypeaheadMultiselect.prototype.clear = function () {
        this.selected.clear();
        this.query = '';
        if (this.searchEl) this.searchEl.value = '';
        this._render();
        if (this.onChange) this.onChange([]);
    };

    TypeaheadMultiselect.prototype.focus = function () {
        if (this.searchEl) this.searchEl.focus();
    };

    TypeaheadMultiselect.prototype.destroy = function () {
        if (this.root && this.root.__whpTypeaheadMultiselect === this) {
            delete this.root.__whpTypeaheadMultiselect;
        }
        if (this.root) {
            while (this.root.firstChild) this.root.removeChild(this.root.firstChild);
        }
    };

    /**
     * Bootstrap an instance from an existing <select multiple> element.
     * The source select is detached from its form so it does not also submit.
     * Caller is responsible for posting the selected values from
     * tams.getSelected().
     */
    TypeaheadMultiselect.fromSelect = function (rootEl, sourceSelectEl, opts) {
        opts = opts || {};
        const items = [];
        const selected = [];
        const options = sourceSelectEl ? sourceSelectEl.querySelectorAll('option') : [];
        for (let i = 0; i < options.length; i++) {
            const o = options[i];
            const val = o.value;
            if (val === '' || val == null) continue;
            items.push({ value: val, label: (o.textContent || '').trim() || val });
            if (o.selected) selected.push(val);
        }
        if (sourceSelectEl) {
            // Detach from its form so this control owns submission.
            sourceSelectEl.disabled = true;
            sourceSelectEl.removeAttribute('name');
            sourceSelectEl.style.display = 'none';
        }
        const merged = Object.assign({}, opts, {
            items: opts.items || items,
            selected: opts.selected || selected,
        });
        return new TypeaheadMultiselect(rootEl, merged);
    };

    global.TypeaheadMultiselect = TypeaheadMultiselect;
})(typeof window !== 'undefined' ? window : this);
