import TomSelect from 'tom-select';
// Built-in plugin: moves the search input from the trigger field into
// the dropdown panel header. Without this, users have to discover that
// the trigger itself doubles as a text input — not obvious.
import 'tom-select/dist/js/plugins/dropdown_input.js';
import { posGet } from '../lib/http.js';

/**
 * Alpine factory that upgrades a native <select> with TomSelect.
 *
 * Drop it on any styled <select class="pos-input"> to add:
 *   - search-as-you-type (no library setup needed)
 *   - capped dropdown — only N options render at once; the rest
 *     appear once the user filters them down (good for lists with
 *     hundreds of rows)
 *   - keyboard nav, ARIA, indented option text preserved
 *
 *   <select x-data="enhancedSelect()" name="parent_id" x-model="form.parent_id">
 *     <option value="">…</option>
 *     <option value="5">Snacks</option>
 *   </select>
 *
 * Default `maxOptions` is 20 — change via opts:
 *   x-data="enhancedSelect({ maxOptions: 50 })"
 *
 * The closed-state UI is themed in resources/css/components/select.css
 * to look pixel-identical to a native `select.pos-input`, so visually
 * nothing changes; only the open dropdown gets the search input.
 */
export function enhancedSelect(overrides = {}) {
    // `value` is OUR option (not a TomSelect config key): the initial value
    // to display. Needed because an x-model bound to JS state (e.g. an
    // address row's country, a customer group on the edit screen) is set on
    // the native <select> but TomSelect won't reflect it on its own — so we
    // push it in explicitly after init. Server-rendered <option selected>
    // works without this; this covers the JS-driven cases.
    const { value: initialValue, ...tsOverrides } = overrides;

    return {
        ts: null,

        init() {
            if (this.$el.tomselect) return;                  // idempotent

            // Multi-select picks up the `remove_button` plugin so each
            // chip ships with an × control. Single-select skips it (a
            // chip-style × on the single value would be visual noise).
            const isMulti = this.$el.multiple || this.$el.hasAttribute('multiple');
            const plugins = isMulti
                ? ['dropdown_input', 'remove_button']
                : ['dropdown_input'];

            const config = {
                allowEmptyOption: true,
                maxOptions:       20,                        // cap the visible list
                searchField:      ['text'],                  // search the option label
                sortField:        { field: '$order' },       // preserve server-rendered order
                plugins,
                placeholder:      'Search…',                 // shown in the dropdown's search input
                ...tsOverrides,
            };

            config.render = { ...(config.render || {}) };

            // Auto-detect tree selects: any option with data-depth gets
            // a custom renderer that applies CSS-based indentation instead
            // of the unreliable &nbsp; approach (TomSelect trims leading
            // whitespace from option text during initialization).
            const hasTree = Array.from(this.$el.options).some((o) => o.dataset.depth !== undefined);
            if (hasTree && !config.render.option) {
                config.render.option = (data, escape) => {
                    const depth  = parseInt(data.depth ?? 0, 10);
                    const padPx  = depth > 0 ? depth * 20 + 10 : 10;
                    const arrow  = depth > 0 ? '<span class="ts-tree-arrow">↳</span> ' : '';
                    return `<div class="option" style="padding-inline-start:${padPx}px">${arrow}${escape(data.text)}</div>`;
                };
            }

            // Show the full label on hover. The closed control truncates in
            // narrow columns (e.g. the purchase items' Tax column), so a
            // title attribute surfaces the complete text.
            if (!config.render.item) {
                config.render.item = (data, escape) => {
                    const text = data.text ?? '';
                    return `<div class="item" title="${escape(text)}">${escape(text)}</div>`;
                };
            }

            this.ts = new TomSelect(this.$el, config);

            // Reflect a JS-driven initial value (x-model bound to state) that
            // TomSelect can't read off the native <select> on its own. Silent
            // (second arg) so it doesn't fire a change and clobber x-model.
            if (initialValue !== undefined && initialValue !== null && initialValue !== '') {
                this.ts.setValue(String(initialValue), true);
            }

            // Flip the dropdown UP when there isn't room below. A select near
            // the page bottom (e.g. the last field on a form) otherwise opens
            // downward past the viewport, and the browser grows the scroll
            // area to fit the absolutely-positioned panel — leaving a blank
            // gap below the footer. `.ts-dropdown-up` repositions it above.
            const ts = this.ts;
            const decideDirection = () => {
                const rect = ts.control.getBoundingClientRect();
                const ddH  = ts.dropdown?.offsetHeight || 260;
                const below = window.innerHeight - rect.bottom;
                const above = rect.top;
                ts.wrapper.classList.toggle('ts-dropdown-up', below < ddH && above > below);
            };
            ts.on('dropdown_open', () => requestAnimationFrame(decideDirection));
            ts.on('dropdown_close', () => ts.wrapper.classList.remove('ts-dropdown-up'));

            // Mirror the selected label onto the control as a title so the
            // full text shows on hover when it's ellipsis-truncated in a
            // narrow field (e.g. the purchase items' Tax column).
            const syncTitle = () => {
                const item = ts.control.querySelector('.item');
                ts.control.title = item ? item.textContent.trim() : '';
            };
            ts.on('change', syncTitle);
            syncTitle();
        },

        destroy() {
            this.ts?.destroy();
            this.ts = null;
        },
    };
}

