var GameKit = (function () {
    'use strict';

    var dialog, fanContent, prevBtn, nextBtn, fanWrap, flipBtn;
    var stageBlock, spinBtn, fyiBtn, fyiPanel;
    var fyiTitle, fyiType, fyiEffect, fyiIndex, fyiPrev, fyiNext;
    var statUpright, statReversed;
    var _polarity = 'levity';   // FLIP toggles 'levity' ↔ 'gravity'

    var currentDeckId = null;
    var currentIndex  = 0;
    var cards         = [];
    var _revealTimer  = null;
    var REVEAL_DELAY_MS = 500;

    var _infoData = [];
    var _infoIdx  = 0;
    var _infoOpen = false;
    var _spinOrigLabel = 'SPIN';
    var _fyiOrigLabel  = 'FYI';

    // ── Storage ────────────────────────────────────────────────────────────────

    function storageKey(deckId) { return 'tarot-fan-' + deckId; }
    function savePosition() {
        if (currentDeckId !== null) {
            sessionStorage.setItem(storageKey(currentDeckId), currentIndex);
        }
    }
    function restorePosition(deckId) {
        var saved = sessionStorage.getItem(storageKey(deckId));
        return saved !== null ? parseInt(saved, 10) : 0;
    }

    // ── Carousel transforms ───────────────────────────────────────────────────

    function _carouselStep() {
        // Pull the per-breakpoint step length from CSS so mobile @media rules
        // can shrink the carousel without code changes.
        if (!fanWrap) return 200;
        var raw = getComputedStyle(fanWrap).getPropertyValue('--fan-carousel-step').trim();
        var n = parseFloat(raw);
        return isFinite(n) && n > 0 ? n : 200;
    }

    function cardTransform(offset) {
        var abs  = Math.abs(offset);
        var step = _carouselStep();
        return {
            transform: 'translateX(' + (offset * step) + 'px) rotateY(' + (offset * 22) + 'deg) scale(' + Math.max(0.3, 1 - abs * 0.15) + ')',
            opacity: Math.max(0.15, 1 - abs * 0.25),
            zIndex: 10 - abs,
        };
    }

    function updateFan() {
        var total = cards.length;
        if (!total) return;
        cards.forEach(function (card, i) {
            var offset = i - currentIndex;
            if (offset > total / 2) offset -= total;
            if (offset < -total / 2) offset += total;
            var abs = Math.abs(offset);
            card.classList.toggle('fan-card--active', offset === 0);
            if (abs > 3) {
                card.style.display = 'none';
            } else {
                card.style.display = '';
                var t = cardTransform(offset);
                // Active card may also be reversed — append rotate(180deg) to
                // the carousel transform without disturbing the layout values.
                var spin = (offset === 0 && card.classList.contains('stage-card--reversed'))
                    ? ' rotate(180deg)' : '';
                card.style.transform = t.transform + spin;
                card.style.opacity   = t.opacity;
                card.style.zIndex    = t.zIndex;
            }
        });
        _populateStage();
        _scheduleReveal();
    }

    // ── Stage block: idle reveal / careen-out ─────────────────────────────────

    function _scheduleReveal() {
        if (!stageBlock) return;
        stageBlock.classList.remove('is-revealed');
        if (_revealTimer) clearTimeout(_revealTimer);
        _revealTimer = setTimeout(function () {
            stageBlock.classList.add('is-revealed');
        }, REVEAL_DELAY_MS);
    }

    function _hideStageImmediate() {
        if (!stageBlock) return;
        if (_revealTimer) { clearTimeout(_revealTimer); _revealTimer = null; }
        stageBlock.classList.remove('is-revealed');
        _closeFyi();
    }

    // ── Stage population (delegates to shared StageCard module) ──────────────

    function _populateStage() {
        if (!stageBlock || !cards.length) return;
        var cardEl = cards[currentIndex];
        if (!cardEl) return;

        var card = StageCard.fromDataset(cardEl);
        // Repaint the focused fan-card's faces in the active polarity (so FLIP
        // and per-polarity qualifier rendering stay consistent with the data).
        StageCard.populateCard(cardEl, card, _polarity);
        cardEl.dataset.polarity = _polarity;
        // Mirror polarity on `.tarot-fan-wrap` so the fan-stage-block can
        // invert its bg per the sig convention (user-spec 2026-05-23: stat
        // block always carries the opposite-polarity color of its adjacent
        // card). See `_card-deck.scss:.tarot-fan-wrap[data-polarity]`.
        if (fanWrap) fanWrap.dataset.polarity = _polarity;

        StageCard.populateKeywords(stageBlock, card.keywords_upright, card.keywords_reversed, {
            uprightSel:  '#id_fan_stat_upright',
            reversedSel: '#id_fan_stat_reversed',
        });
        // Sprint A.7.5 — fill the title + arcana + top-left rank+suit chip
        // fields per the new image-mode stat-block shape. The carousel stage
        // block previously had only the keyword list; for image-mode decks
        // the stat block is the only home for textual metadata.
        StageCard.populateStatExtras(stageBlock, card);
        _infoData = StageCard.buildInfoData(card);
        _infoIdx  = 0;

        // Reset SPIN state on every card change — clears rotation on all cards
        // so a previously-reversed card returns to upright when it leaves focus.
        stageBlock.classList.remove('is-reversed');
        cards.forEach(function (c) { c.classList.remove('stage-card--reversed'); });
        // Sprint A.7.5 — also clear FLIP-to-back state on card change so a
        // back-flipped non-polarized card returns to its front when it leaves
        // focus (mirrors the SPIN reset semantics).
        cards.forEach(function (c) { c.classList.remove('is-flipped-to-back'); });
        _closeFyi();
    }

    // ── FLIP — toggle polarity with perspective horizontal flip animation ────
    // Retains SPIN state across the flip (the .stage-card--reversed class on
    // the card is left untouched).

    function _flipActive() {
        var active = cards[currentIndex];
        if (!active) return;
        if (active.dataset.flipping) return;  // mid-flip

        // Sprint A.7.5 — non-polarized image-equipped decks (Minchiate today)
        // render a `.sig-stage-card-back-img` child server-side per the my_sign
        // pattern. Presence of that child = FLIP toggles back-image instead of
        // polarity cycle (which has no meaning for non-polarized decks).
        if (active.querySelector('.sig-stage-card-back-img')) {
            _flipToBack(active);
            return;
        }

        active.dataset.flipping = '1';

        // Build the resting transform (carousel offset 0 + optional SPIN rotate(180))
        // and the edge-on midpoint by adding a rotateY layer at the end. Keeping
        // the carousel + spin intact means the card actually rotates in place
        // around its centre rather than just the inner face.
        var spin = active.classList.contains('stage-card--reversed') ? ' rotate(180deg)' : '';
        var rest = 'translateX(0px) rotateY(0deg) scale(1)' + spin;
        var mid  = 'translateX(0px) rotateY(0deg) scale(1)' + spin + ' rotateY(90deg)';

        active.animate([
            { transform: rest },
            { transform: mid,  offset: 0.5 },
            { transform: rest },
        ], { duration: 500, easing: 'ease' });

        _polarity = (_polarity === 'levity') ? 'gravity' : 'levity';
        // Swap polarity content + colour at the edge-on midpoint, so the user
        // sees the new face from the start of the second half-rotation.
        setTimeout(function () {
            var card = StageCard.fromDataset(active);
            StageCard.populateCard(active, card, _polarity);
            active.dataset.polarity = _polarity;
            // Mirror onto the wrap so the stage block re-invertds in lockstep.
            if (fanWrap) fanWrap.dataset.polarity = _polarity;
        }, 250);
        // Clear the in-flight flag at animation end. Using setTimeout (not
        // anim.onfinish) so jasmine.clock().tick() can fake-advance it in tests.
        setTimeout(function () { delete active.dataset.flipping; }, 500);
    }

    // Sprint A.7.5 — FLIP-to-back animation for non-polarized image-equipped
    // decks. Mirrors my_sign.html / _applet-my-sign.html FLIP-to-back: rotateY
    // 0→90→0 over 500ms, toggle `.is-flipped-to-back` at the halfway point,
    // clear `data-flipping` at end. SCSS hides/shows the front + back img via
    // the same `.is-flipped-to-back` class used by the my_sign surface.
    function _flipToBack(active) {
        active.dataset.flipping = '1';
        var spin = active.classList.contains('stage-card--reversed') ? ' rotate(180deg)' : '';
        var rest = 'translateX(0px) rotateY(0deg) scale(1)' + spin;
        var mid  = 'translateX(0px) rotateY(0deg) scale(1)' + spin + ' rotateY(90deg)';
        active.animate([
            { transform: rest },
            { transform: mid,  offset: 0.5 },
            { transform: rest },
        ], { duration: 500, easing: 'ease' });
        setTimeout(function () { active.classList.toggle('is-flipped-to-back'); }, 250);
        setTimeout(function () { delete active.dataset.flipping; }, 500);
    }

    // ── FYI panel ─────────────────────────────────────────────────────────────

    function _renderFyi() {
        StageCard.renderFyi(fyiPanel, _infoData, _infoIdx);
    }

    function _openFyi() {
        _infoOpen = true;
        _renderFyi();
        if (fyiPanel) fyiPanel.style.display = 'flex';
        if (spinBtn) { spinBtn.classList.add('btn-disabled'); spinBtn.textContent = '×'; }
        if (fyiBtn)  { fyiBtn.classList.add('btn-disabled');  fyiBtn.textContent  = '×'; }
        if (stageBlock) stageBlock.classList.add('fyi-open');
    }

    function _closeFyi() {
        _infoOpen = false;
        if (fyiPanel) fyiPanel.style.display = 'none';
        if (spinBtn) { spinBtn.classList.remove('btn-disabled'); spinBtn.textContent = _spinOrigLabel; }
        if (fyiBtn)  { fyiBtn.classList.remove('btn-disabled');  fyiBtn.textContent  = _fyiOrigLabel; }
        if (stageBlock) stageBlock.classList.remove('fyi-open');
    }

    // ── Open / close the dialog ───────────────────────────────────────────────

    function openFan(deckId) {
        currentDeckId = deckId;
        currentIndex = restorePosition(deckId);
        // Always open on the levity-painted face — the FLIP cue. The bookmark
        // (sessionStorage) intentionally tracks card index only, not polarity,
        // so the styling matches the SSR gravity default until JS repaints.
        _polarity = 'levity';

        fetch('/gameboard/game-kit/deck/' + deckId + '/')
            .then(function (r) { return r.text(); })
            .then(function (html) {
                fanContent.innerHTML = html;
                cards = Array.from(fanContent.querySelectorAll('.fan-card'));
                if (currentIndex >= cards.length) currentIndex = 0;
                cards.forEach(function (c) {
                    c.style.transition = 'transform 0.18s ease-out, opacity 0.18s ease-out';
                });
                updateFan();
                if (dialog && typeof dialog.showModal === 'function') dialog.showModal();
            });
    }

    function closeFan() {
        savePosition();
        _hideStageImmediate();
        if (dialog && typeof dialog.close === 'function') dialog.close();
    }

    // ── Navigation ────────────────────────────────────────────────────────────

    function navigate(delta) {
        if (!cards.length) return;
        currentIndex = (currentIndex + delta + cards.length) % cards.length;
        savePosition();
        _hideStageImmediate();
        updateFan();
    }

    var _navTimer = null;
    function navigateAnimated(steps) {
        if (!cards.length || steps === 0) return;
        clearTimeout(_navTimer);
        var sign = steps > 0 ? 1 : -1;
        var remaining = Math.abs(steps);
        function tick() {
            navigate(sign);
            remaining--;
            if (remaining > 0) _navTimer = setTimeout(tick, 60);
        }
        tick();
    }

    // ── Init ──────────────────────────────────────────────────────────────────

    function init() {
        dialog = document.getElementById('id_tarot_fan_dialog');
        if (!dialog) return;

        fanContent = document.getElementById('id_fan_content');
        prevBtn    = document.getElementById('id_fan_prev');
        nextBtn    = document.getElementById('id_fan_next');
        flipBtn    = document.getElementById('id_fan_flip');
        fanWrap    = dialog.querySelector('.tarot-fan-wrap');

        stageBlock = document.getElementById('id_fan_stage_block');
        if (stageBlock) {
            spinBtn      = stageBlock.querySelector('.spin-btn');
            fyiBtn       = stageBlock.querySelector('.fyi-btn');
            fyiPanel     = stageBlock.querySelector('#id_fan_fyi_panel');
            fyiTitle     = fyiPanel && fyiPanel.querySelector('.sig-info-title');
            fyiType      = fyiPanel && fyiPanel.querySelector('.sig-info-type');
            fyiEffect    = fyiPanel && fyiPanel.querySelector('.sig-info-effect');
            fyiIndex     = fyiPanel && fyiPanel.querySelector('.sig-info-index');
            fyiPrev      = stageBlock.querySelector('.fyi-prev');
            fyiNext      = stageBlock.querySelector('.fyi-next');
            statUpright  = stageBlock.querySelector('#id_fan_stat_upright');
            statReversed = stageBlock.querySelector('#id_fan_stat_reversed');
            _spinOrigLabel = spinBtn ? spinBtn.textContent : 'SPIN';
            _fyiOrigLabel  = fyiBtn  ? fyiBtn.textContent  : 'FYI';
        }

        // Backdrop click closes
        if (dialog && fanWrap) {
            dialog.addEventListener('click', function (e) {
                if (e.target === dialog || e.target === fanWrap) closeFan();
            });
        }

        // Keyboard nav
        if (dialog) {
            dialog.addEventListener('keydown', function (e) {
                if (e.key === 'ArrowRight') navigate(1);
                if (e.key === 'ArrowLeft')  navigate(-1);
            });
        }

        // Wheel nav
        var wheelAccum = 0;
        var wheelDecayTimer = null;
        var WHEEL_STEP = 150;
        if (dialog) {
            dialog.addEventListener('wheel', function (e) {
                e.preventDefault();
                clearTimeout(wheelDecayTimer);
                wheelAccum += e.deltaY;
                var steps = Math.trunc(wheelAccum / WHEEL_STEP);
                if (steps !== 0) {
                    steps = Math.sign(steps) * Math.min(Math.abs(steps), 3);
                    wheelAccum -= steps * WHEEL_STEP;
                    navigate(steps);
                }
                wheelDecayTimer = setTimeout(function () { wheelAccum = 0; }, 200);
            }, { passive: false });
        }

        // Touch nav
        var touchStartX = 0, touchStartY = 0, touchStartTime = 0;
        if (dialog) {
            dialog.addEventListener('touchstart', function (e) {
                touchStartX = e.touches[0].clientX;
                touchStartY = e.touches[0].clientY;
                touchStartTime = Date.now();
            }, { passive: true });
            dialog.addEventListener('touchend', function (e) {
                var dx = e.changedTouches[0].clientX - touchStartX;
                var dy = e.changedTouches[0].clientY - touchStartY;
                if (Math.abs(dy) > Math.abs(dx)) return;
                if (Math.abs(dx) < 60) return;
                var elapsed = Math.max(1, Date.now() - touchStartTime);
                var velocity = Math.abs(dx) / elapsed;
                var steps = velocity > 0.8
                    ? Math.max(1, Math.round(velocity * 4))
                    : Math.round(Math.abs(dx) / 150);
                navigateAnimated(dx < 0 ? steps : -steps);
            }, { passive: true });
        }

        if (prevBtn) prevBtn.addEventListener('click', function () { navigate(-1); });
        if (nextBtn) nextBtn.addEventListener('click', function () { navigate(1); });
        if (flipBtn) flipBtn.addEventListener('click', function (e) {
            // Without this the click bubbles to the dialog's backdrop-close
            // handler and shuts the modal — FLIP is a primary action, not a
            // dismiss.
            e.stopPropagation();
            _flipActive();
        });

        // SPIN — toggle stat block face AND rotate the active fan-card 180°.
        // Reapply the active card's transform directly so the rotation animates
        // off the existing carousel transform without going through updateFan
        // (which would reset .stage-card--reversed via _populateStage).
        if (spinBtn) {
            spinBtn.addEventListener('click', function () {
                if (spinBtn.classList.contains('btn-disabled')) return;
                stageBlock.classList.toggle('is-reversed');
                var active = cards[currentIndex];
                if (!active) return;
                // Sprint A.7.5-polish-4 — image-mode + text-mode share the
                // same SPIN path: toggle `.stage-card--reversed` + reset
                // inline transform. The existing `transition: transform
                // 0.18s ease-out` on `.fan-card` (set in updateFan) handles
                // the visual rotation — no Element.animate layered on top,
                // which had caused a double/triple-flip when the JS anim
                // overlay + the CSS transition both fired.
                active.classList.toggle('stage-card--reversed');
                var t = cardTransform(0);
                var spin = active.classList.contains('stage-card--reversed') ? ' rotate(180deg)' : '';
                active.style.transform = t.transform + spin;
            });
        }

        // FYI
        if (fyiBtn) {
            fyiBtn.addEventListener('click', function () {
                if (fyiBtn.classList.contains('btn-disabled')) return;
                _infoOpen ? _closeFyi() : _openFyi();
            });
        }

        if (fyiPanel) {
            fyiPanel.addEventListener('click', function (e) {
                if (!e.target.closest('.fyi-prev') && !e.target.closest('.fyi-next')) {
                    _closeFyi();
                }
            });
        }
        if (fyiPrev) {
            fyiPrev.addEventListener('click', function () {
                if (!_infoData.length) return;
                _infoIdx = (_infoIdx - 1 + _infoData.length) % _infoData.length;
                _renderFyi();
            });
        }
        if (fyiNext) {
            fyiNext.addEventListener('click', function () {
                if (!_infoData.length) return;
                _infoIdx = (_infoIdx + 1) % _infoData.length;
                _renderFyi();
            });
        }

        // Deck-card click → openFan
        document.querySelectorAll('.gk-deck-card[data-deck-id]').forEach(function (card) {
            card.addEventListener('click', function () { openFan(card.dataset.deckId); });
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    return {
        openFan:   openFan,
        closeFan:  closeFan,
        navigate:  navigate,
        reinit:    init,
        _testInit: function () {
            dialog = null; fanContent = null; prevBtn = null; nextBtn = null; fanWrap = null;
            flipBtn = null;
            stageBlock = null; spinBtn = null; fyiBtn = null; fyiPanel = null;
            fyiTitle = null; fyiType = null; fyiEffect = null; fyiIndex = null;
            fyiPrev = null; fyiNext = null; statUpright = null; statReversed = null;
            currentDeckId = null; currentIndex = 0; cards = [];
            _revealTimer = null; _infoData = []; _infoIdx = 0; _infoOpen = false;
            _polarity = 'levity';
            init();
        },
        // Test seam: skip fetch by reading already-rendered #id_fan_content.
        // Mirrors openFan's polarity reset so reopen-after-FLIP is exercisable.
        _testOpen: function () {
            cards = Array.from(fanContent.querySelectorAll('.fan-card'));
            currentIndex = 0;
            _polarity = 'levity';
            updateFan();
        },
        _testNavigate: navigate,
    };
}());

// ── Pronouns applet — guard-portal-confirmed preference flip ─────────────
(function () {
    'use strict';

    function csrfFromCookie() {
        var name = 'csrftoken';
        if (!document.cookie) return '';
        var found = '';
        document.cookie.split(';').forEach(function (c) {
            c = c.trim();
            if (c.indexOf(name + '=') === 0) {
                found = decodeURIComponent(c.substring(name.length + 1));
            }
        });
        return found;
    }

    function commit(card) {
        var key = card.getAttribute('data-pronoun');
        var body = new URLSearchParams({ pronouns: key });
        fetch('/dashboard/set-pronouns', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'X-CSRFToken': csrfFromCookie(),
            },
            credentials: 'same-origin',
            body: body.toString(),
        }).then(function (resp) {
            if (resp.status === 200) {
                // First-time `bawlmorese` selection — server returns the
                // Baltimorean Brief payload to render inline (no reload, or the
                // banner would be lost the moment the page navigates).
                return resp.json().then(function (data) {
                    if (data && data.brief && typeof Brief !== 'undefined') {
                        Brief.showBanner(data.brief);
                    }
                });
            }
            if (resp.status === 204) {
                // Reload so any provenance prose currently on the page renders
                // with the new pronouns and the .active class moves to the new card.
                window.location.reload();
            }
        });
    }

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

    function bindCards() {
        var cards = document.querySelectorAll('.gk-pronoun-card');
        if (!cards.length) return;
        cards.forEach(function (card) {
            card.addEventListener('click', function (e) {
                e.stopPropagation();
                if (typeof window.showGuard !== 'function') return;
                var trio = card.getAttribute('data-trio') || '';
                var msg = 'Set pronoun preference?'
                    + '<span class="guard-pronoun-trio">' + escapeHtml(trio) + '</span>';
                window.showGuard(card, msg, function () {
                    commit(card);
                });
            });
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', bindCards);
    } else {
        bindCards();
    }
}());
