// public/assets/table.js
// Shared table controller for pagination/infinite mode

class TableController {
  constructor(opts) {
    this.table = opts.table;
    this.tbody = this.table.querySelector('tbody');
    this.wrap = this.table.closest('.table-wrap');
    this.footer = this.wrap?.querySelector('[data-table-footer]');
    this.metaEl = this.wrap?.querySelector('[data-table-meta]');
    this.pageEl = this.wrap?.querySelector('[data-table-page]');
    this.prevBtn = this.wrap?.querySelector('[data-table-prev]');
    this.nextBtn = this.wrap?.querySelector('[data-table-next]');
    this.renderRow = opts.renderRow;
    this.fetchPage = opts.fetchPage;
    this.mode = opts.mode || 'pagination';
    this.pageSize = Number(opts.pageSize || 50);
    this.page = 1;
    this.total = 0;
    this.sort = opts.sort || '';
    this.dir = opts.dir || 'asc';
    this.onSortChange = opts.onSortChange || null;
    this.params = opts.params || {};
    this.filters = Array.isArray(opts.filters) ? opts.filters : [];
    this.loading = false;

    this.attachSortHandlers();
    this.attachPagerHandlers();
    this.initFilters();
    this.initViewEditor();
    this.defaultColumns = this.buildColumnsFromHeaders([]);
    const tableId = this.getTableId();
    if (tableId && window.UserPrefs?.getTable) {
      const prefs = window.UserPrefs.getTable(tableId);
      if (prefs && prefs.columns) {
        this.applyColumnPrefs(prefs);
      }
    }
  }

  setMode(mode) {
    this.mode = mode === 'infinite' ? 'infinite' : 'pagination';
  }

  setPageSize(size) {
    this.pageSize = Math.max(1, Number(size || 50));
  }

  setParams(params) {
    this.params = params || {};
  }

  setFilters(filters) {
    this.filters = Array.isArray(filters) ? filters : [];
    this.syncFilterInputs();
  }

  setSort(sort, dir) {
    this.sort = sort || '';
    this.dir = dir === 'desc' ? 'desc' : 'asc';
  }

  attachSortHandlers() {
    const headers = this.table.querySelectorAll('thead th[data-sort]');
    headers.forEach(th => {
      th.classList.add('sortable');
      th.addEventListener('click', () => {
        const key = th.dataset.sort || '';
        if (!key) return;
        if (this.sort === key) {
          this.dir = (this.dir === 'asc') ? 'desc' : 'asc';
        } else {
          this.sort = key;
          this.dir = 'asc';
        }
        if (typeof this.onSortChange === 'function') {
          this.onSortChange(this.sort, this.dir);
        }
        this.load(1, false);
      });
    });
  }

  attachPagerHandlers() {
    if (this.prevBtn) {
      this.prevBtn.addEventListener('click', () => {
        if (this.page > 1) this.load(this.page - 1, false);
      });
    }
    if (this.nextBtn) {
      this.nextBtn.addEventListener('click', () => {
        const totalPages = this.totalPages();
        if (this.page < totalPages) this.load(this.page + 1, false);
      });
    }

    const handleScroll = () => {
      if (this.mode !== 'infinite') return;
      if (this.loading) return;
      if (this.page >= this.totalPages()) return;
      if (this.isWrapScrollable()) {
        const nearBottom = this.wrap.scrollTop + this.wrap.clientHeight >= this.wrap.scrollHeight - 80;
        if (nearBottom) this.load(this.page + 1, true);
      } else {
        const doc = document.documentElement;
        const nearBottom = window.scrollY + window.innerHeight >= doc.scrollHeight - 120;
        if (nearBottom) this.load(this.page + 1, true);
      }
    };

    if (this.wrap) {
      this.wrap.addEventListener('scroll', handleScroll);
    }
    window.addEventListener('scroll', handleScroll);
  }

