import {Controller} from '@hotwired/stimulus';
import {getComponent} from '@symfony/ux-live-component';
import {enter, leave} from 'el-transition';

export default class extends Controller {
    static values = {
        expanded: { type: Boolean, default: true },
        key: String,
        viewKey: String,
        viewPathPrefix: String,
    }
    static targets = ["sidebar", "button", "pills"]

// URL params we mirror to localStorage per list. Each entry maps a query
    // param name to its own localStorage namespace, so view and sort live in
    // separate buckets that admins can read in DevTools without confusion.
    static PARAMS = [
        { name: 'view', storagePrefix: 'admin-view-prefered' },
        { name: 'sort', storagePrefix: 'admin-sort-prefered' },
    ];

    initialize() {
        this._restoreState();

        getComponent(this.element).then(component => {
            this.component = component;
            this.component.on('render:finished', () => {
                this._restoreState();
                this._updateVisibility();
                // Live actions (toggle layout, change sort) reflect into the
                // URL via UrlMapping; mirror the resulting state into
                // localStorage. removeIfAbsent=true so toggling BACK to the
                // server-side default (which clears ?view= or ?sort= from
                // the URL) also clears the stored preference.
                if (this.viewKeyValue && this.viewPathPrefixValue) {
                    this._captureUrlParams(true);
                }
            });
        }).catch(() => {
            // Not a live component
        });
    }

    connect() {
        this._updateVisibility(this.expandedValue);
        this.collapseForCache = this.collapseForCache.bind(this);
        window.addEventListener('turbo:before-cache', this.collapseForCache);

        if (this.viewKeyValue && this.viewPathPrefixValue) {
            this._registerRoute();
            // Cold load capture: write present params only. We don't wipe on
            // absence here because a fresh visit (e.g. typed into the address
            // bar before our rewriter could inject anything) would otherwise
            // erase a perfectly good preference.
            this._captureUrlParams(false);
            // Path slugs (selected editions on /admin/angebote/<slug>, roles on
            // /admin/accounts/<slug>, …) — additive capture only. Destructive
            // intent ("show all") is signalled by the EditionSwitcher /
            // AccountTypeSwitcher itself (entity_switcher.js#_clearSavedSlug),
            // which wipes the bucket BEFORE its Turbo.visit. That way a hard
            // reload of the bare URL — or typing it into the address bar —
            // never accidentally forgets the user's last selection.
            this._capturePathSlug(false);
            this.onBeforeVisitForParams = this.onBeforeVisitForParams.bind(this);
            document.addEventListener('turbo:before-visit', this.onBeforeVisitForParams);
        }
    }

    disconnect() {
        window.removeEventListener('turbo:before-cache', this.collapseForCache);
        if (this.onBeforeVisitForParams) {
            document.removeEventListener('turbo:before-visit', this.onBeforeVisitForParams);
        }
    }

    _storageKey(param) {
        const entry = this.constructor.PARAMS.find(p => p.name === param);
        return `${entry.storagePrefix}--${this.viewKeyValue}`;
    }

    _registerRoute() {
        try {
            const routes = this._readRoutes();
            routes[this.viewKeyValue] = this.viewPathPrefixValue;
            localStorage.setItem('admin-view-prefered--routes', JSON.stringify(routes));
        } catch (_) {
            // localStorage unavailable — silently skip
        }
    }

    _readRoutes() {
        try {
            const raw = localStorage.getItem('admin-view-prefered--routes');
            const parsed = raw ? JSON.parse(raw) : null;
            return (parsed && typeof parsed === 'object') ? parsed : {};
        } catch (_) {
            return {};
        }
    }

    /** localStorage bucket for the filter-sidebar selections (a JSON object
     *  like {"host":"foo","past":"1"} reflecting the current ?filter[*]=*
     *  URL params). One bucket per list. */
    get _filterStorageKey() {
        return `admin-filter-prefered--${this.viewKeyValue}`;
    }

    /** localStorage bucket for the path-tail slug — the bit after the route
     *  prefix that selects entities (e.g. "herbstferienpass-2026+sommer-2026"
     *  on `/admin/angebote/<slug>`). Captured once on page load; re-injected
     *  on Turbo navigation when the destination is the bare prefix. One
     *  bucket per list. */
    get _slugStorageKey() {
        return `admin-path-prefered--${this.viewKeyValue}`;
    }

