/* global aieoDmm, jQuery, tinyMCE */
/**
 * AI eShop Optimizer — Discount & Marketplace Management (DMM) admin JS
 *
 *  - Select2 AJAX init for brand / product / user selects.
 *  - TinyMCE editor flush before form submit.
 *  - Generic form-submit handler posting to admin-ajax.php with data-action.
 *  - Batch runner for long imports/exports.
 *  - Custom AJAX DataTable (sortable, searchable, inline editable).
 */
(function ($) {
    'use strict';

    var __ = ( window.wp && window.wp.i18n && window.wp.i18n.__ ) ? window.wp.i18n.__ : function (s) { return s; };
    var _x = ( window.wp && window.wp.i18n && window.wp.i18n._x ) ? window.wp.i18n._x : function (s) { return s; };
    var _n = ( window.wp && window.wp.i18n && window.wp.i18n._n ) ? window.wp.i18n._n : function (s, p, n) { return n === 1 ? s : p; };
    var _sprintf = ( window.wp && window.wp.i18n && window.wp.i18n.sprintf ) ? window.wp.i18n.sprintf : function (s) { return s; };

    if (typeof aieoDmm === 'undefined') { return; }

    // -------------------------------------------------------------------------
    // Flash
    // -------------------------------------------------------------------------
    function flash(message, type) {
        type = type || 'success';
        var $c = $('#aieo-dmm-flash');
        if (!$c.length) { $c = $('<div id="aieo-dmm-flash" class="aieo-dmm-flash"></div>').appendTo('body'); }
        var $i = $('<div class="aieo-dmm-flash-item ' + type + '"></div>').text(message);
        $c.append($i);
        setTimeout(function () { $i.fadeOut(220, function () { $i.remove(); }); }, 3500);
    }

    // -------------------------------------------------------------------------
    // TinyMCE flush — pushes visual-mode content back to the textarea before
    // the form is serialised.
    // -------------------------------------------------------------------------
    function flushTinyMCE($form) {
        if (typeof window.tinyMCE === 'undefined' || !tinyMCE.editors) { return; }
        $form.find('textarea').each(function () {
            var id = this.id;
            if (id && tinyMCE.get(id)) {
                tinyMCE.get(id).save();
            }
        });
    }

    // -------------------------------------------------------------------------
    // Select2 bootstrapping
    // -------------------------------------------------------------------------
    function hasSelect2() { return typeof $.fn.select2 === 'function'; }

    function initSelect2(context) {
        if (!hasSelect2()) { return; }
        var $ctx = context ? $(context) : $(document);

        // Brand search (product_brand taxonomy or configured fallback).
        $ctx.find('.aieo-dmm-brand-select').each(function () {
            var $el = $(this);
            if ($el.data('select2-ready')) return;
            $el.select2({
                width: 'resolve',
                placeholder: $el.attr('multiple') ? aieoDmm.i18n.pickBrands : aieoDmm.i18n.pickBrand,
                ajax: {
                    url: aieoDmm.ajaxurl,
                    dataType: 'json',
                    delay: 250,
                    data: function (params) {
                        return { action: 'aieo_dmm_search_brand_terms', q: params.term || '', nonce: aieoDmm.nonce };
                    },
                    processResults: function (data) {
                        return { results: (data && data.results) ? data.results : [] };
                    },
                },
                minimumInputLength: 0,
                allowClear: true,
            });
            $el.data('select2-ready', true);
        });

        // Product search.
        $ctx.find('.aieo-dmm-product-select, .aieo-dmm-product-search').each(function () {
            var $el = $(this);
            if ($el.data('select2-ready')) return;
            $el.select2({
                width: 'resolve',
                placeholder: aieoDmm.i18n.searchByNameSku,
                ajax: {
                    url: aieoDmm.ajaxurl,
                    dataType: 'json',
                    delay: 250,
                    data: function (params) {
                        return { action: 'aieo_dmm_search_products', q: params.term || '', nonce: aieoDmm.nonce };
                    },
                    processResults: function (data) {
                        return { results: (data && data.results) ? data.results : [] };
                    },
                },
                minimumInputLength: 0,
                allowClear: true,
            });
            $el.data('select2-ready', true);
        });

        // Product Needs editor — sorted by has-needs ASC + turnover DESC
        // so high-impact products without any pa_need term surface first.
        $ctx.find('.aieo-dmm-needs-product-search').each(function () {
            var $el = $(this);
            if ($el.data('select2-ready')) return;
            $el.select2({
                width: 'resolve',
                placeholder: aieoDmm.i18n.searchByNameSku,
                ajax: {
                    url: aieoDmm.ajaxurl,
                    dataType: 'json',
                    delay: 250,
                    data: function (params) {
                        return { action: 'aieo_dmm_needs_search_products', q: params.term || '', nonce: aieoDmm.nonce };
                    },
                    processResults: function (data) {
                        return { results: (data && data.results) ? data.results : [] };
                    },
                },
                minimumInputLength: 0,
                allowClear: true,
            });
            $el.data('select2-ready', true);
        });

        // Need search (pa_need taxonomy).
        $ctx.find('.aieo-dmm-need-select').each(function () {
            var $el = $(this);
            if ($el.data('select2-ready')) return;
            $el.select2({
                width: 'resolve',
                placeholder: $el.attr('multiple') ? aieoDmm.i18n.pickNeeds : aieoDmm.i18n.pickNeed,
                ajax: {
                    url: aieoDmm.ajaxurl,
                    dataType: 'json',
                    delay: 250,
                    data: function (params) {
                        return { action: 'aieo_dmm_search_need_terms', q: params.term || '', nonce: aieoDmm.nonce };
                    },
                    processResults: function (data) {
                        return { results: (data && data.results) ? data.results : [] };
                    },
                },
                minimumInputLength: 0,
                allowClear: true,
            });
            $el.data('select2-ready', true);
        });

        // Category search (product_cat taxonomy).
        $ctx.find('.aieo-dmm-category-select').each(function () {
            var $el = $(this);
            if ($el.data('select2-ready')) return;
            $el.select2({
                width: 'resolve',
                placeholder: $el.attr('multiple') ? aieoDmm.i18n.pickCategories : aieoDmm.i18n.pickCategory,
                ajax: {
                    url: aieoDmm.ajaxurl,
                    dataType: 'json',
                    delay: 250,
                    data: function (params) {
                        return { action: 'aieo_dmm_search_categories', q: params.term || '', nonce: aieoDmm.nonce };
                    },
                    processResults: function (data) {
                        return { results: (data && data.results) ? data.results : [] };
                    },
                },
                minimumInputLength: 0,
                allowClear: true,
            });
            $el.data('select2-ready', true);
        });

        // User search.
        $ctx.find('.aieo-dmm-user-select').each(function () {
            var $el = $(this);
            if ($el.data('select2-ready')) return;
            $el.select2({
                width: 'resolve',
                placeholder: aieoDmm.i18n.searchUsers,
                ajax: {
                    url: aieoDmm.ajaxurl,
                    dataType: 'json',
                    delay: 250,
                    data: function (params) {
                        return { action: 'aieo_dmm_loyalty_search_users', q: params.term || '', nonce: aieoDmm.nonce };
                    },
                    processResults: function (data) {
                        return { results: (data && data.results) ? data.results : [] };
                    },
                },
                minimumInputLength: 0,
                allowClear: true,
            });
            $el.data('select2-ready', true);
        });

        // Country multi-select (static <option>s — no AJAX). Used by the
        // "Disable payments by country" card to pick countries per gateway.
        $ctx.find('.aieo-dmm-country-select').each(function () {
            var $el = $(this);
            if ($el.data('select2-ready')) return;
            $el.select2({
                width: '100%',
                placeholder: $el.attr('data-placeholder') || '',
                allowClear: true,
                closeOnSelect: false,
            });
            $el.data('select2-ready', true);
        });
    }

    $(function () { initSelect2(); });

    // -------------------------------------------------------------------------
    // Generic form submit
    // -------------------------------------------------------------------------
    $(document).on('submit', 'form.aieo-dmm-form', function (e) {
        var $form = $(this);
        var $btn = $form.find('[data-action]:focus, button[type=submit][data-action]').first();
        if (!$btn.length) { $btn = $form.find('button[data-action]').first(); }
        var action = $btn.data('action');
        if (!action) { return; /* browser default */ }
        e.preventDefault();

        flushTinyMCE($form);

        var hasFile = $form.find('input[type=file]').filter(function () { return this.files && this.files.length > 0; }).length > 0;
        var req;
        if (hasFile) {
            var fd = new FormData(this);
            fd.append('action', action);
            fd.append('nonce', aieoDmm.nonce);
            req = $.ajax({ url: aieoDmm.ajaxurl, type: 'POST', data: fd, contentType: false, processData: false });
        } else {
            req = $.post(aieoDmm.ajaxurl, $form.serialize() + '&action=' + encodeURIComponent(action) + '&nonce=' + encodeURIComponent(aieoDmm.nonce));
        }

        var original = $btn.text();
        $btn.prop('disabled', true).text(aieoDmm.i18n.saving);
        req.done(function (resp) {
            if (resp && resp.success) {
                flash((resp.data && resp.data.message) ? resp.data.message : aieoDmm.i18n.saved, 'success');
                $form.trigger('aieo-dmm:saved', [resp.data || {}]);

                // A job_id + total in the response means "start batch loop".
                if (resp.data && resp.data.job_id) {
                    runBatch($form, resp.data.job_id, resp.data.total || 0, action);
                } else if (resp.data && resp.data.url) {
                    window.open(resp.data.url, '_blank');
                }

                // Delete row from table if the response hints at a removal context.
                if ($form.data('then-reload')) { setTimeout(function () { window.location.reload(); }, 400); }
            } else {
                flash((resp && resp.data && resp.data.message) ? resp.data.message : aieoDmm.i18n.error, 'error');
            }
        }).fail(function () { flash(aieoDmm.i18n.error, 'error'); })
          .always(function () { $btn.prop('disabled', false).text(original); });
    });

    // -------------------------------------------------------------------------
    // Batch runner (role pricing, loyalty bulk, orders export, stock import, prices)
    // -------------------------------------------------------------------------
    var BATCH_MAP = {
        'aieo_dmm_role_pricing_upload':      'aieo_dmm_role_pricing_process_batch',
        'aieo_dmm_loyalty_bulk_upload':      'aieo_dmm_loyalty_bulk_process',
        'aieo_dmm_orders_export_start':      'aieo_dmm_orders_export_process',
        'aieo_dmm_stock_import_upload':      'aieo_dmm_stock_import_process',
        'aieo_dmm_price_export_start':       'aieo_dmm_price_export_process',
    };

    function runBatch($form, jobId, total, startAction) {
        var batchAction = BATCH_MAP[startAction];
        if (!batchAction) { return; }

        var $progress = $form.closest('.aieo-dmm-section').find('.aieo-dmm-progress').show();
        if (!$progress.length) {
            $progress = $form.find('.aieo-dmm-progress').show();
        }
        var $bar = $progress.find('.aieo-dmm-progress-bar span');
        var $text = $progress.find('.aieo-dmm-progress-text');
        var $log = $progress.find('.aieo-dmm-progress-log');

        function tick() {
            $.post(aieoDmm.ajaxurl, {
                action: batchAction, nonce: aieoDmm.nonce, job_id: jobId
            }).done(function (resp) {
                if (resp && resp.success) {
                    var d = resp.data || {};
                    var pct = d.percentage != null ? d.percentage : Math.floor((d.processed / Math.max(1, d.total)) * 100);
                    $bar.css('width', pct + '%');
                    $text.text(d.processed + ' / ' + d.total + '  —  ' + pct + '%');
                    if (d.errors && d.errors.length && $log.length) {
                        $log.append(d.errors.join('\n') + '\n');
                    }
                    if (d.complete) {
                        flash(aieoDmm.i18n.done, 'success');
                        if (d.download_url) { window.open(d.download_url, '_blank'); }
                        setTimeout(function () { window.location.reload(); }, 800);
                    } else {
                        setTimeout(tick, 60);
                    }
                } else {
                    flash((resp && resp.data && resp.data.message) ? resp.data.message : aieoDmm.i18n.error, 'error');
                }
            }).fail(function () { flash(aieoDmm.i18n.error, 'error'); });
        }
        tick();
    }

    // -------------------------------------------------------------------------
    // Row-level actions: delete, feed run-now, remove exclusion, unassign
    // -------------------------------------------------------------------------
    $(document).on('click', '.aieo-dmm-delete-row', function () {
        var $row = $(this).closest('tr');
        var id = $row.data('id');
        var action = $(this).data('action');
        if (!id || !action) return;
        if (!window.confirm(aieoDmm.i18n.confirmDelete)) return;
        $.post(aieoDmm.ajaxurl, { action: action, nonce: aieoDmm.nonce, id: id })
         .done(function (resp) {
            if (resp && resp.success) { $row.fadeOut(200, function () { $row.remove(); }); }
            else { flash((resp && resp.data && resp.data.message) ? resp.data.message : aieoDmm.i18n.error, 'error'); }
         });
    });

    $(document).on('click', '[data-action="aieo_dmm_feed_run_now"]', function (e) {
        e.preventDefault();
        var id = $(this).closest('tr').data('id');
        if (!id) return;
        var $btn = $(this);
        $btn.prop('disabled', true).text(aieoDmm.i18n.processing);
        $.post(aieoDmm.ajaxurl, { action: 'aieo_dmm_feed_run_now', nonce: aieoDmm.nonce, id: id })
         .done(function (resp) {
            flash(resp && resp.success
                ? (aieoDmm.i18n.done + ' (' + (resp.data.count || 0) + ')')
                : ((resp && resp.data && resp.data.message) || aieoDmm.i18n.error),
                resp && resp.success ? 'success' : 'error');
            $btn.prop('disabled', false).text( __( 'Run now', 'ai-eshop-optimizer' ) );
         });
    });

    $(document).on('click', '.aieo-dmm-remove-exclusion', function () {
        var $row = $(this).closest('tr');
        var pid = $row.data('product-id');
        if (!pid) return;
        if (!window.confirm(aieoDmm.i18n.confirmDelete)) return;
        $.post(aieoDmm.ajaxurl, { action: 'aieo_dmm_free_shipping_remove_exclusion', nonce: aieoDmm.nonce, product_id: pid })
         .done(function (resp) { if (resp && resp.success) { $row.fadeOut(200, function () { $row.remove(); }); } });
    });

    $(document).on('click', '.aieo-dmm-unassign-loyalty', function () {
        var $row = $(this).closest('tr');
        var userId = $(this).data('user-id');
        var classId = $(this).data('class-id');
        if (!userId || !classId) return;
        $.post(aieoDmm.ajaxurl, { action: 'aieo_dmm_loyalty_unassign_user', nonce: aieoDmm.nonce, user_id: userId, class_id: classId })
         .done(function (resp) { if (resp && resp.success) { $row.fadeOut(200, function () { $row.remove(); }); } });
    });

    $(document).on('click', '.aieo-dmm-download-template', function () {
        $.post(aieoDmm.ajaxurl, { action: 'aieo_dmm_role_pricing_download_template', nonce: aieoDmm.nonce })
         .done(function (resp) { if (resp && resp.success && resp.data.url) { window.open(resp.data.url, '_blank'); } });
    });

    // Stock Import: download sample CSV/XLSX.
    $(document).on('click', '.aieo-dmm-stock-sample', function () {
        var format = $(this).data('format') || 'csv';
        var keyBy = $('input[name=key_by]:checked').val() || 'sku';
        $.post(aieoDmm.ajaxurl, {
            action:  'aieo_dmm_stock_import_download_sample',
            nonce:   aieoDmm.nonce,
            format:  format,
            key_by:  keyBy,
        }).done(function (resp) {
            if (resp && resp.success && resp.data.url) {
                window.open(resp.data.url, '_blank');
                flash(aieoDmm.i18n.done, 'success');
            } else {
                flash((resp && resp.data && resp.data.message) ? resp.data.message : aieoDmm.i18n.error, 'error');
            }
        });
    });

    $(document).on('click', '.aieo-dmm-export-prices', function () {
        $.post(aieoDmm.ajaxurl, { action: 'aieo_dmm_role_pricing_export', nonce: aieoDmm.nonce })
         .done(function (resp) {
            if (resp && resp.success && resp.data.url) {
                window.open(resp.data.url, '_blank');
                flash(aieoDmm.i18n.done, 'success');
            } else {
                flash((resp && resp.data && resp.data.message) ? resp.data.message : aieoDmm.i18n.error, 'error');
            }
         });
    });

    // Role-pricing → GF integration → export configured forms.
    $(document).on('click', '.aieo-dmm-gf-export-forms', function () {
        var $btn = $(this).prop('disabled', true);
        $.post(aieoDmm.ajaxurl, { action: 'aieo_dmm_role_pricing_gf_export', nonce: aieoDmm.nonce })
         .done(function (resp) {
            if (resp && resp.success && resp.data.url) {
                window.open(resp.data.url, '_blank');
                flash(aieoDmm.i18n.done, 'success');
            } else {
                flash((resp && resp.data && resp.data.message) ? resp.data.message : aieoDmm.i18n.error, 'error');
            }
         })
         .always(function () { $btn.prop('disabled', false); });
    });

    // Marketplace Feeds: add field-map row.
    $(document).on('click', '.aieo-dmm-feed-add-row', function () {
        var $tbody = $('#aieo-dmm-feed-fields tbody');
        var $clone = $tbody.find('tr').last().clone();
        $clone.find('input[type=text]').val('');
        $tbody.append($clone);
    });

    // Brand Discounts: after saving, append the returned rows to the table
    // without reloading the page. Works for single-brand or multi-brand submits.
    $(document).on('aieo-dmm:saved', '#aieo-dmm-brand-discount-form', function (e, data) {
        if (!data || !data.inserted || !data.inserted.length) {
            // Nothing new was inserted (all duplicates, or edit mode).
            return;
        }
        var $tbody = $('#aieo-dmm-brand-discount-rows');
        var $table = $('#aieo-dmm-brand-discounts-table');
        var $empty = $('#aieo-dmm-brand-discounts-empty');
        var $count = $('#aieo-dmm-brand-discount-count');

        data.inserted.forEach(function (r) {
            var tr = $('<tr></tr>').attr('data-id', r.id);

            // Label cell.
            var $labelTd = $('<td></td>');
            if (r.label) {
                $labelTd.append($('<strong></strong>').text(r.label));
            } else {
                $labelTd.append($('<em class="description"></em>').text('—'));
            }
            tr.append($labelTd);

            // Brand cell.
            var $brandTd = $('<td></td>');
            if (r.brand_label) {
                $brandTd.append(document.createTextNode(r.brand_label + ' '));
                $brandTd.append($('<code></code>').text('#' + r.brand_term_id));
            } else {
                $brandTd.append($('<em class="description"></em>').text('(any)'));
            }
            tr.append($brandTd);

            // Categories cell.
            var $catTd = $('<td></td>');
            if (r.category_labels && r.category_labels.length) {
                $catTd.text(r.category_labels.join(', '));
            } else {
                $catTd.append($('<em class="description"></em>').text('(any)'));
            }
            tr.append($catTd);

            // Needs cell.
            var $needTd = $('<td></td>');
            if (r.need_labels && r.need_labels.length) {
                $needTd.text(r.need_labels.join(', '));
            } else {
                $needTd.append($('<em class="description"></em>').text('(any)'));
            }
            tr.append($needTd);

            tr.append($('<td></td>').append($('<strong></strong>').text(r.rule_operator || 'AND')));
            tr.append($('<td></td>').text((r.percent != null) ? Number(r.percent).toFixed(2) : ''));
            tr.append($('<td></td>').text(r.date_from || ''));
            tr.append($('<td></td>').text(r.date_to || ''));
            tr.append($('<td></td>').text(r.enabled ? '✔' : '—'));
            tr.append($('<td></td>').text(r.notes || ''));
            tr.append(
                $('<td></td>').append(
                    $('<button type="button" class="button button-small aieo-dmm-delete-row" data-action="aieo_dmm_brand_discount_delete">' + __( 'Delete', 'ai-eshop-optimizer' ) + '</button>')
                )
            );
            tr.css('background', '#fff3cd');
            setTimeout(function () { tr.css({ transition: 'background 1s', background: '' }); }, 20);
            $tbody.prepend(tr);
        });

        if ($table.length) { $table.show(); }
        if ($empty.length) { $empty.hide(); }
        if ($count.length) {
            var n = $tbody.children('tr').length;
            $count.text('(' + n + ')');
        }

        // Reset the form for a fresh entry (keep dates for convenience).
        var $form = $(this);
        var keepFrom = $form.find('[name=date_from]').val();
        var keepTo   = $form.find('[name=date_to]').val();
        this.reset();
        $form.find('[name=date_from]').val(keepFrom);
        $form.find('[name=date_to]').val(keepTo);
        // Clear Select2 multi-selects.
        $form.find('select.aieo-dmm-brand-select, select.aieo-dmm-category-select, select.aieo-dmm-need-select').val(null).trigger('change');
        // Clear shared product pickers (empty the hidden store + chips). The
        // native form.reset() above only deselects dynamically-added <option>s,
        // it doesn't remove them, so the chips would otherwise persist.
        if (window.aieoProdPicker) {
            $form.find('.aieo-prodpick').each(function () { window.aieoProdPicker.hydrate(this, null, []); });
        }
    });

    // Delete an export/import job (and its file on disk).
    $(document).on('click', '.aieo-dmm-delete-export-job', function () {
        var $row = $(this).closest('tr');
        var jobId = $row.data('job-id');
        if (!jobId) return;
        if (!window.confirm(aieoDmm.i18n.confirmDelete)) return;
        var $btn = $(this);
        $btn.prop('disabled', true).text(aieoDmm.i18n.processing);
        $.post(aieoDmm.ajaxurl, {
            action: 'aieo_dmm_delete_export_job',
            nonce:  aieoDmm.nonce,
            job_id: jobId,
        }).done(function (resp) {
            if (resp && resp.success) {
                var msg = aieoDmm.i18n.done;
                if (resp.data && typeof resp.data.files_removed === 'number') {
                    msg = aieoDmm.i18n.done + ' (' + resp.data.files_removed + ' file(s) removed)';
                }
                flash(msg, 'success');
                $row.fadeOut(220, function () { $row.remove(); });
            } else {
                flash((resp && resp.data && resp.data.message) ? resp.data.message : aieoDmm.i18n.error, 'error');
                $btn.prop('disabled', false).text( __( 'Delete', 'ai-eshop-optimizer' ) );
            }
        }).fail(function () {
            flash(aieoDmm.i18n.error, 'error');
            $btn.prop('disabled', false).text( __( 'Delete', 'ai-eshop-optimizer' ) );
        });
    });

    // Copy-to-clipboard for feed URLs/paths.
    $(document).on('click', '.aieo-dmm-copy', function () {
        var value = $(this).data('value') || '';
        if (!value) return;
        var tmp = document.createElement('textarea');
        tmp.value = value;
        document.body.appendChild(tmp);
        tmp.select();
        try { document.execCommand('copy'); flash(aieoDmm.i18n.done, 'success'); }
        catch (e) { flash(aieoDmm.i18n.error, 'error'); }
        document.body.removeChild(tmp);
    });

    // -------------------------------------------------------------------------
    // Product Needs editor
    // -------------------------------------------------------------------------
    var $needsEditor = $('#aieo-dmm-needs-editor');
    if ($needsEditor.length) {
        var selectedOrder = [];
        var sortableInit = false;

        function renderSelected(termIds) {
            var $ul = $('#aieo-dmm-needs-selected .aieo-dmm-chips');
            $ul.empty();
            termIds.forEach(function (tid) {
                var $cb = $('#aieo-dmm-needs-available input[value="' + tid + '"]');
                // Reach up to the parent <li> in the available list so we
                // can detect if this term is internal/hidden — those
                // chips render grey instead of pink so the operator can
                // tell at a glance which are NOT user-facing.
                var $availLi = $cb.closest('li');
                var label = $cb.parent().text().trim();
                var hidden = $availLi.hasClass('is-hidden-need');
                var cls = hidden ? ' class="is-hidden"' : '';
                $ul.append('<li data-term-id="' + tid + '"' + cls + '>' + label + '</li>');
            });
            // Wire drag-to-reorder on first render. The sortable instance
            // survives subsequent empty/append cycles because we reinitialise
            // only when jQuery UI hasn't been attached yet.
            if (!sortableInit && typeof $ul.sortable === 'function') {
                $ul.sortable({
                    items: '> li',
                    cursor: 'move',
                    placeholder: 'aieo-dmm-chip-placeholder',
                    tolerance: 'pointer',
                    forcePlaceholderSize: true,
                    update: function () {
                        selectedOrder = $ul.children('li').map(function () {
                            return parseInt($(this).data('term-id'), 10);
                        }).get().filter(function (v) { return !isNaN(v); });
                    },
                }).disableSelection();
                sortableInit = true;
            } else if (sortableInit && typeof $ul.sortable === 'function') {
                // Refresh sortable so newly-added items are recognised.
                $ul.sortable('refresh');
            }
        }
        function loadProduct(pid) {
            if (!pid) return;
            $.post(aieoDmm.ajaxurl, { action: 'aieo_dmm_needs_load_product', nonce: aieoDmm.nonce, product_id: pid })
             .done(function (resp) {
                if (!resp.success) { flash(resp.data.message || aieoDmm.i18n.error, 'error'); return; }
                var d = resp.data;
                $('#aieo-dmm-needs-meta').show();
                $('#aieo-dmm-needs-title').text(d.title);
                $('#aieo-dmm-needs-sku').text(d.sku || '');
                var selected = (d.order && d.order.length) ? d.order : d.selected_term_ids;
                selectedOrder = selected.slice();
                $('#aieo-dmm-needs-available input[type=checkbox]').prop('checked', false);
                selected.forEach(function (tid) {
                    $('#aieo-dmm-needs-available input[value="' + tid + '"]').prop('checked', true);
                });
                renderSelected(selectedOrder);
             });
        }
        $(document).on('change', '#aieo-dmm-needs-product', function () { loadProduct($(this).val()); });
        $(document).on('change', '#aieo-dmm-needs-available input[type=checkbox]', function () {
            var tid = parseInt($(this).val(), 10);
            if ($(this).is(':checked')) { if (selectedOrder.indexOf(tid) === -1) selectedOrder.push(tid); }
            else { selectedOrder = selectedOrder.filter(function (v) { return v !== tid; }); }
            renderSelected(selectedOrder);
        });
        $(document).on('click', '#aieo-dmm-needs-save', function () {
            var pid = parseInt($('#aieo-dmm-needs-product').val(), 10);
            if (!pid) { flash(aieoDmm.i18n.error, 'error'); return; }
            var payload = { action: 'aieo_dmm_needs_save', nonce: aieoDmm.nonce, product_id: pid };
            selectedOrder.forEach(function (tid, i) {
                payload['term_ids[' + i + ']'] = tid;
                payload['order[' + i + ']'] = tid;
            });
            $.post(aieoDmm.ajaxurl, payload).done(function (resp) {
                flash(resp && resp.success ? aieoDmm.i18n.saved : ((resp && resp.data && resp.data.message) || aieoDmm.i18n.error),
                      resp && resp.success ? 'success' : 'error');
            });
        });

        var preselect = parseInt($needsEditor.data('preselect-pid'), 10);
        if (preselect > 0) { loadProduct(preselect); }
    }

    // -------------------------------------------------------------------------
    // AJAX DataTable — sortable, searchable, paginated, inline editable
    // -------------------------------------------------------------------------
    function AieoDataTable($root, config) {
        this.$root   = $root;
        this.action  = config.action || '';       // fetch action
        this.saveAction = config.saveAction || ''; // cell-save action
        this.state   = {
            page: 1, per_page: parseInt(config.per_page || 25, 10),
            sort: config.sort || 'product_id', dir: config.dir || 'asc',
            search: '', only_nonempty: !!config.only_nonempty,
        };
        this.columns = [];
        this.init();
    }
    AieoDataTable.prototype.init = function () {
        var t = this;
        this.$root.html(
            '<div class="aieo-dmm-datatable-toolbar">' +
                '<input type="search" class="aieo-dmm-dt-search" placeholder="' + __( 'Search SKU or product id…', 'ai-eshop-optimizer' ) + '">' +
                '<label><input type="checkbox" class="aieo-dmm-dt-nonempty"' + (t.state.only_nonempty ? ' checked' : '') + '> ' + __( 'Only rows with prices', 'ai-eshop-optimizer' ) + '</label>' +
                '<div class="spacer"></div>' +
                '<label>' + __( 'Show', 'ai-eshop-optimizer' ) + ' <select class="aieo-dmm-dt-perpage">' +
                    '<option value="10">10</option>' +
                    '<option value="25" selected>25</option>' +
                    '<option value="50">50</option>' +
                    '<option value="100">100</option>' +
                '</select> ' + __( 'per page', 'ai-eshop-optimizer' ) + '</label>' +
                '<button type="button" class="button aieo-dmm-dt-refresh">' + __( 'Refresh', 'ai-eshop-optimizer' ) + '</button>' +
            '</div>' +
            '<div class="aieo-dmm-dt-body"></div>' +
            '<div class="aieo-dmm-datatable-footer">' +
                '<div class="aieo-dmm-dt-summary"></div>' +
                '<div class="aieo-dmm-datatable-pager"></div>' +
            '</div>'
        );
        var debounce;
        this.$root.on('input', '.aieo-dmm-dt-search', function () {
            clearTimeout(debounce);
            var v = this.value;
            debounce = setTimeout(function () {
                t.state.search = v; t.state.page = 1; t.load();
            }, 220);
        });
        this.$root.on('change', '.aieo-dmm-dt-nonempty', function () {
            t.state.only_nonempty = this.checked; t.state.page = 1; t.load();
        });
        this.$root.on('change', '.aieo-dmm-dt-perpage', function () {
            t.state.per_page = parseInt(this.value, 10); t.state.page = 1; t.load();
        });
        this.$root.on('click', '.aieo-dmm-dt-refresh', function () { t.load(); });
        this.$root.on('click', 'thead th.sortable', function () {
            var key = $(this).data('key');
            if (!key) return;
            if (t.state.sort === key) {
                t.state.dir = (t.state.dir === 'asc') ? 'desc' : 'asc';
            } else {
                t.state.sort = key; t.state.dir = 'asc';
            }
            t.load();
        });
        this.$root.on('click', 'td.editable', function () {
            var $td = $(this); if ($td.find('input').length) return;
            var raw = $td.attr('data-value') || '';
            $td.data('prev', raw);
            $td.html('<input type="text" value="' + raw + '" />');
            $td.find('input').trigger('focus').trigger('select');
        });
        this.$root.on('blur keydown', 'td.editable input', function (e) {
            if (e.type === 'keydown') {
                if (e.key === 'Escape') { $(this).val($(this).closest('td').data('prev') || ''); $(this).trigger('blur'); return; }
                if (e.key !== 'Enter' && e.key !== 'Tab') return;
                e.preventDefault();
            }
            var $input = $(this);
            var $td = $input.closest('td');
            var $tr = $td.closest('tr');
            var v = $input.val();
            var prev = $td.data('prev') || '';
            if (String(v) === String(prev)) {
                t.restoreCell($td, v);
                return;
            }
            $.post(aieoDmm.ajaxurl, {
                action: t.saveAction,
                nonce: aieoDmm.nonce,
                product_id: $tr.data('product-id'),
                role: $td.data('role'),
                value: v,
            }).done(function (resp) {
                if (resp && resp.success) {
                    t.restoreCell($td, v);
                    flash(aieoDmm.i18n.saved, 'success');
                } else {
                    flash((resp && resp.data && resp.data.message) ? resp.data.message : aieoDmm.i18n.error, 'error');
                    t.restoreCell($td, prev);
                }
            }).fail(function () {
                flash(aieoDmm.i18n.error, 'error');
                t.restoreCell($td, prev);
            });
        });
        this.load();
    };
    AieoDataTable.prototype.restoreCell = function ($td, val) {
        $td.attr('data-value', val);
        $td.text(val === '' || val == null ? '—' : val);
    };
    AieoDataTable.prototype.load = function () {
        var t = this;
        var $body = this.$root.find('.aieo-dmm-dt-body');
        $body.css('opacity', 0.5);
        $.post(aieoDmm.ajaxurl, {
            action: t.action, nonce: aieoDmm.nonce,
            page: t.state.page, per_page: t.state.per_page,
            sort: t.state.sort, dir: t.state.dir,
            search: t.state.search, only_nonempty: t.state.only_nonempty ? 1 : 0,
        }).done(function (resp) {
            $body.css('opacity', 1);
            if (!resp || !resp.success) {
                $body.html('<div class="aieo-dmm-datatable-empty">' + aieoDmm.i18n.error + '</div>');
                return;
            }
            t.columns = resp.data.columns || [];
            t.render(resp.data);
        }).fail(function () { $body.css('opacity', 1); $body.html('<div class="aieo-dmm-datatable-empty">' + aieoDmm.i18n.error + '</div>'); });
    };
    AieoDataTable.prototype.render = function (data) {
        var t = this;
        var $body = this.$root.find('.aieo-dmm-dt-body');

        if (!data.rows || !data.rows.length) {
            $body.html('<div class="aieo-dmm-datatable-empty">' + __( 'No rows match your filter.', 'ai-eshop-optimizer' ) + '</div>');
            this.renderFooter(data);
            return;
        }
        var html = '<table class="widefat"><thead><tr>';
        this.columns.forEach(function (col) {
            var cls = [];
            if (col.sortable) cls.push('sortable');
            if (t.state.sort === col.key) cls.push(t.state.dir === 'asc' ? 'sorted-asc' : 'sorted-desc');
            html += '<th class="' + cls.join(' ') + '" data-key="' + col.key + '">' + escapeHtml(col.label);
            if (col.sortable) {
                html += ' <span class="sort-indicator">' + (t.state.sort === col.key ? (t.state.dir === 'asc' ? '▲' : '▼') : '⇅') + '</span>';
            }
            html += '</th>';
        });
        html += '</tr></thead><tbody>';
        data.rows.forEach(function (row) {
            html += '<tr data-product-id="' + row.product_id + '">';
            t.columns.forEach(function (col) {
                var v = row[col.key];
                if (col.editable) {
                    var display = (v == null || v === '') ? '—' : (+v).toFixed(2);
                    var raw = (v == null || v === '') ? '' : v;
                    html += '<td class="editable" data-role="' + col.key + '" data-value="' + raw + '">' + display + '</td>';
                } else {
                    html += '<td>' + escapeHtml(v == null ? '' : v) + '</td>';
                }
            });
            html += '</tr>';
        });
        html += '</tbody></table>';
        $body.html(html);
        this.renderFooter(data);
    };
    AieoDataTable.prototype.renderFooter = function (data) {
        var t = this;
        var summary = (data.filtered || 0) === (data.total || 0)
            ? (data.total + ' rows')
            : (data.filtered + ' of ' + data.total + ' rows');
        this.$root.find('.aieo-dmm-dt-summary').text(summary);

        var pages = Math.max(1, Math.ceil((data.filtered || 0) / t.state.per_page));
        var $pager = this.$root.find('.aieo-dmm-datatable-pager').empty();
        if (pages <= 1) return;
        var cur = t.state.page;
        function btn(lbl, p, cls) {
            var $b = $('<button type="button"></button>').text(lbl);
            if (cls) $b.addClass(cls);
            if (p === cur) $b.addClass('is-current').prop('disabled', true);
            if (p < 1 || p > pages) $b.prop('disabled', true);
            $b.on('click', function () { t.state.page = p; t.load(); });
            return $b;
        }
        $pager.append(btn('‹', cur - 1));
        var start = Math.max(1, cur - 3);
        var end   = Math.min(pages, cur + 3);
        if (start > 1) { $pager.append(btn('1', 1)); if (start > 2) $pager.append($('<span>…</span>')); }
        for (var i = start; i <= end; i++) { $pager.append(btn(i, i)); }
        if (end < pages) { if (end < pages - 1) $pager.append($('<span>…</span>')); $pager.append(btn(pages, pages)); }
        $pager.append(btn('›', cur + 1));
    };

    function escapeHtml(s) {
        return String(s == null ? '' : s)
            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
    }

    // Auto-mount any [data-aieo-dmm-datatable] container.
    $(function () {
        $('[data-aieo-dmm-datatable]').each(function () {
            var $el = $(this);
            var cfg = $el.data();
            new AieoDataTable($el, {
                action:     cfg.action,
                saveAction: cfg.saveAction,
                per_page:   cfg.perPage || 25,
                sort:       cfg.sort || 'product_id',
                dir:        cfg.dir || 'asc',
                only_nonempty: !!cfg.onlyNonempty,
            });
        });
    });

    // Expose for future subtabs.
    window.AieoDmm = { DataTable: AieoDataTable, initSelect2: initSelect2, flash: flash };

})(jQuery);