  initFilters() {
    const headRow = this.table?.querySelector('thead tr');
    if (!headRow) return;
    const filtersEnabled = this.table?.dataset?.filtersEnabled;
    if (filtersEnabled === '0') {
      const tools = this.wrap?.querySelector('[data-table-tools]');
      if (tools) tools.classList.add('is-hidden');
      return;
    }
    const ths = Array.from(headRow.querySelectorAll('th'));
    const hasFilters = ths.some(th => th.dataset.filter);
    const tools = this.wrap?.querySelector('[data-table-tools]');
    const toggleBtn = this.wrap?.querySelector('[data-table-filters-toggle]');
    const summaryEl = this.wrap?.querySelector('[data-table-filters-summary]');

    if (!hasFilters) {
      if (tools) tools.classList.add('is-hidden');
      return;
    }
    if (tools) tools.classList.remove('is-hidden');

    const ensureIndicator = (th) => {
      if (!th.classList.contains('has-filter')) {
        th.classList.add('has-filter');
      }
      if (!th.querySelector('.filter-indicator')) {
        const dot = document.createElement('span');
        dot.className = 'filter-indicator';
        dot.setAttribute('aria-hidden', 'true');
        th.appendChild(dot);
      }
    };

    ths.forEach((th) => {
      if (th.dataset.filter) {
        ensureIndicator(th);
      }
    });

    let row = this.table.querySelector('thead tr[data-filter-row]');
    if (!row) {
      row = document.createElement('tr');
      row.className = 'table-filter-row';
      row.dataset.filterRow = '1';
      headRow.insertAdjacentElement('afterend', row);
      ths.forEach((th) => {
        const td = document.createElement('td');
        const key = th.dataset.key || '';
        if (key) td.dataset.key = key;
        row.appendChild(td);
      });
    }
    this.filterRow = row;
    this.filterThs = ths;
    this.filterSummaryEl = summaryEl;

    const parseFilter = (value) => {
      if (!value) return null;
      try {
        const obj = JSON.parse(value);
        if (obj && typeof obj === 'object') return obj;
      } catch (e) {
        return null;
      }
      return null;
    };

    const defaultOp = (meta, type) => {
      if (Array.isArray(meta?.ops) && meta.ops.length > 0) return meta.ops[0];
      if (type === 'text') return 'like';
      return 'eq';
    };

    const buildInput = (meta, key) => {
      const type = meta?.type || 'text';
      const control = meta?.control || '';
      const isSelect = type === 'select' || control === 'select' || Array.isArray(meta?.options);
      const ops = Array.isArray(meta?.ops) ? meta.ops : [];
      const op = defaultOp(meta, type);
      if (isSelect) {
        const dict = window.I18N || {};
        const t = (i18nKey, fallback) => {
          if (i18nKey && Object.prototype.hasOwnProperty.call(dict, i18nKey)) {
            const v = dict[i18nKey];
            if (typeof v === 'string' && v.length) return v;
          }
          return fallback;
        };
        const select = document.createElement('select');
        select.dataset.filterKey = key;
        select.dataset.filterOp = op;
        const optAny = document.createElement('option');
        optAny.value = '';
        optAny.textContent = t('filters.all', 'All');
        select.appendChild(optAny);
        const options = Array.isArray(meta?.options) ? meta.options : [];
        options.forEach((value) => {
          const opt = document.createElement('option');
          if (value && typeof value === 'object') {
            opt.value = String(value.value ?? '');
            opt.textContent = t(value.i18n || '', String(value.label ?? value.value ?? ''));
          } else {
            opt.value = String(value);
            opt.textContent = String(value);
          }
          select.appendChild(opt);
        });
        return select;
      }
      if (type === 'boolean') {
        const select = document.createElement('select');
        select.dataset.filterKey = key;
        select.dataset.filterOp = op;
        const optAny = document.createElement('option');
        optAny.value = '';
        optAny.textContent = 'All';
        select.appendChild(optAny);
        const optTrue = document.createElement('option');
        optTrue.value = 'true';
        optTrue.textContent = 'Yes';
        select.appendChild(optTrue);
        const optFalse = document.createElement('option');
        optFalse.value = 'false';
        optFalse.textContent = 'No';
        select.appendChild(optFalse);
        return select;
      }
      if ((type === 'number' || type === 'date') && ops.includes('between')) {
        const wrap = document.createElement('div');
        wrap.className = 'table-filter-range';
        const inputFrom = document.createElement('input');
        const inputTo = document.createElement('input');
        inputFrom.type = type === 'date' ? 'date' : 'number';
        inputTo.type = type === 'date' ? 'date' : 'number';
        inputFrom.dataset.filterKey = key;
        inputFrom.dataset.filterOp = 'between';
        inputFrom.dataset.filterRange = 'from';
        inputTo.dataset.filterKey = key;
        inputTo.dataset.filterOp = 'between';
        inputTo.dataset.filterRange = 'to';
        wrap.appendChild(inputFrom);
        wrap.appendChild(inputTo);
        return wrap;
      }
      const input = document.createElement('input');
      input.type = type === 'number' ? 'number' : (type === 'date' ? 'date' : 'text');
      input.dataset.filterKey = key;
      input.dataset.filterOp = op;
      return input;
    };

    const cells = Array.from(row.children);
    ths.forEach((th, idx) => {
      const meta = parseFilter(th.dataset.filter || '');
      const key = th.dataset.key || '';
      const cell = cells[idx];
      if (!cell) return;
      cell.innerHTML = '';
      if (!meta || key === '') return;
      const control = buildInput(meta, key);
      cell.appendChild(control);
    });

    let debounceId = null;
    const readFilters = () => {
      const filters = [];
      const inputs = row.querySelectorAll('[data-filter-key]');
      const rangeMap = new Map();
      inputs.forEach((el) => {
        const key = el.dataset.filterKey || '';
        const op = el.dataset.filterOp || 'eq';
        if (!key) return;
        if (el.dataset.filterRange) {
          const entry = rangeMap.get(key) || { from: '', to: '', op };
          if (el.dataset.filterRange === 'from') entry.from = el.value || '';
          if (el.dataset.filterRange === 'to') entry.to = el.value || '';
          rangeMap.set(key, entry);
          return;
        }
        const value = el.value;
        if (value === '' || value === null || value === undefined) return;
        filters.push({ key, op, value });
      });
      rangeMap.forEach((entry, key) => {
        if (entry.from && entry.to) {
          filters.push({ key, op: 'between', value: [entry.from, entry.to] });
        }
      });
      return filters;
    };

    const updateIndicators = (filters) => {
      const active = new Set(filters.map(f => f.key));
      ths.forEach((th) => {
        if (!th.dataset.filter) return;
        const key = th.dataset.key || '';
        th.classList.toggle('filter-active', key !== '' && active.has(key));
      });
      if (summaryEl) {
        summaryEl.textContent = filters.length ? `Filters: ${filters.length}` : '';
      }
    };

    const applyFilters = () => {
      const filters = readFilters();
      this.setFilters(filters);
      updateIndicators(filters);
      this.load(1, false);
    };

    const handleInput = (e) => {
      const target = e.target;
      if (!target || !target.dataset.filterKey) return;
      if (target.tagName === 'INPUT' && target.type === 'text') {
        if (debounceId) clearTimeout(debounceId);
        debounceId = setTimeout(applyFilters, 350);
        return;
      }
      applyFilters();
    };

    row.addEventListener('input', handleInput);
    row.addEventListener('change', handleInput);

    if (toggleBtn && !toggleBtn.dataset.filtersInit) {
      toggleBtn.dataset.filtersInit = '1';
      toggleBtn.addEventListener('click', () => {
        if (!this.wrap) return;
        this.wrap.classList.toggle('filters-open');
      });
    }
    updateIndicators(this.filters);
  }