    /** Mirror the current path's slug-tail (the segment AFTER viewPathPrefix)
     *  into localStorage. Authoritative on every load: an empty tail wipes
     *  the stored preference because the EditionSwitcher always writes its
     *  selection back to the URL, so "no slug" means the user picked "all".
     */
    _capturePathSlug(removeIfAbsent) {
        try {
            const prefix = this.viewPathPrefixValue;
            if (!prefix) return;
            const path = window.location.pathname;
            if (!path.startsWith(prefix)) return;

            // Everything between the prefix and the next `/` (or EOL) is the
            // single `{edition?}`/`{role?}` slug — already `+`-joined by the
            // controllers, so we store it verbatim. Nested segments (deep
            // links to edit views etc.) are not list-page state and never
            // get captured.
            let tail = path.slice(prefix.length);
            if (tail.startsWith('/')) tail = tail.slice(1);
            const slashIdx = tail.indexOf('/');
            if (slashIdx !== -1) return; // not on the list view itself
            // Strip trailing whitespace; an empty tail after this point is
            // the legitimate "no slug" state.

            const key = this._slugStorageKey;
            if (tail) {
                localStorage.setItem(key, tail);
            } else if (removeIfAbsent) {
                localStorage.removeItem(key);
            }
        } catch (_) {}
    }

    /** Mirror current URL params for THIS list into localStorage.
     *  @param removeIfAbsent when true, params missing from the URL clear
     *      their stored value (used after a live action so toggle-to-default
     *      properly forgets the preference). When false, missing params are
     *      left alone (used on cold page load). */
    _captureUrlParams(removeIfAbsent) {
        try {
            const params = new URLSearchParams(window.location.search);

            // Flat single-value params (view, sort).
            for (const { name } of this.constructor.PARAMS) {
                const value = params.get(name);
                const key = this._storageKey(name);
                if (value !== null && value !== '') {
                    localStorage.setItem(key, value);
                } else if (removeIfAbsent) {
                    localStorage.removeItem(key);
                }
            }

            // Filter sidebar — multi-key, shape `filter[<name>]=<value>`.
            // Captured as a JSON object so the storage stays per-list and
            // we can replay all keys on a future navigation in one go.
            const filterRe = /^filter\[(.+)]$/;
            const filterData = {};
            let hasFilter = false;
            for (const [k, v] of params.entries()) {
                const m = filterRe.exec(k);
                if (!m) continue;
                if (v === '') continue;
                filterData[m[1]] = v;
                hasFilter = true;
            }
            if (hasFilter) {
                localStorage.setItem(this._filterStorageKey, JSON.stringify(filterData));
            } else if (removeIfAbsent) {
                localStorage.removeItem(this._filterStorageKey);
            }
        } catch (_) {}
    }

    /** Inject any stored params into the destination URL when it lands in a
     *  known list's prefix and doesn't already pin those params. */
    onBeforeVisitForParams(event) {
        let target;
        try {
            target = new URL(event.detail.url, window.location.href);
        } catch (_) {
            return;
        }

        // Longest-prefix wins so a nested list (if any) takes precedence.
        let matchedKey = null;
        let matchedPrefix = null;
        let matchedLen = 0;
        for (const [viewKey, prefix] of Object.entries(this._readRoutes())) {
            if (typeof prefix !== 'string') continue;
            if (target.pathname.startsWith(prefix) && prefix.length > matchedLen) {
                matchedKey = viewKey;
                matchedPrefix = prefix;
                matchedLen = prefix.length;
            }
        }
        if (!matchedKey) return;

        let rewritten = false;

        // Path-slug injection — only when the destination is the BARE list
        // prefix (e.g. a navbar link to /admin/angebote). Any path tail —
        // even an edition slug from the EditionSwitcher — is the user's
        // explicit choice and wins outright. We compare with trailing
        // slashes normalized away on both sides.
        const targetPath = target.pathname.replace(/\/+$/, '');
        const bare = matchedPrefix.replace(/\/+$/, '');
        if (targetPath === bare) {
            let storedSlug;
            try {
                storedSlug = localStorage.getItem(`admin-path-prefered--${matchedKey}`);
            } catch (_) {
                storedSlug = null;
            }
            if (storedSlug) {
                target.pathname = `${bare}/${storedSlug}`;
                rewritten = true;
            }
        }

        for (const { name, storagePrefix } of this.constructor.PARAMS) {
            if (target.searchParams.has(name)) continue; // explicit deep-link wins
            let stored;
            try {
                stored = localStorage.getItem(`${storagePrefix}--${matchedKey}`);
            } catch (_) {
                continue;
            }
            if (stored === null || stored === '') continue;
            target.searchParams.set(name, stored);
            rewritten = true;
        }

        // Filter sidebar (multi-key). Only inject when the target has NO
        // filter[*] params already — partial overrides would confuse the
        // sidebar (mixing user's deep-link filters with stored ones).
        const hasAnyFilter = [...target.searchParams.keys()].some(k => k.startsWith('filter['));
        if (!hasAnyFilter) {
            let storedFilters = null;
            try {
                const raw = localStorage.getItem(`admin-filter-prefered--${matchedKey}`);
                storedFilters = raw ? JSON.parse(raw) : null;
            } catch (_) {}
            if (storedFilters && typeof storedFilters === 'object') {
                for (const [k, v] of Object.entries(storedFilters)) {
                    if (v === null || v === '') continue;
                    target.searchParams.set(`filter[${k}]`, String(v));
                    rewritten = true;
                }
            }
        }

        if (!rewritten) return;
        const newUrl = target.toString();
        if (newUrl === event.detail.url) return;

        event.preventDefault();
        setTimeout(() => window.Turbo.visit(newUrl), 0);
    }