/**
 * Remote (server-backed) searchable select — the system-wide pattern for
 * picking one row from a large table (products, customers, suppliers, …)
 * WITHOUT preloading the whole list into the page.
 *
 *   - Loads the first page (≈25 rows) only when the field is focused.
 *   - Searches server-side as the user types, debounced (`loadThrottle`).
 *   - Seeds the currently-selected row's label so edit screens render the
 *     chosen value instantly, no fetch required.
 *
 * The endpoint must return a JSON array of `{ value, label, ... }`
 * objects (extra fields are preserved and passed to `onResults`).
 *
 *   <select x-data="remoteSelect({
 *               url: '{{ route('admin.products.search') }}',
 *               value: k.component_product_id,   // current value (edit)
 *               label: k.component_label,        // current label (edit)
 *               exclude: 5,                       // optional id to omit
 *               onResults: (rows) => cacheSomething(rows),
 *           })"
 *           x-model="k.component_product_id"
 *           :name="`kit_items[${i}][component_product_id]`"
 *           class="pos-input"></select>
 *
 * Closed-state styling is identical to `enhancedSelect` (see select.css),
 * so remote and local selects look the same across the system.
 */
export function remoteSelect(opts = {}) {
    return {
        ts: null,

        init() {
            if (this.$el.tomselect) return;

            const config = {
                valueField:   'value',
                labelField:   'label',
                searchField:  ['label'],                 // server filters; keep loaded rows matchable
                loadThrottle: opts.throttle ?? 300,       // debounce: one request per typing pause
                maxOptions:   opts.maxOptions ?? 25,
                preload:      'focus',                    // fetch the first page on focus, not page load
                plugins:      ['dropdown_input'],
                placeholder:  opts.placeholder ?? 'Search…',
                allowEmptyOption: true,
                // `dropdownParent: 'body'` portals the dropdown panel out
                // of its host so it escapes modal/overflow:hidden ancestors.
                // Used by the delete-with-move modal. Default unchanged for
                // inline form selects.
                ...(opts.dropdownParent ? { dropdownParent: opts.dropdownParent } : {}),
                load: (query, callback) => {
                    const params = {};
                    if (query) params.q = query;
                    if (opts.exclude) params.exclude = opts.exclude;

                    posGet(opts.url, Object.keys(params).length ? params : null)
                        .then(({ data }) => {
                            const list = Array.isArray(data) ? data : [];
                            if (typeof opts.onResults === 'function') opts.onResults(list);
                            callback(list);
                        })
                        .catch(() => callback());
                },
            };

            // Seed the current selection so an edit screen shows its label
            // without waiting for a fetch.
            if (opts.value !== undefined && opts.value !== null && opts.value !== '' && opts.label) {
                config.options = [{ value: String(opts.value), label: String(opts.label) }];
                config.items   = [String(opts.value)];
            }

            this.ts = new TomSelect(this.$el, config);
        },

        destroy() {
            this.ts?.destroy();
            this.ts = null;
        },
    };
}