  initViewEditor() {
    const dialog = document.getElementById('dlg-table-view');
    const tools = this.wrap?.querySelector('[data-table-tools]');
    const viewBtn = this.wrap?.querySelector('[data-table-view-toggle]');
    if (!dialog || !viewBtn) return;
    const hasConfig = this.table?.dataset?.viewConfig === '1';
    if (!hasConfig) {
      if (viewBtn) viewBtn.classList.add('is-hidden');
      if (tools && !this.table?.querySelector('thead th[data-filter]')) {
        tools.classList.add('is-hidden');
      }
      return;
    }
    viewBtn.classList.remove('is-hidden');
    if (viewBtn.dataset.viewInit === '1') return;
    viewBtn.dataset.viewInit = '1';

    viewBtn.addEventListener('click', () => {
      this.openViewEditor(dialog);
    });
  }

  openViewEditor(dialog) {
    if (!dialog) return;
    const body = dialog.querySelector('[data-table-view-body]');
    if (!body) return;
    const tableId = this.getTableId();
    const prefs = window.UserPrefs?.getTable?.(tableId) || {};
    const columns = this.buildColumnsFromHeaders(prefs.columns || []);

    body.innerHTML = '';
    columns.forEach((col, idx) => {
      const tr = document.createElement('tr');
      tr.className = 'table-view-row';
      tr.dataset.key = col.key;

      const tdOn = document.createElement('td');
      const chk = document.createElement('input');
      chk.type = 'checkbox';
      chk.checked = col.visible !== false;
      tdOn.appendChild(chk);

      const tdLabel = document.createElement('td');
      tdLabel.textContent = col.label;

      const tdWidth = document.createElement('td');
      const sel = document.createElement('select');
      const optAuto = document.createElement('option');
      optAuto.value = 'auto';
      optAuto.textContent = 'Auto';
      const optManual = document.createElement('option');
      optManual.value = 'manual';
      optManual.textContent = 'Manual';
      sel.appendChild(optAuto);
      sel.appendChild(optManual);
      const widthInput = document.createElement('input');
      widthInput.type = 'number';
      widthInput.min = '40';
      widthInput.max = '800';
      widthInput.value = col.width ? String(col.width) : '';
      const widthMode = col.width ? 'manual' : 'auto';
      sel.value = widthMode;
      widthInput.disabled = widthMode !== 'manual';
      sel.addEventListener('change', () => {
        widthInput.disabled = sel.value !== 'manual';
      });
      tdWidth.appendChild(sel);
      tdWidth.appendChild(widthInput);

      const tdHeadAlign = document.createElement('td');
      const headAlign = document.createElement('select');
      ['left', 'center', 'right'].forEach((val) => {
        const opt = document.createElement('option');
        opt.value = val;
        opt.textContent = val;
        headAlign.appendChild(opt);
      });
      headAlign.value = col.headAlign || 'left';
      tdHeadAlign.appendChild(headAlign);

      const tdCellAlign = document.createElement('td');
      const cellAlign = document.createElement('select');
      ['left', 'center', 'right'].forEach((val) => {
        const opt = document.createElement('option');
        opt.value = val;
        opt.textContent = val;
        cellAlign.appendChild(opt);
      });
      cellAlign.value = col.cellAlign || 'left';
      tdCellAlign.appendChild(cellAlign);

      const tdOrder = document.createElement('td');
      const upBtn = document.createElement('button');
      upBtn.type = 'button';
      upBtn.className = 'btn';
      upBtn.textContent = '↑';
      const downBtn = document.createElement('button');
      downBtn.type = 'button';
      downBtn.className = 'btn';
      downBtn.textContent = '↓';
      upBtn.addEventListener('click', () => {
        const prev = tr.previousElementSibling;
        if (prev) prev.before(tr);
      });
      downBtn.addEventListener('click', () => {
        const next = tr.nextElementSibling;
        if (next) next.after(tr);
      });
      tdOrder.appendChild(upBtn);
      tdOrder.appendChild(downBtn);

      tr.appendChild(tdOn);
      tr.appendChild(tdLabel);
      tr.appendChild(tdWidth);
      tr.appendChild(tdHeadAlign);
      tr.appendChild(tdCellAlign);
      tr.appendChild(tdOrder);
      body.appendChild(tr);
    });

    const applyBtn = dialog.querySelector('[data-table-view-apply]');
    const resetBtn = dialog.querySelector('[data-table-view-reset]');
    if (applyBtn && applyBtn.dataset.bound !== '1') {
      applyBtn.dataset.bound = '1';
      applyBtn.addEventListener('click', () => {
        const rows = Array.from(body.querySelectorAll('tr.table-view-row'));
        const cols = rows.map((row) => {
          const key = row.dataset.key || '';
          const inputs = row.querySelectorAll('input, select');
          const chk = inputs[0];
          const mode = inputs[1];
          const widthInput = inputs[2];
          const headAlign = inputs[3];
          const cellAlign = inputs[4];
          const width = mode && mode.value === 'manual' ? Number(widthInput.value || 0) : 0;
          return {
            key,
            visible: chk?.checked !== false,
            width: width > 0 ? width : 0,
            headAlign: headAlign?.value || 'left',
            cellAlign: cellAlign?.value || 'left',
          };
        }).filter(c => c.key !== '');
        window.UserPrefs?.setTable?.(tableId, { columns: cols });
        this.applyColumnPrefs({ columns: cols });
        dialog.close();
      });
    }
    if (resetBtn && resetBtn.dataset.bound !== '1') {
      resetBtn.dataset.bound = '1';
      resetBtn.addEventListener('click', () => {
        window.UserPrefs?.setTable?.(tableId, { columns: [] });
        this.applyColumnPrefs({ columns: [] });
        dialog.close();
      });
    }
    dialog.showModal();
  }

