(() => {
    'use strict';

    const syncCategoryRootState = (form) => {
        const root = form.querySelector('[data-category-root]');
        const parent = form.querySelector('[data-category-parent]');
        const parentField = form.querySelector('[data-category-parent-field]');
        if (!root || !parent) return;

        parent.disabled = root.checked;
        if (root.checked) parent.value = '';
        if (parentField) parentField.classList.toggle('is-disabled', root.checked);
    };

    for (const form of document.querySelectorAll('[data-category-form]')) {
        syncCategoryRootState(form);
        form.querySelector('[data-category-root]')?.addEventListener('change', () => syncCategoryRootState(form));
    }
})();

(() => {
    'use strict';

    // Managed primary-menu ordering mirrors the public header. The hidden
    // order[] fields travel with their rows, so the server receives the same
    // sequence that the operator sees when the form is submitted.
    for (const list of document.querySelectorAll('[data-menu-reorder-list]')) {
        let dragged = null;

        const rowFrom = (target) => target instanceof Element ? target.closest('[data-menu-item]') : null;

        list.addEventListener('dragstart', (event) => {
            dragged = rowFrom(event.target);
            if (!dragged) return;
            dragged.classList.add('is-dragging');
            if (event.dataTransfer) {
                event.dataTransfer.effectAllowed = 'move';
                event.dataTransfer.setData('text/plain', dragged.dataset.menuItemId || '');
            }
        });

        list.addEventListener('dragend', () => {
            if (dragged) dragged.classList.remove('is-dragging');
            dragged = null;
        });

        list.addEventListener('dragover', (event) => {
            if (!dragged) return;
            event.preventDefault();

            const over = rowFrom(event.target);
            if (!over || over === dragged) return;
            const box = over.getBoundingClientRect();
            const after = (event.clientY - box.top) > box.height / 2;
            over.parentNode.insertBefore(dragged, after ? over.nextSibling : over);
        });

        list.addEventListener('drop', (event) => event.preventDefault());

        list.addEventListener('click', (event) => {
            const button = event.target instanceof Element ? event.target.closest('[data-menu-move]') : null;
            if (!button) return;

            const row = rowFrom(button);
            if (!row) return;
            if (button.dataset.menuMove === 'up' && row.previousElementSibling) {
                row.parentNode.insertBefore(row, row.previousElementSibling);
            } else if (button.dataset.menuMove === 'down' && row.nextElementSibling) {
                row.parentNode.insertBefore(row.nextElementSibling, row);
            }
            button.focus();
        });
    }
})();

(() => {
    'use strict';

    // Sidebar accordion. The <details> elements collapse on their own, so this
    // only enforces that opening one group closes the others, and remembers the
    // choice across page loads. Without JavaScript every group still opens and
    // closes normally -- it just allows more than one open at a time.
    const nav = document.querySelector('[data-admin-nav]');
    if (!nav) return;

    const groups = [...nav.querySelectorAll('.admin-nav-group')];
    if (groups.length === 0) return;

    const storageKey = 'quaeriva_admin_nav_group';
    const groupName = (group) => group.querySelector('summary > span')?.textContent?.trim() || '';

    let restoring = false;

    try {
        const remembered = window.localStorage.getItem(storageKey);
        // The group holding the current page wins; only fall back to the
        // remembered one when the server did not open anything.
        if (remembered && !groups.some((group) => group.open)) {
            for (const group of groups) {
                if (groupName(group) === remembered) group.open = true;
            }
        }
    } catch (error) {
        // Storage is unavailable in private or hardened browser modes.
    }

    for (const group of groups) {
        group.addEventListener('toggle', () => {
            if (restoring || !group.open) return;

            restoring = true;
            for (const other of groups) {
                if (other !== group) other.open = false;
            }
            restoring = false;

            try {
                window.localStorage.setItem(storageKey, groupName(group));
            } catch (error) {
                // Ignored, see above.
            }
        });
    }
})();