    collapseForCache() {
        this.expandedValue = false;
        this._updateVisibility(false);
        this._saveExpandedState();
    }

    toggle() {
        this.expandedValue = !this.expandedValue;
        this._updateVisibility(true);
        this._saveExpandedState();
    }

    expand() {
        this.expandedValue = true;
        this._updateVisibility(true);
        this._saveExpandedState();
    }

    collapse() {
        this.expandedValue = false;
        this._updateVisibility(true);
        this._saveExpandedState();
    }

    _updateVisibility(animate = false) {
        if (this.hasSidebarTarget) {
            if (this.expandedValue) {
                this.sidebarTarget.classList.remove('hidden');
                if (animate) {
                    enter(this.sidebarTarget);
                }
            } else {
                if (animate) {
                    leave(this.sidebarTarget).then(() => {
                        this.sidebarTarget.classList.add('hidden');
                    });
                } else {
                    this.sidebarTarget.classList.add('hidden');
                }
            }
        }
        
        // Reflect sidebar state on the toggle button via aria-pressed
        // instead of hiding it — the user keeps a single, always-visible
        // control. Tailwind's `aria-pressed:*` classes on the button drive
        // the active / hover-on-active styling from this attribute.
        if (this.hasButtonTarget) {
            this.buttonTarget.setAttribute('aria-pressed', this.expandedValue ? 'true' : 'false');
        }

        // Active-filter pills are the inverse of the sidebar: they're a
        // glanceable summary of the filter form, only useful while the form
        // itself is hidden. When the sidebar is expanded the user can read
        // the form values directly, so showing both would be redundant
        // (and would compete for horizontal space in the toolbar).
        if (this.hasPillsTarget) {
            this.pillsTarget.classList.toggle('hidden', this.expandedValue);
        }
    }

    _restoreState() {
        const storedExpanded = localStorage.getItem(this._expandedStorageKey);
        if (storedExpanded !== null) {
            this.expandedValue = storedExpanded === 'true';
        }
    }

    _saveExpandedState() {
        localStorage.setItem(this._expandedStorageKey, this.expandedValue);
    }

    get _expandedStorageKey() {
        return this.keyValue ? `filter-sidebar-expanded-${this.keyValue}` : 'filter-sidebar-expanded';
    }

    showContextMenu(event) {
        const row = event.currentTarget;
        const menu = row.querySelector('el-menu');
        if (!menu) return;

        event.preventDefault();

        const oldAnchor = menu.getAttribute('anchor');
        menu.removeAttribute('anchor');

        menu.style.position = 'fixed';
        menu.style.left = `${event.clientX}px`;
        menu.style.top = `${event.clientY}px`;
        menu.style.margin = '0';

        if (menu.showPopover) {
            menu.showPopover();
        } else {
            menu.classList.remove('hidden');
        }

        const onToggle = (e) => {
            if (e.newState === 'closed') {
                if (oldAnchor) {
                    menu.setAttribute('anchor', oldAnchor);
                }
                menu.removeEventListener('toggle', onToggle);
            }
        };
        menu.addEventListener('toggle', onToggle);
    }
}