  buildColumnsFromHeaders(saved) {
    const headRow = this.table?.querySelector('thead tr');
    if (!headRow) return [];
    const ths = Array.from(headRow.querySelectorAll('th'));
    const base = ths.map((th) => ({
      key: th.dataset.key || '',
      label: th.textContent?.trim() || '',
      visible: true,
      width: 0,
      headAlign: th.classList.contains('align-right') ? 'right' : (th.classList.contains('align-center') ? 'center' : 'left'),
      cellAlign: th.classList.contains('align-right') ? 'right' : (th.classList.contains('align-center') ? 'center' : 'left'),
    })).filter(c => c.key !== '');

    if (!Array.isArray(saved) || saved.length === 0) return base;
    const map = new Map(saved.map(c => [c.key, c]));
    const ordered = [];
    saved.forEach((c) => {
      const baseCol = base.find(b => b.key === c.key);
      if (!baseCol) return;
      ordered.push({
        ...baseCol,
        visible: c.visible !== false,
        width: Number(c.width || 0),
        headAlign: c.headAlign || baseCol.headAlign || 'left',
        cellAlign: c.cellAlign || baseCol.cellAlign || 'left',
      });
    });
    base.forEach((c) => {
      if (!map.has(c.key)) ordered.push(c);
    });
    return ordered;
  }