(() => {
    'use strict';

    // Opening a record editor is a full page load, so the browser lands at the
    // top and the form the operator just asked for is somewhere below. Bring it
    // into view and put the caret in its first field. Applies to every editor
    // on the panel, so a new one needs no wiring.
    const editor = document.querySelector('.admin-record-editor');
    if (!editor) return;

    const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    editor.scrollIntoView({ block: 'start', behavior: reduceMotion ? 'auto' : 'smooth' });

    const first = editor.querySelector('input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled])');
    // preventScroll keeps focus from fighting the scroll that just started.
    if (first) first.focus({ preventScroll: true });
})();

(() => {
    'use strict';

    // Reordering widgets inside a page region.
    //
    // Dragging and the up/down buttons do the same thing: move the row, which
    // moves its hidden order[] input with it. Nothing is saved until the form
    // is submitted, so a mis-drop is undone by leaving the page. The buttons
    // exist because drag-and-drop alone is unusable by keyboard and awkward on
    // touch.
    for (const list of document.querySelectorAll('[data-reorder-list]')) {
        let dragged = null;

        const rowFrom = (target) => target instanceof Element ? target.closest('[data-reorder-item]') : null;

        list.addEventListener('dragstart', (event) => {
            dragged = rowFrom(event.target);
            if (!dragged) return;
            dragged.classList.add('is-dragging');
            if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
        });

        list.addEventListener('dragend', () => {
            if (dragged) dragged.classList.remove('is-dragging');
            dragged = null;
        });

        list.addEventListener('dragover', (event) => {
            if (!dragged) return;
            event.preventDefault();

            const over = rowFrom(event.target);
            if (!over || over === dragged) return;

            // Insert before or after depending on which half was entered, so
            // the row lands where the pointer actually is.
            const box = over.getBoundingClientRect();
            const after = (event.clientY - box.top) > box.height / 2;
            over.parentNode.insertBefore(dragged, after ? over.nextSibling : over);
        });

        list.addEventListener('drop', (event) => event.preventDefault());

        list.addEventListener('click', (event) => {
            const button = event.target instanceof Element ? event.target.closest('[data-move]') : null;
            if (!button) return;

            const row = rowFrom(button);
            if (!row) return;

            if (button.dataset.move === 'up' && row.previousElementSibling) {
                row.parentNode.insertBefore(row, row.previousElementSibling);
            } else if (button.dataset.move === 'down' && row.nextElementSibling) {
                row.parentNode.insertBefore(row.nextElementSibling, row);
            }

            button.focus();
        });
    }
})();

(() => {
    'use strict';

    // Appearance palette: the colour picker and the hex box are two views of
    // one value. Only the picker carries the field name, so the text box is
    // purely an accessible way to type or paste a hex code.
    for (const picker of document.querySelectorAll('[data-color-sync]')) {
        const text = document.querySelector('[data-color-text="' + picker.dataset.colorSync + '"]');
        if (!text) continue;

        picker.addEventListener('input', () => { text.value = picker.value.toUpperCase(); });
        text.addEventListener('input', () => {
            const value = text.value.trim();
            if (/^#[0-9A-Fa-f]{6}$/.test(value)) picker.value = value;
        });
    }
})();

(() => {
    'use strict';

    // Widget editor: show only the selected type's configuration fields. Every
    // group is rendered server-side, so switching type needs no request; the
    // hidden groups stay disabled so their values are never submitted.
    const typeSelect = document.querySelector('select[name="type"]');
    const groups = [...document.querySelectorAll('[data-widget-config]')];
    if (!typeSelect || groups.length === 0) return;

    const sync = () => {
        for (const group of groups) {
            const active = group.dataset.widgetConfig === typeSelect.value;
            group.hidden = !active;
            for (const field of group.querySelectorAll('input, select, textarea')) {
                field.disabled = !active;
            }
        }
    };

    typeSelect.addEventListener('change', sync);
    sync();
})();

(() => {
    'use strict';
    for (const button of document.querySelectorAll('.admin-confirm-delete')) {
        const form = button.closest('form');
        if (!form) continue;
        form.addEventListener('submit', (event) => {
            const message = document.documentElement.dataset.deleteConfirm || 'Delete this item? This action cannot be undone.';
            if (!window.confirm(message)) {
                event.preventDefault();
            }
        });
    }
})();
