/**
 * CardSection — collapsible section with name filtering and incremental
 * card reconciliation for card grids.
 *
 * Usage:
 *   const section = new CardSection('led-devices', {
 *       titleKey: 'targets.section.devices',
 *       gridClass: 'devices-grid',
 *       addCardOnclick: "showAddDevice()",
 *       keyAttr: 'data-device-id',
 *   });
 *
 *   const items = devices.map(d => ({ key: d.id, html: createDeviceCard(d) }));
 *
 *   if (section.isMounted()) {
 *       section.reconcile(items);          // incremental DOM diff
 *   } else {
 *       html += section.render(items);     // initial HTML string
 *       // after container.innerHTML = html:
 *       section.bind();
 *   }
 */

import { t } from './i18n.ts';
import { showBulkToolbar, hideBulkToolbar, updateBulkToolbar } from './bulk-toolbar.ts';
import { ICON_LIST_CHECKS, ICON_EYE, ICON_EYE_OFF } from './icons.ts';

export interface BulkAction {
    key: string;
    labelKey: string;
    icon?: string;
    style?: string;
    confirm?: string;
    handler: (ids: string[]) => Promise<void>;
}

export interface CardItem {
    key: string;
    html: string;
}

export interface CardSectionOpts {
    titleKey: string;
    gridClass: string;
    addCardOnclick?: string;
    keyAttr?: string;
    headerExtra?: string;
    collapsible?: boolean;
    emptyKey?: string;
    bulkActions?: BulkAction[];
}

const STORAGE_KEY = 'sections_collapsed';
const ORDER_PREFIX = 'card_order_';
const HIDDEN_KEY = 'hidden_cards';
const DRAG_THRESHOLD = 5;
const SCROLL_EDGE = 60;
const SCROLL_SPEED = 12;
const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

function _getHiddenMap(): Record<string, string[]> {
    try { return JSON.parse(localStorage.getItem(HIDDEN_KEY) as string) || {}; }
    catch { return {}; }
}

function _saveHiddenMap(map: Record<string, string[]>): void {
    localStorage.setItem(HIDDEN_KEY, JSON.stringify(map));
}

/** Get hidden IDs for a section. */
export function getHiddenIds(sectionKey: string): string[] {
    return _getHiddenMap()[sectionKey] || [];
}

/** Hide a card in a section. */
export function hideCard(sectionKey: string, id: string): void {
    const map = _getHiddenMap();
    const ids = map[sectionKey] || [];
    if (!ids.includes(id)) {
        map[sectionKey] = [...ids, id];
        _saveHiddenMap(map);
    }
}

/** Unhide a card in a section. */
export function unhideCard(sectionKey: string, id: string): void {
    const map = _getHiddenMap();
    const ids = map[sectionKey] || [];
    map[sectionKey] = ids.filter(x => x !== id);
    _saveHiddenMap(map);
}

function _getCollapsedMap(): Record<string, boolean> {
    try { return JSON.parse(localStorage.getItem(STORAGE_KEY) as string) || {}; }
    catch { return {}; }
}

/** Generate skeleton placeholder cards for loading state. */
export function renderSkeletonCards(count = 3, gridClass = 'devices-grid') {
    let html = '';
    for (let i = 0; i < count; i++) {
        const delay = `animation-delay: ${i * 0.15}s`;
        html += `<div class="skeleton-card">
            <div class="skeleton-line skeleton-line-title" style="${delay}"></div>
            <div class="skeleton-line skeleton-line-medium" style="${delay}"></div>
            <div class="skeleton-line skeleton-line-short" style="${delay}"></div>
            <div class="skeleton-actions">
                <div class="skeleton-btn" style="${delay}"></div>
                <div class="skeleton-btn" style="${delay}"></div>
            </div>
        </div>`;
    }
    return `<div class="${gridClass}">${html}</div>`;
}

/** Registry of all CardSection instances for global access. */
const _sectionRegistry = new Map<string, CardSection>();

/** Toggle hidden state of a card from any context (e.g. inline onclick). */
export function toggleCardHidden(sectionKey: string, id: string): void {
    const section = _sectionRegistry.get(sectionKey);
    if (section) section.toggleHidden(id);
}

export class CardSection {

    sectionKey: string;
    titleKey: string;
    gridClass: string;
    addCardOnclick: string;
    keyAttr: string;
    headerExtra: string;
    collapsible: boolean;
    emptyKey: string;
    bulkActions: BulkAction[] | null;
    _filterValue: string;
    _lastItems: CardItem[] | null;
    _dragState: any;
    _dragBound: boolean;
    _selecting: boolean;
    _selected: Set<string>;
    _lastClickedKey: string | null;
    _escHandler: ((e: KeyboardEvent) => void) | null;
    _pendingReconcile: CardItem[] | null;
    _animated: boolean;
    _showHidden: boolean;