  getTableId() {
    return this.table?.dataset?.tableId || this.table?.id || '';
  }

  applyColumnPrefs(prefs) {
    let columns = Array.isArray(prefs?.columns) ? prefs.columns : [];
    if (columns.length === 0 && Array.isArray(this.defaultColumns)) {
      columns = this.defaultColumns.map(c => ({ ...c }));
    }
    if (columns.length === 0) return;
    this.columnPrefs = columns;
    const headRow = this.table?.querySelector('thead tr');
    if (!headRow) return;
    const ths = Array.from(headRow.querySelectorAll('th'));
    const keyToIndex = new Map();
    ths.forEach((th, idx) => {
      const key = th.dataset.key || '';
      if (key) keyToIndex.set(key, idx);
    });
    const order = columns.map(c => c.key).filter(k => keyToIndex.has(k));
    const used = new Set(order);
    ths.forEach((th) => {
      const key = th.dataset.key || '';
      if (key && !used.has(key)) order.push(key);
    });
    this.columnOrder = order;
    this.reorderColumns(order, ths.length, keyToIndex);
    this.applyColumnStyles();
  }

  reorderColumns(order, count, keyToIndex) {
    if (!this.table) return;
    const rows = Array.from(this.table.querySelectorAll('tr'));
    rows.forEach((row) => {
      const cells = Array.from(row.children);
      if (cells.length !== count) return;
      const cellByKey = new Map();
      let hasKeys = false;
      cells.forEach((cell) => {
        const key = cell.dataset?.key || '';
        if (key) {
          cellByKey.set(key, cell);
          hasKeys = true;
        }
      });
      if (hasKeys) {
        const newCells = [];
        const used = new Set();
        order.forEach((key) => {
          const cell = cellByKey.get(key);
          if (cell) {
            newCells.push(cell);
            used.add(cell);
          }
        });
        cells.forEach((cell) => {
          if (!used.has(cell)) newCells.push(cell);
        });
        if (newCells.length !== cells.length) return;
        newCells.forEach((cell) => row.appendChild(cell));
        return;
      }
      const newCells = [];
      order.forEach((key) => {
        const idx = keyToIndex.get(key);
        if (idx >= 0 && cells[idx]) newCells.push(cells[idx]);
      });
      if (newCells.length !== count) return;
      newCells.forEach((cell) => row.appendChild(cell));
    });
  }

  applyColumnVisibility(columns, order) {
    if (!this.table) return;
    const visibility = new Map(columns.map(c => [c.key, c.visible !== false]));
    const rows = Array.from(this.table.querySelectorAll('tr'));
    rows.forEach((row) => {
      const cells = Array.from(row.children);
      if (cells.length !== order.length) return;
      order.forEach((key, idx) => {
        const visible = visibility.get(key);
        if (visible === undefined) return;
        cells[idx].classList.toggle('is-hidden', !visible);
      });
    });
  }

  applyFilterVisibility(columns, order) {
    const row = this.filterRow;
    if (!row) return;
    const visibility = new Map(columns.map(c => [c.key, c.visible !== false]));
    const visibleKeys = new Set(columns.filter(c => c.visible !== false).map(c => c.key));
    const cells = Array.from(row.children);
    cells.forEach((cell, idx) => {
      const key = cell.dataset?.key || order[idx] || '';
      if (!key) return;
      const visible = visibility.get(key);
      if (visible === undefined) return;
      cell.classList.toggle('is-hidden', !visible);
      const inputs = cell.querySelectorAll('input, select, textarea, button');
      inputs.forEach((el) => {
        el.disabled = !visible;
        if (!visible && 'value' in el) el.value = '';
      });
    });

    const before = Array.isArray(this.filters) ? this.filters : [];
    const next = before.filter(f => visibleKeys.has(f.key));
    if (next.length !== before.length) {
      this.filters = next;
      this.syncFilterInputs();
    }
  }

  applyColumnWidths(columns, order) {
    if (!this.table) return;
    const widths = new Map(columns.map(c => [c.key, Number(c.width || 0)]));
    const rows = Array.from(this.table.querySelectorAll('tr'));
    rows.forEach((row) => {
      const cells = Array.from(row.children);
      if (cells.length !== order.length) return;
      order.forEach((key, idx) => {
        const width = widths.get(key) || 0;
        if (width > 0) {
          cells[idx].style.width = `${width}px`;
        } else {
          cells[idx].style.width = '';
        }
      });
    });
    this.table.style.tableLayout = 'fixed';
  }

  applyColumnStyles() {
    const columns = Array.isArray(this.columnPrefs) ? this.columnPrefs : [];
    const order = Array.isArray(this.columnOrder) ? this.columnOrder : [];
    if (columns.length === 0 || order.length === 0) return;
    const headRow = this.table?.querySelector('thead tr');
    if (headRow) {
      const ths = Array.from(headRow.querySelectorAll('th'));
      const keyToIndex = new Map();
      ths.forEach((th, idx) => {
        const key = th.dataset.key || '';
        if (key) keyToIndex.set(key, idx);
      });
      this.reorderColumns(order, ths.length, keyToIndex);
    }
    this.applyColumnVisibility(columns, order);
    this.applyFilterVisibility(columns, order);
    this.applyColumnWidths(columns, order);
    this.applyColumnAlign(columns, order);
  }

  applyColumnAlign(columns, order) {
    if (!this.table) return;
    const headAlign = new Map(columns.map(c => [c.key, c.headAlign || 'left']));
    const cellAlign = new Map(columns.map(c => [c.key, c.cellAlign || 'left']));
    const rows = Array.from(this.table.querySelectorAll('tr'));
    rows.forEach((row) => {
      const isHead = row.parentElement?.tagName === 'THEAD';
      const cells = Array.from(row.children);
      if (cells.length !== order.length) return;
      order.forEach((key, idx) => {
        const align = (isHead ? headAlign.get(key) : cellAlign.get(key)) || 'left';
        const cell = cells[idx];
        if (!cell) return;
        cell.classList.remove('align-left', 'align-center', 'align-right');
        cell.classList.add(`align-${align}`);
      });
    });
  }