    constructor(sectionKey: string, { titleKey, gridClass, addCardOnclick, keyAttr, headerExtra, collapsible, emptyKey, bulkActions }: CardSectionOpts) {
        this.sectionKey = sectionKey;
        this.titleKey = titleKey;
        this.gridClass = gridClass;
        this.addCardOnclick = addCardOnclick || '';
        this.keyAttr = keyAttr || '';
        this.headerExtra = headerExtra || '';
        this.collapsible = !!collapsible;
        this.emptyKey = emptyKey || '';
        this.bulkActions = bulkActions || null;
        this._filterValue = '';
        this._lastItems = null;
        this._dragState = null;
        this._dragBound = false;
        // Bulk selection state
        this._selecting = false;
        this._selected = new Set();
        this._lastClickedKey = null;
        this._escHandler = null;
        this._pendingReconcile = null;
        this._animated = false;
        this._showHidden = false;
        _sectionRegistry.set(sectionKey, this);
    }

    /** True if this section's DOM element exists (i.e. not the first render). */
    isMounted() {
        return !!document.querySelector(`[data-card-section="${this.sectionKey}"]`);
    }

    /** Returns section HTML string for initial innerHTML building. */
    render(items: CardItem[]) {
        this._lastItems = items;
        this._dragBound = false; // DOM will be recreated → need to re-init drag

        const hiddenIds = new Set(getHiddenIds(this.sectionKey));
        const visibleItems = this._showHidden ? items : items.filter(i => !hiddenIds.has(i.key));
        const hiddenCount = items.length - visibleItems.length + (this._showHidden ? 0 : 0);
        const totalHidden = items.filter(i => hiddenIds.has(i.key)).length;
        const count = visibleItems.length;
        const cardsHtml = visibleItems.map(i => {
            if (hiddenIds.has(i.key)) {
                return i.html.replace(/class="(card|template-card)/, 'class="$1 cs-card-hidden');
            }
            return i.html;
        }).join('');

        const isCollapsed = this.collapsible && !!_getCollapsedMap()[this.sectionKey];
        const chevronStyle = isCollapsed ? '' : ' style="transform:rotate(90deg)"';
        const contentDisplay = isCollapsed ? ' style="display:none"' : '';
        const collapsedClass = isCollapsed ? ' cs-collapsed' : '';

        const addCard = this.addCardOnclick
            ? `<div class="template-card add-template-card cs-add-card" data-cs-add="${this.sectionKey}" onclick="${this.addCardOnclick}"><div class="add-template-icon">+</div></div>`
            : '';

        const hiddenToggle = totalHidden > 0
            ? `<button type="button" class="cs-hidden-toggle${this._showHidden ? ' active' : ''}" data-cs-hidden-toggle="${this.sectionKey}" title="${t('section.show_hidden')}">${this._showHidden ? ICON_EYE : ICON_EYE_OFF} <span class="cs-hidden-badge">${totalHidden}</span></button>`
            : '';

        return `
        <div class="subtab-section${collapsedClass}" data-card-section="${this.sectionKey}">
            <div class="subtab-section-header cs-header" data-cs-toggle="${this.sectionKey}">
                ${this.collapsible ? `<span class="cs-chevron"${chevronStyle}>&#9654;</span>` : ''}
                <span class="cs-title" data-i18n="${this.titleKey}">${t(this.titleKey)}</span>
                <span class="cs-count">${count}</span>
                ${this.headerExtra ? `<span class="cs-header-extra">${this.headerExtra}</span>` : ''}
                ${hiddenToggle}
                ${this.bulkActions ? `<button type="button" class="cs-bulk-toggle" data-cs-bulk="${this.sectionKey}" title="${t('bulk.select')}">${ICON_LIST_CHECKS}</button>` : ''}
                <div class="cs-filter-wrap">
                    <input type="text" class="cs-filter" data-cs-filter="${this.sectionKey}"
                           data-i18n-placeholder="section.filter.placeholder" placeholder="${t('section.filter.placeholder')}" autocomplete="off">
                    <button type="button" class="cs-filter-reset" data-cs-filter-reset="${this.sectionKey}" data-i18n-title="section.filter.reset" title="${t('section.filter.reset')}">×</button>
                </div>
            </div>
            <div class="cs-content ${this.gridClass}" data-cs-content="${this.sectionKey}"${contentDisplay}>
                ${cardsHtml}
                ${addCard}
            </div>
        </div>`;
    }

    /** Attach event listeners after innerHTML is set. */
    bind() {
        const header = document.querySelector(`[data-cs-toggle="${this.sectionKey}"]`);
        const content = document.querySelector(`[data-cs-content="${this.sectionKey}"]`) as HTMLElement | null;
        const filterInput = document.querySelector(`[data-cs-filter="${this.sectionKey}"]`) as HTMLInputElement | null;
        if (!header || !content) return;

        if (this.collapsible) {
            header.addEventListener('mousedown', (e) => {
                if ((e.target as HTMLElement).closest('.cs-filter-wrap') || (e.target as HTMLElement).closest('.cs-header-extra')) return;
                this._toggleCollapse(header, content);
            });
        }

        if (filterInput) {
            const resetBtn = document.querySelector(`[data-cs-filter-reset="${this.sectionKey}"]`) as HTMLElement | null;
            const updateResetVisibility = () => {
                if (resetBtn) resetBtn.style.display = filterInput.value ? '' : 'none';
            };

            filterInput.addEventListener('mousedown', (e) => e.stopPropagation());
            if (resetBtn) resetBtn.addEventListener('mousedown', (e) => e.stopPropagation());

            let timer: any = null;
            filterInput.addEventListener('input', () => {
                clearTimeout(timer);
                updateResetVisibility();
                timer = setTimeout(() => {
                    this._filterValue = filterInput.value.trim();
                    this._applyFilter(content, this._filterValue);
                }, 150);
            });
            filterInput.addEventListener('keydown', (e: KeyboardEvent) => {
                if (e.key === 'Escape') {
                    if (filterInput.value) {
                        e.stopPropagation();
                        filterInput.value = '';
                        this._filterValue = '';
                        this._applyFilter(content, '');
                        updateResetVisibility();
                    }
                }
            });

            if (resetBtn) {
                resetBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    filterInput.value = '';
                    this._filterValue = '';
                    this._applyFilter(content, '');
                    updateResetVisibility();
                    filterInput.focus();
                });
            }

            // Restore filter from before re-render
            if (this._filterValue) {
                filterInput.value = this._filterValue;
                this._applyFilter(content, this._filterValue);
            }
            updateResetVisibility();
        }

        // Hidden cards toggle button
        const hiddenToggleBtn = document.querySelector(`[data-cs-hidden-toggle="${this.sectionKey}"]`);
        if (hiddenToggleBtn) {
            hiddenToggleBtn.addEventListener('mousedown', (e) => e.stopPropagation());
            hiddenToggleBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                this._showHidden = !this._showHidden;
                // Re-render by triggering reconcile with last items
                if (this._lastItems) this.reconcile(this._lastItems);
                // Update toggle button visual
                this._updateHiddenToggle();
            });
        }

        // Bulk selection toggle button
        if (this.bulkActions) {
            const bulkBtn = document.querySelector(`[data-cs-bulk="${this.sectionKey}"]`);
            if (bulkBtn) {
                bulkBtn.addEventListener('mousedown', (e) => e.stopPropagation());
                bulkBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    if (this._selecting) this.exitSelectionMode();
                    else this.enterSelectionMode();
                });
            }

            // Card click delegation for selection
            // Ctrl+Click on a card auto-enters bulk mode if not already selecting
            content.addEventListener('click', (e: MouseEvent) => {
                if (!this.keyAttr) return;
                const card = (e.target as HTMLElement).closest(`[${this.keyAttr}]`);
                if (!card) return;
                // Don't hijack clicks on buttons, links, inputs inside cards
                if ((e.target as HTMLElement).closest('button, a, input, select, textarea, .card-actions, .template-card-actions, .color-picker-wrapper')) return;

                // Auto-enter selection mode on Ctrl/Cmd+Click
                if (!this._selecting && (e.ctrlKey || e.metaKey)) {
                    this.enterSelectionMode();
                }
                if (!this._selecting) return;

                const key = card.getAttribute(this.keyAttr);
                if (!key) return;

                if (e.shiftKey && this._lastClickedKey) {
                    this._selectRange(content, this._lastClickedKey, key);
                } else {
                    this._toggleSelect(key);
                }
                this._lastClickedKey = key;
            });

            // Escape to exit selection mode
            this._escHandler = (e: KeyboardEvent) => {
                if (e.key === 'Escape' && this._selecting) {
                    this.exitSelectionMode();
                }
            };
            document.addEventListener('keydown', this._escHandler);
        }

        // Tag card elements with their source HTML for future reconciliation
        this._tagCards(content);

        // Inject hide buttons and drag handles, initialize drag-and-drop
        if (this.keyAttr) {
            this._injectHideButtons(content);
            this._injectDragHandles(content);
            this._initDrag(content);
        }

        // Stagger card entrance animation
        this._animateEntrance(content);

        // Cache searchable text on cards for faster filtering
        this._cacheSearchText(content);
    }

    /** Incremental DOM diff — update cards in-place without rebuilding the section. */
    reconcile(items: CardItem[]) {
        // Skip DOM mutations while a drag is in progress — would destroy drag state
        if (this._dragState) {
            this._pendingReconcile = items;
            return { added: new Set(), replaced: new Set(), removed: new Set() };
        }

        const content = document.querySelector(`[data-cs-content="${this.sectionKey}"]`) as HTMLElement | null;
        if (!content) return { added: new Set(), replaced: new Set(), removed: new Set() };

        this._lastItems = items;

        // Filter hidden items
        const hiddenIds = new Set(getHiddenIds(this.sectionKey));
        const visibleItems = this._showHidden ? items : items.filter(i => !hiddenIds.has(i.key));

        // Update count badge (will be refined by _applyFilter if a filter is active)
        const countEl = document.querySelector(`[data-cs-toggle="${this.sectionKey}"] .cs-count`);
        if (countEl && !this._filterValue) countEl.textContent = String(visibleItems.length);

        // Update hidden toggle button
        this._updateHiddenToggle();

        // Remove any stale empty-state element from DOM
        const staleEmpty = content.querySelector(`[data-cs-empty="${this.sectionKey}"]`);
        if (staleEmpty) staleEmpty.remove();

        // Mark hidden cards with CSS class in HTML
        const processedItems = visibleItems.map(i => {
            if (hiddenIds.has(i.key)) {
                return { key: i.key, html: i.html.replace(/class="(card|template-card)/, 'class="$1 cs-card-hidden') };
            }
            return i;
        });

        const newMap = new Map(processedItems.map(i => [i.key, i.html]));
        const addCard = content.querySelector('.cs-add-card');
        const added = new Set<string>();
        const replaced = new Set<string>();
        const removed = new Set<string>();

        // Process existing cards: remove or update
        if (this.keyAttr) {
            const existing = [...content.querySelectorAll(`[${this.keyAttr}]`)];
            for (const card of existing) {
                const key = card.getAttribute(this.keyAttr) ?? '';
                if (!newMap.has(key)) {
                    card.remove();
                    removed.add(key);
                } else {
                    const newHtml = newMap.get(key)!;
                    if ((card as any)._csHtml !== newHtml) {
                        const tmp = document.createElement('div');
                        tmp.innerHTML = newHtml;
                        const newEl = tmp.firstElementChild as any;
                        newEl._csHtml = newHtml;
                        card.replaceWith(newEl);
                        replaced.add(key!);
                    }
                    // else: unchanged — skip
                }
            }

            // Insert new cards (keys not already in DOM)
            const existingKeys = new Set([...content.querySelectorAll(`[${this.keyAttr}]`)].map(
                el => el.getAttribute(this.keyAttr)
            ));
            for (const { key, html } of processedItems) {
                if (!existingKeys.has(key)) {
                    const tmp = document.createElement('div');
                    tmp.innerHTML = html;
                    const newEl = tmp.firstElementChild as any;
                    newEl._csHtml = html;
                    if (addCard) content.insertBefore(newEl, addCard);
                    else content.appendChild(newEl);
                    added.add(key);
                }
            }
        }

        // Animate newly added cards
        if (added.size > 0 && this.keyAttr && !_reducedMotion.matches) {
            let delay = 0;
            for (const key of added) {
                const card = content.querySelector(`[${this.keyAttr}="${key}"]`) as HTMLElement | null;
                if (card) {
                    card.style.animationDelay = `${delay}ms`;
                    card.classList.add('card-enter');
                    card.addEventListener('animationend', () => card.classList.remove('card-enter'), { once: true });
                    delay += 30;
                }
            }
        }

        // Re-inject hide buttons and drag handles on new/replaced cards
        if (this.keyAttr && (added.size > 0 || replaced.size > 0)) {
            this._injectHideButtons(content);
            this._injectDragHandles(content);
        }

        // Re-cache searchable text for new/replaced cards
        if (added.size > 0 || replaced.size > 0) {
            this._cacheSearchText(content);
        }

        // Re-apply filter
        if (this._filterValue) {
            this._applyFilter(content, this._filterValue);
        }

        // Re-apply bulk selection state after reconcile
        if (this._selecting && this.keyAttr) {
            // Remove selected keys that were removed from DOM
            for (const key of removed) this._selected.delete(key);
            // Re-apply .card-selected on surviving cards
            for (const key of this._selected) {
                const card = content.querySelector(`[${this.keyAttr}="${key}"]`);
                if (card) card.classList.add('card-selected');
            }
            // Inject checkboxes on new/replaced cards
            if (added.size > 0 || replaced.size > 0) {
                this._injectCheckboxes(content);
            }
            updateBulkToolbar();
        }

        return { added, replaced, removed };
    }

    /** Bind an array of CardSection instances. */
    static bindAll(sections: CardSection[]) {
        for (const s of sections) s.bind();
    }

    /** Programmatically expand this section if collapsed. */
    expand() {
        const header = document.querySelector(`[data-cs-toggle="${this.sectionKey}"]`);
        const content = document.querySelector(`[data-cs-content="${this.sectionKey}"]`) as HTMLElement | null;
        if (!header || !content) return;
        const map = _getCollapsedMap();
        if (map[this.sectionKey]) {
            map[this.sectionKey] = false;
            localStorage.setItem(STORAGE_KEY, JSON.stringify(map));
            content.style.display = '';
            const section = header.closest('[data-card-section]');
            if (section) section.classList.remove('cs-collapsed');
            const chevron = header.querySelector('.cs-chevron') as HTMLElement | null;
            if (chevron) chevron.style.transform = 'rotate(90deg)';
        }
    }

    /**
     * Reorder items array according to saved drag order.
     * Call before render() / reconcile().
     */
    applySortOrder(items: CardItem[]) {
        if (!this.keyAttr) return items;
        const order = this._getSavedOrder();
        if (!order.length) return items;
        const orderMap = new Map(order.map((key: string, idx: number) => [key, idx]));
        const sorted = [...items];
        sorted.sort((a, b) => {
            const ia = orderMap.has(a.key) ? orderMap.get(a.key) : Infinity;
            const ib = orderMap.has(b.key) ? orderMap.get(b.key) : Infinity;
            if (ia !== ib) return (ia as number) - (ib as number);
            return 0; // preserve original order for unranked items
        });
        return sorted;
    }

    // ── Bulk selection ──

    enterSelectionMode() {
        if (this._selecting) return;
        this._selecting = true;
        this._selected.clear();
        this._lastClickedKey = null;
        const section = document.querySelector(`[data-card-section="${this.sectionKey}"]`);
        if (section) section.classList.add('cs-selecting');
        const btn = document.querySelector(`[data-cs-bulk="${this.sectionKey}"]`);
        if (btn) btn.classList.add('active');
        const content = document.querySelector(`[data-cs-content="${this.sectionKey}"]`) as HTMLElement | null;
        if (content) this._injectCheckboxes(content);
        showBulkToolbar(this);
    }

    exitSelectionMode() {
        if (!this._selecting) return;
        this._selecting = false;
        this._selected.clear();
        this._lastClickedKey = null;
        const section = document.querySelector(`[data-card-section="${this.sectionKey}"]`);
        if (section) section.classList.remove('cs-selecting');
        const btn = document.querySelector(`[data-cs-bulk="${this.sectionKey}"]`);
        if (btn) btn.classList.remove('active');
        // Remove checkboxes and selection classes
        const content = document.querySelector(`[data-cs-content="${this.sectionKey}"]`);
        if (content) {
            content.querySelectorAll('.card-bulk-check').forEach(cb => cb.remove());
            content.querySelectorAll('.card-selected').forEach(c => c.classList.remove('card-selected'));
        }
        hideBulkToolbar();
    }

    _toggleSelect(key: string) {
        if (this._selected.has(key)) this._selected.delete(key);
        else this._selected.add(key);
        this._applySelectionVisuals();
        updateBulkToolbar();
    }

    _selectRange(content: HTMLElement, fromKey: string, toKey: string) {
        const cards = [...content.querySelectorAll(`[${this.keyAttr}]`)] as HTMLElement[];
        const keys = cards.map(c => c.getAttribute(this.keyAttr)!);
        const fromIdx = keys.indexOf(fromKey);
        const toIdx = keys.indexOf(toKey);
        if (fromIdx < 0 || toIdx < 0) return;
        const lo = Math.min(fromIdx, toIdx);
        const hi = Math.max(fromIdx, toIdx);
        for (let i = lo; i <= hi; i++) {
            const card = cards[i];
            if (card.style.display !== 'none') { // respect filter
                this._selected.add(keys[i]);
            }
        }
        this._applySelectionVisuals();
        updateBulkToolbar();
    }

    selectAll() {
        const content = document.querySelector(`[data-cs-content="${this.sectionKey}"]`);
        if (!content || !this.keyAttr) return;
        (content.querySelectorAll(`[${this.keyAttr}]`) as NodeListOf<HTMLElement>).forEach(card => {
            if (card.style.display !== 'none') {
                this._selected.add(card.getAttribute(this.keyAttr)!);
            }
        });
        this._applySelectionVisuals();
        updateBulkToolbar();
    }

    deselectAll() {
        this._selected.clear();
        this._applySelectionVisuals();
        updateBulkToolbar();
    }

    _visibleCardCount() {
        const content = document.querySelector(`[data-cs-content="${this.sectionKey}"]`);
        if (!content || !this.keyAttr) return 0;
        let count = 0;
        (content.querySelectorAll(`[${this.keyAttr}]`) as NodeListOf<HTMLElement>).forEach(card => {
            if (card.style.display !== 'none') count++;
        });
        return count;
    }

    _applySelectionVisuals() {
        const content = document.querySelector(`[data-cs-content="${this.sectionKey}"]`);
        if (!content || !this.keyAttr) return;
        content.querySelectorAll(`[${this.keyAttr}]`).forEach(card => {
            const key = card.getAttribute(this.keyAttr);
            const selected = this._selected.has(key!);
            card.classList.toggle('card-selected', selected);
            const cb = card.querySelector('.card-bulk-check') as HTMLInputElement | null;
            if (cb) cb.checked = selected;
        });
    }

    _injectCheckboxes(content: HTMLElement) {
        if (!this.keyAttr) return;
        content.querySelectorAll(`[${this.keyAttr}]`).forEach(card => {
            if (card.querySelector('.card-bulk-check')) return;
            const cb = document.createElement('input');
            cb.type = 'checkbox';
            cb.className = 'card-bulk-check';
            cb.checked = this._selected.has(card.getAttribute(this.keyAttr)!);
            cb.addEventListener('click', (e: MouseEvent) => {
                e.stopPropagation();
                const key = card.getAttribute(this.keyAttr)!;
                if (e.shiftKey && this._lastClickedKey) {
                    this._selectRange(content, this._lastClickedKey, key);
                } else {
                    this._toggleSelect(key);
                }
                this._lastClickedKey = key;
            });
            // Insert as first child of .card-top-actions, or prepend to card
            const topActions = card.querySelector('.card-top-actions');
            if (topActions) topActions.prepend(cb);
            else card.prepend(cb);
        });
    }

    _getSavedOrder() {
        try { return JSON.parse(localStorage.getItem(ORDER_PREFIX + this.sectionKey) as string) || []; }
        catch { return []; }
    }

    _saveOrder(keys: (string | null)[]) {
        localStorage.setItem(ORDER_PREFIX + this.sectionKey, JSON.stringify(keys));
    }

    // ── private ──

    _animateEntrance(content: HTMLElement) {
        if (this._animated) return;
        this._animated = true;
        if (_reducedMotion.matches) return;
        const selector = this.keyAttr ? `[${this.keyAttr}]` : '.card, .template-card:not(.add-template-card)';
        const cards = content.querySelectorAll(selector) as NodeListOf<HTMLElement>;
        cards.forEach((card, i) => {
            card.style.animationDelay = `${i * 30}ms`;
            card.classList.add('card-enter');
            card.addEventListener('animationend', () => card.classList.remove('card-enter'), { once: true });
        });
    }

    _tagCards(content: HTMLElement) {
        if (!this.keyAttr || !this._lastItems) return;
        const htmlMap = new Map(this._lastItems.map(i => [i.key, i.html]));
        const cards = content.querySelectorAll(`[${this.keyAttr}]`);
        cards.forEach(card => {
            const key = card.getAttribute(this.keyAttr) ?? '';
            if (htmlMap.has(key)) (card as any)._csHtml = htmlMap.get(key);
        });
    }

    /** Cache each card's lowercased text content in data-search for fast filtering. */
    _cacheSearchText(content: HTMLElement) {
        const cards = content.querySelectorAll('.card, .template-card:not(.add-template-card)') as NodeListOf<HTMLElement>;
        cards.forEach(card => {
            card.dataset.search = card.textContent!.toLowerCase();
        });
    }

    _toggleCollapse(header: Element, content: HTMLElement) {
        const map = _getCollapsedMap();
        const nowCollapsed = !map[this.sectionKey];
        map[this.sectionKey] = nowCollapsed;
        localStorage.setItem(STORAGE_KEY, JSON.stringify(map));

        const section = header.closest('[data-card-section]');
        if (section) section.classList.toggle('cs-collapsed', nowCollapsed);

        const chevron = header.querySelector('.cs-chevron') as HTMLElement | null;
        if (chevron) chevron.style.transform = nowCollapsed ? '' : 'rotate(90deg)';

        // Cancel any running animation on this content
        if ((content as any)._csAnim) { (content as any)._csAnim.cancel(); (content as any)._csAnim = null; }

        // Skip animation when user prefers reduced motion
        if (_reducedMotion.matches) {
            content.style.display = nowCollapsed ? 'none' : '';
            return;
        }

        if (nowCollapsed) {
            const h = content.offsetHeight;
            content.style.overflow = 'hidden';
            const anim = content.animate(
                [{ height: h + 'px', opacity: 1 }, { height: '0px', opacity: 0 }],
                { duration: 200, easing: 'ease-in-out' }
            );
            (content as any)._csAnim = anim;
            anim.onfinish = () => {
                content.style.display = 'none';
                content.style.overflow = '';
                (content as any)._csAnim = null;
            };
        } else {
            // Intentional forced layout: reading scrollHeight after setting display=''
            // is required to measure the target height for the expand animation.
            content.style.display = '';
            content.style.overflow = 'hidden';
            const h = content.scrollHeight;
            const anim = content.animate(
                [{ height: '0px', opacity: 0 }, { height: h + 'px', opacity: 1 }],
                { duration: 200, easing: 'ease-in-out' }
            );
            (content as any)._csAnim = anim;
            anim.onfinish = () => {
                content.style.overflow = '';
                (content as any)._csAnim = null;
            };
        }
    }

    _applyFilter(content: HTMLElement, query: string) {
        const cards = content.querySelectorAll('.card, .template-card:not(.add-template-card)') as NodeListOf<HTMLElement>;
        const addCard = content.querySelector('.cs-add-card') as HTMLElement | null;
        const countEl = document.querySelector(`[data-cs-toggle="${this.sectionKey}"] .cs-count`);
        const total = cards.length;

        if (!query) {
            content.classList.remove('cs-filtering');
            cards.forEach(card => { card.style.display = ''; });
            if (addCard) addCard.style.display = '';
            if (countEl) countEl.textContent = String(total);
            return;
        }
        content.classList.add('cs-filtering');

        const lower = query.toLowerCase();
        // Comma-separated segments → AND; spaces within a segment → OR
        const groups = lower.split(',').map(g => g.trim().split(/\s+/).filter(Boolean)).filter(g => g.length);
        let visible = 0;

        cards.forEach(card => {
            const text = card.dataset.search || card.textContent!.toLowerCase();
            // Each group must have at least one matching term (AND of ORs)
            const match = groups.every(orTerms => orTerms.some(term => text.includes(term)));
            card.style.display = match ? '' : 'none';
            if (match) visible++;
        });

        if (addCard) addCard.style.display = 'none';
        if (countEl) countEl.textContent = `${visible}/${total}`;
    }

    /** Update the hidden toggle button's visual state. */
    _updateHiddenToggle() {
        const btn = document.querySelector(`[data-cs-hidden-toggle="${this.sectionKey}"]`) as HTMLElement | null;
        const totalHidden = this._lastItems ? this._lastItems.filter(i => getHiddenIds(this.sectionKey).includes(i.key)).length : 0;

        if (totalHidden === 0 && btn) {
            btn.remove();
            return;
        }

        if (totalHidden > 0 && !btn) {
            // Insert toggle button into header
            const bulkBtn = document.querySelector(`[data-cs-bulk="${this.sectionKey}"]`);
            const filterWrap = document.querySelector(`[data-cs-toggle="${this.sectionKey}"] .cs-filter-wrap`);
            const insertBefore = bulkBtn || filterWrap;
            if (insertBefore) {
                const newBtn = document.createElement('button');
                newBtn.type = 'button';
                newBtn.className = `cs-hidden-toggle${this._showHidden ? ' active' : ''}`;
                newBtn.dataset.csHiddenToggle = this.sectionKey;
                newBtn.title = t('section.show_hidden');
                newBtn.innerHTML = `${this._showHidden ? ICON_EYE : ICON_EYE_OFF} <span class="cs-hidden-badge">${totalHidden}</span>`;
                newBtn.addEventListener('mousedown', (e) => e.stopPropagation());
                newBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    this._showHidden = !this._showHidden;
                    if (this._lastItems) this.reconcile(this._lastItems);
                    this._updateHiddenToggle();
                });
                insertBefore.parentNode!.insertBefore(newBtn, insertBefore);
            }
            return;
        }

        if (btn) {
            btn.classList.toggle('active', this._showHidden);
            btn.innerHTML = `${this._showHidden ? ICON_EYE : ICON_EYE_OFF} <span class="cs-hidden-badge">${totalHidden}</span>`;
        }
    }

    /** Toggle hidden state for a card and re-render. */
    toggleHidden(id: string) {
        const hidden = getHiddenIds(this.sectionKey);
        if (hidden.includes(id)) {
            unhideCard(this.sectionKey, id);
        } else {
            hideCard(this.sectionKey, id);
        }
        if (this._lastItems) this.reconcile(this._lastItems);
    }

    // ── drag-and-drop reordering ──

    _injectHideButtons(content: HTMLElement) {
        if (!this.keyAttr) return;
        content.querySelectorAll(`[${this.keyAttr}]`).forEach(card => {
            if (card.querySelector('.card-hide-btn')) return;
            const topActions = card.querySelector('.card-top-actions');
            if (!topActions) return;
            const key = card.getAttribute(this.keyAttr)!;
            const btn = document.createElement('button');
            btn.className = 'card-hide-btn';
            btn.title = 'Hide';
            btn.innerHTML = ICON_EYE_OFF;
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                this.toggleHidden(key);
            });
            // Insert as first child so order is [hide] [power] [trash]
            topActions.insertBefore(btn, topActions.firstChild);
        });
    }

    _injectDragHandles(content: HTMLElement) {
        const cards = content.querySelectorAll(`[${this.keyAttr}]`);
        cards.forEach(card => {
            if (card.querySelector('.card-drag-handle')) return;
            const handle = document.createElement('span');
            handle.className = 'card-drag-handle';
            handle.textContent = '\u2807'; // ⠇ braille dots vertical
            handle.setAttribute('aria-hidden', 'true');
            card.prepend(handle);
        });
    }

    _initDrag(content: HTMLElement) {
        if (this._dragBound) return;
        this._dragBound = true;

        content.addEventListener('pointerdown', (e: PointerEvent) => {
            if (this._filterValue) return;
            const handle = (e.target as HTMLElement).closest('.card-drag-handle');
            if (!handle) return;
            const card = handle.closest(`[${this.keyAttr}]`) as HTMLElement | null;
            if (!card) return;

            e.preventDefault();
            this._dragState = {
                card,
                content,
                startX: e.clientX,
                startY: e.clientY,
                started: false,
                clone: null,
                placeholder: null,
                scrollRaf: null,
            };

            const onMove = (ev: PointerEvent) => this._onDragMove(ev);
            const cleanup = () => {
                document.removeEventListener('pointermove', onMove);
                document.removeEventListener('pointerup', cleanup);
                document.removeEventListener('pointercancel', cleanup);
                this._onDragEnd();
            };
            document.addEventListener('pointermove', onMove);
            document.addEventListener('pointerup', cleanup);
            document.addEventListener('pointercancel', cleanup);
        });
    }

    _onDragMove(e: PointerEvent) {
        const ds = this._dragState;
        if (!ds) return;

        if (!ds.started) {
            const dx = e.clientX - ds.startX;
            const dy = e.clientY - ds.startY;
            if (Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) return;
            this._startDrag(ds, e);
        }

        // Position clone at pointer
        ds.clone.style.left = (e.clientX - ds.offsetX) + 'px';
        ds.clone.style.top = (e.clientY - ds.offsetY) + 'px';

        // Find which card the pointer is over — only move placeholder on hit
        const hit = this._hitTestCard(e.clientX, e.clientY, ds);
        if (hit) {
            const r = hit.getBoundingClientRect();
            const before = e.clientX < r.left + r.width / 2;
            // Track both target card and direction to avoid dead zones at last card
            if (hit !== ds._lastHit || before !== ds._lastBefore) {
                ds._lastHit = hit;
                ds._lastBefore = before;
                if (before) {
                    ds.content.insertBefore(ds.placeholder, hit);
                } else if (hit.nextElementSibling && hit.nextElementSibling.hasAttribute(this.keyAttr)) {
                    ds.content.insertBefore(ds.placeholder, hit.nextElementSibling);
                } else {
                    // Last card in grid — insert after it but before add-button
                    hit.after(ds.placeholder);
                }
            }
        }

        // Auto-scroll near viewport edges
        this._autoScroll(e.clientY, ds);
    }

    _startDrag(ds: any, e: PointerEvent) {
        ds.started = true;
        const rect = ds.card.getBoundingClientRect();

        // Clone for visual drag
        const clone = ds.card.cloneNode(true) as HTMLElement;
        clone.className = ds.card.className + ' card-drag-clone';
        clone.style.width = rect.width + 'px';
        clone.style.height = rect.height + 'px';
        clone.style.left = rect.left + 'px';
        clone.style.top = rect.top + 'px';
        document.body.appendChild(clone);
        ds.clone = clone;
        ds.offsetX = e.clientX - rect.left;
        ds.offsetY = e.clientY - rect.top;

        // Placeholder
        const placeholder = document.createElement('div');
        placeholder.className = 'card-drag-placeholder';
        placeholder.style.width = rect.width + 'px';
        placeholder.style.height = rect.height + 'px';
        ds.card.parentNode.insertBefore(placeholder, ds.card);
        ds.placeholder = placeholder;

        // Hide original
        ds.card.style.display = 'none';
        document.body.classList.add('cs-drag-active');

    }

    _onDragEnd() {
        const ds = this._dragState;
        this._dragState = null;
        if (!ds || !ds.started) return;

        // Cancel auto-scroll
        if (ds.scrollRaf) cancelAnimationFrame(ds.scrollRaf);

        // Move original card to placeholder position
        ds.content.insertBefore(ds.card, ds.placeholder);
        ds.card.style.display = '';
        ds.placeholder.remove();
        ds.clone.remove();
        document.body.classList.remove('cs-drag-active');

        // Save new order from DOM
        const keys = this._readDomOrder(ds.content);
        this._saveOrder(keys);

        // Flush any reconcile that was deferred during drag
        if (this._pendingReconcile) {
            const items = this._pendingReconcile;
            this._pendingReconcile = null;
            this.reconcile(items);
        }
    }

    /**
     * Point-in-rect hit test: find which card the pointer is directly over.
     * Only triggers placeholder move when cursor is inside a card — dragging
     * over gaps keeps the placeholder in its last position.
     */
    _hitTestCard(x: number, y: number, ds: any): HTMLElement | null {
        const cards = ds.content.querySelectorAll(`[${this.keyAttr}]`) as NodeListOf<HTMLElement>;
        for (const card of cards) {
            if (card === ds.card) continue;
            if (card.style.display === 'none') continue;
            const r = card.getBoundingClientRect();
            if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
                return card;
            }
        }
        return null;
    }

    _readDomOrder(content: HTMLElement) {
        return [...content.querySelectorAll(`[${this.keyAttr}]`)]
            .map(el => el.getAttribute(this.keyAttr));
    }

    _autoScroll(clientY: number, ds: any) {
        if (ds.scrollRaf) cancelAnimationFrame(ds.scrollRaf);
        const vp = window.innerHeight;
        let speed = 0;
        if (clientY < SCROLL_EDGE) speed = -SCROLL_SPEED * (1 - clientY / SCROLL_EDGE);
        else if (clientY > vp - SCROLL_EDGE) speed = SCROLL_SPEED * (1 - (vp - clientY) / SCROLL_EDGE);

        if (Math.abs(speed) < 0.5) return;
        const scroll = () => {
            window.scrollBy(0, speed);
            ds.scrollRaf = requestAnimationFrame(scroll);
        };
        ds.scrollRaf = requestAnimationFrame(scroll);
    }
}