  syncFilterInputs() {
    const row = this.filterRow;
    if (!row) return;
    const inputs = row.querySelectorAll('[data-filter-key]');
    const rangeMap = new Map();
    inputs.forEach((el) => {
      if (el.dataset.filterRange) {
        const key = el.dataset.filterKey || '';
        if (!key) return;
        const entry = rangeMap.get(key) || { fromEl: null, toEl: null };
        if (el.dataset.filterRange === 'from') entry.fromEl = el;
        if (el.dataset.filterRange === 'to') entry.toEl = el;
        rangeMap.set(key, entry);
        return;
      }
      el.value = '';
    });
    rangeMap.forEach((entry) => {
      if (entry.fromEl) entry.fromEl.value = '';
      if (entry.toEl) entry.toEl.value = '';
    });
    const filters = Array.isArray(this.filters) ? this.filters : [];
    filters.forEach((f) => {
      const key = f?.key || '';
      if (!key) return;
      if (f.op === 'between' && Array.isArray(f.value)) {
        const entry = rangeMap.get(key);
        if (entry?.fromEl) entry.fromEl.value = String(f.value[0] ?? '');
        if (entry?.toEl) entry.toEl.value = String(f.value[1] ?? '');
        return;
      }
      inputs.forEach((el) => {
        if (el.dataset.filterKey !== key) return;
        if (el.dataset.filterRange) return;
        el.value = f.value ?? '';
      });
    });
    if (this.filterThs) {
      const active = new Set(filters.map(f => f.key));
      this.filterThs.forEach((th) => {
        if (!th.dataset.filter) return;
        const key = th.dataset.key || '';
        th.classList.toggle('filter-active', key !== '' && active.has(key));
      });
    }
    if (this.filterSummaryEl) {
      this.filterSummaryEl.textContent = filters.length ? `Filters: ${filters.length}` : '';
    }
  }

  isWrapScrollable() {
    if (!this.wrap) return false;
    return this.wrap.scrollHeight > this.wrap.clientHeight + 4;
  }

  totalPages() {
    return Math.max(1, Math.ceil((this.total || 0) / this.pageSize));
  }

  async load(page = 1, append = false) {
    if (!this.fetchPage || !this.renderRow || !this.tbody) return;
    if (this.loading) return;
    this.loading = true;

    const payload = {
      page,
      per_page: this.pageSize,
      sort: this.sort,
      dir: this.dir,
      filters: this.filters,
      params: this.params,
    };

    try {
      const res = await this.fetchPage(payload);
      const items = res.items || [];
      this.total = Number(res.total || 0);
      this.page = Number(res.page || page);
      if (!append) {
        this.tbody.innerHTML = '';
      }
      items.forEach(item => this.renderRow(this.tbody, item));
      this.applyColumnStyles();
      this.updateFooter();
    } finally {
      this.loading = false;
    }
  }

  reload() {
    return this.load(this.page, false);
  }

  updateFooter() {
    if (!this.footer) return;
    if (this.mode === 'infinite') {
      if (this.prevBtn) this.prevBtn.disabled = true;
      if (this.nextBtn) this.nextBtn.disabled = true;
    } else {
      const totalPages = this.totalPages();
      if (this.prevBtn) this.prevBtn.disabled = this.page <= 1;
      if (this.nextBtn) this.nextBtn.disabled = this.page >= totalPages;
    }

    if (this.pageEl) {
      const totalPages = this.totalPages();
      this.pageEl.textContent = `${this.page} / ${totalPages}`;
    }
    if (this.metaEl) {
      const start = (this.page - 1) * this.pageSize + 1;
      const end = Math.min(this.total, this.page * this.pageSize);
      if (this.total === 0) {
        this.metaEl.textContent = '';
      } else {
        this.metaEl.textContent = `${start}-${end} / ${this.total}`;
      }
    }
  }
}

window.TableController = TableController;
