/**
 * TitanCart Cart — Frontend JavaScript
 *
 * Global cart functionality loaded on EVERY storefront page:
 *   - Cart drawer (slide-out panel from the right)
 *   - Add-to-cart buttons (product cards + single product page)
 *   - Header cart count badge
 *   - Cart page interactions (qty spinners, remove, coupon, notes)
 *
 * Config injected via wp_localize_script('titancart-cart', 'titancartConfig', {...})
 *
 * @package TitanCart
 */

( function () {
    'use strict';

    // ── Config (from PHP wp_localize_script) ─────────────────────────────────
    const CFG          = window.titancartConfig || {};
    const API_BASE     = CFG.apiUrl      || '/wp-json/titancart/v1';
    const API          = API_BASE + '/cart';
    const NONCE        = CFG.nonce       || '';
    const SYMBOL       = CFG.symbol      || '$';
    const SHOP_URL     = CFG.shopUrl     || '/shop/';
    const CART_URL     = CFG.cartUrl     || '/cart/';
    const CHECKOUT_URL = CFG.checkoutUrl || '/checkout/';
    const CHECKOUT_ONLY_CART = !!CFG.checkoutOnlyFromCart;

    // ── Helpers ──────────────────────────────────────────────────────────────

    function debounce( fn, wait ) {
        let timer;
        return function ( ...args ) {
            clearTimeout( timer );
            timer = setTimeout( () => fn.apply( this, args ), wait );
        };
    }

    async function apiRequest( url, options = {} ) {
        const defaults = {
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce':   NONCE,
            },
        };
        const init = { ...defaults, ...options };

        // KI-119 (S476): WordPress Playground — the WP.org reviewer's environment —
        // drops the BODY of every non-POST request. From the shipped 0.2.20 ZIP the
        // qty spinner's PUT answered 400 "Missing parameter(s): quantity" and the
        // notes PATCH stored "" (silently clearing the note). WP_REST_Server maps
        // X-HTTP-Method-Override back onto the request before routing, so a POST
        // carrying the header is indistinguishable server-side — the same shim the
        // admin SPA has carried since 0.2.19 (KI-114), which never reached this
        // helper because the storefront owns its own fetch wrapper.
        const method = String( init.method || 'GET' ).toUpperCase();
        if ( method === 'PUT' || method === 'PATCH' || method === 'DELETE' ) {
            init.headers = { ...( init.headers || {} ), 'X-HTTP-Method-Override': method };
            init.method  = 'POST';
        }

        const response = await fetch( url, init );
        const data     = await response.json();
        if ( ! response.ok ) throw new Error( data?.message || 'Error' );
        return data;
    }

    function fmtPrice( val ) {
        return SYMBOL + parseFloat( val || 0 ).toFixed( 2 );
    }

    function setLoading( btn, loading ) {
        if ( ! btn ) return;
        btn.disabled = loading;
        btn.dataset.originalText = btn.dataset.originalText || btn.textContent;
        btn.textContent = loading ? 'Updating…' : btn.dataset.originalText;
    }

    function showMessage( el, msg, isError ) {
        if ( ! el ) return;
        el.textContent   = msg;
        el.className     = 'tc-cart__msg ' + ( isError ? 'tc-cart__msg--error' : 'tc-cart__msg--success' );
        el.style.display = 'block';
        setTimeout( () => { el.style.display = 'none'; }, 4000 );
    }

    // ═════════════════════════════════════════════════════════════════════════
    // CART DRAWER — Offcanvas panel (right side)
    // ═════════════════════════════════════════════════════════════════════════

    let drawerEl    = null;
    let overlayEl   = null;
    let drawerBody  = null;
    let drawerCount = null;
    let drawerSub   = null;
    let isOpen      = false;

    /** Inject the drawer HTML into the page once. */
    function initDrawer() {
        if ( drawerEl ) return;

        // Overlay
        overlayEl = document.createElement( 'div' );
        overlayEl.className = 'tc-drawer-overlay';
        overlayEl.addEventListener( 'click', closeDrawer );

        // Drawer
        drawerEl = document.createElement( 'div' );
        drawerEl.className = 'tc-drawer';
        drawerEl.innerHTML = `
            <div class="tc-drawer__header">
                <h3 class="tc-drawer__title">
                    Shopping Cart
                    <span class="tc-drawer__count" id="tc-drawer-count"></span>
                </h3>
                <button class="tc-drawer__close" aria-label="Close cart">
                    <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
                        <path d="M5 5l10 10M15 5L5 15"/>
                    </svg>
                </button>
            </div>
            <div class="tc-drawer__body" id="tc-drawer-body">
                <div class="tc-drawer__empty">
                    <svg width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M6 6h4.5l2.7 18a3 3 0 003 2.6h14.6a3 3 0 003-2.2L37 14H12"/>
                        <circle cx="17" cy="38" r="2.5"/><circle cx="33" cy="38" r="2.5"/>
                    </svg>
                    <p>Your cart is empty</p>
                    <a href="${SHOP_URL}" class="tc-drawer__shop-link">Continue Shopping</a>
                </div>
            </div>
            <div class="tc-drawer__footer" id="tc-drawer-footer" style="display:none">
                <div class="tc-drawer__subtotal">
                    <span>Subtotal</span>
                    <strong id="tc-drawer-subtotal">${SYMBOL}0.00</strong>
                </div>
                <div class="tc-drawer__actions">
                    <a href="${CART_URL}" class="tc-drawer__btn tc-drawer__btn--outline">View Cart</a>
                    ${CHECKOUT_ONLY_CART ? '' : '<a href="' + CHECKOUT_URL + '" class="tc-drawer__btn tc-drawer__btn--primary">Checkout</a>'}
                </div>
            </div>
        `;

        document.body.appendChild( overlayEl );
        document.body.appendChild( drawerEl );

        // Wire close button
        drawerEl.querySelector( '.tc-drawer__close' ).addEventListener( 'click', closeDrawer );

        // Wire remove buttons (delegated)
        drawerEl.addEventListener( 'click', async function ( e ) {
            const removeBtn = e.target.closest( '.tc-drawer-item__remove' );
            if ( ! removeBtn ) return;
            const itemId = removeBtn.dataset.itemId;
            removeBtn.disabled = true;
            try {
                const result = await apiRequest( `${API}/items/${itemId}`, { method: 'DELETE' } );
                renderDrawerItems( result.cart || result );
                updateAllCounts( result.cart?.item_count ?? result.item_count ?? 0 );
                syncCartPage( result.cart || result );
            } catch ( err ) {
                removeBtn.disabled = false;
            }
        } );

        // Cache elements
        drawerBody  = drawerEl.querySelector( '#tc-drawer-body' );
        drawerCount = drawerEl.querySelector( '#tc-drawer-count' );
        drawerSub   = drawerEl.querySelector( '#tc-drawer-subtotal' );
    }

    function openDrawer() {
        initDrawer();
        isOpen = true;
        overlayEl.classList.add( 'tc-drawer-overlay--active' );
        drawerEl.classList.add( 'tc-drawer--open' );
        document.body.style.overflow = 'hidden';
    }

    function closeDrawer() {
        if ( ! isOpen ) return;
        isOpen = false;
        overlayEl.classList.remove( 'tc-drawer-overlay--active' );
        drawerEl.classList.remove( 'tc-drawer--open' );
        document.body.style.overflow = '';
    }

    /** Render the cart items inside the drawer. */
    function renderDrawerItems( cart ) {
        if ( ! drawerBody ) return;
        const items  = cart?.items || [];
        const totals = cart?.totals || {};
        const footer = document.getElementById( 'tc-drawer-footer' );

        if ( items.length === 0 ) {
            drawerBody.innerHTML = `
                <div class="tc-drawer__empty">
                    <svg width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M6 6h4.5l2.7 18a3 3 0 003 2.6h14.6a3 3 0 003-2.2L37 14H12"/>
                        <circle cx="17" cy="38" r="2.5"/><circle cx="33" cy="38" r="2.5"/>
                    </svg>
                    <p>Your cart is empty</p>
                    <a href="${SHOP_URL}" class="tc-drawer__shop-link">Continue Shopping</a>
                </div>`;
            if ( footer ) footer.style.display = 'none';
            if ( drawerCount ) drawerCount.textContent = '';
            return;
        }

        drawerBody.innerHTML = items.map( function ( item ) {
            const img = item.image?.thumb || item.image?.url || '';
            const price = fmtPrice( item.effective_price || item.unit_price );
            const optionsHtml = ( item.options || [] ).map( function ( o ) {
                return '<span class="tc-drawer-item__opt">' +
                    ( o.option_name || '' ) + ': ' + ( o.variant_name || '' ) +
                    '</span>';
            } ).join( '' );

            return `
                <div class="tc-drawer-item">
                    <div class="tc-drawer-item__img-wrap">
                        ${img ? '<img src="' + img + '" alt="' + ( item.name || '' ) + '" class="tc-drawer-item__img" />' : '<div class="tc-drawer-item__img-placeholder"></div>'}
                    </div>
                    <div class="tc-drawer-item__info">
                        <p class="tc-drawer-item__name">${item.name || 'Product'}</p>
                        ${optionsHtml ? '<div class="tc-drawer-item__options">' + optionsHtml + '</div>' : ''}
                        <div class="tc-drawer-item__meta">
                            <span class="tc-drawer-item__price">${price}</span>
                            <span class="tc-drawer-item__qty">× ${item.quantity}</span>
                        </div>
                    </div>
                    <button class="tc-drawer-item__remove" data-item-id="${item.item_id}" aria-label="Remove">
                        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
                            <path d="M2 4h12M5.3 4V2.7a.7.7 0 01.7-.7h4a.7.7 0 01.7.7V4M6.7 7.3v4M9.3 7.3v4M3.3 4l.7 9.3a1.3 1.3 0 001.3 1.4h5.4a1.3 1.3 0 001.3-1.4L12.7 4"/>
                        </svg>
                    </button>
                </div>`;
        } ).join( '' );

        if ( footer ) footer.style.display = '';
        if ( drawerCount ) drawerCount.textContent = '(' + ( cart.item_count || items.length ) + ')';
        if ( drawerSub ) drawerSub.textContent = fmtPrice( totals.subtotal || 0 );

        // S321 — broadcast cart state so extensions (e.g. the Essentials free-
        // shipping progress bar) can react to ANY cart change. Read-only signal;
        // fires on add / drawer-remove / drawer-open since all route through here.
        document.dispatchEvent( new CustomEvent( 'tc:cart-updated', {
            detail: { subtotal: parseFloat( totals.subtotal || 0 ), item_count: cart.item_count || items.length || 0 }
        } ) );
    }

    /** Fetch cart and render into drawer. */
    async function refreshDrawer() {
        try {
            const cart = await apiRequest( API );
            renderDrawerItems( cart );
            updateAllCounts( cart.item_count || 0 );
        } catch ( e ) {
            // Silently fail — drawer just shows current state.
        }
    }

    // ═════════════════════════════════════════════════════════════════════════
    // GLOBAL — Add-to-cart buttons (works on every page)
    // ═════════════════════════════════════════════════════════════════════════

    document.addEventListener( 'DOMContentLoaded', function () {

        // Init drawer HTML immediately so header cart icon can open it.
        initDrawer();

        // Fetch initial count for badge.
        apiRequest( API + '/count' ).then( function ( d ) {
            updateAllCounts( d.count || 0 );
        } ).catch( function () {} );

        // ── Delegated add-to-cart handler ────────────────────────────────────
        document.addEventListener( 'click', async function ( e ) {
            const btn = e.target.closest( '.tc-add-to-cart-btn' );
            if ( ! btn ) return;

            const productId = parseInt( btn.dataset.productId, 10 );
            if ( ! productId ) return;

            e.preventDefault();
            const origHTML = btn.innerHTML;
            btn.disabled   = true;
            btn.innerHTML  = '&#8230; Adding';

            // Collect quantity + options from product page form (or the theme's
            // quick-view modal, whose .tc-qv-qty input was never read — S478).
            const form    = btn.closest( '.tc-pdp' ) || btn.closest( '.tc-product' ) || btn.closest( '.tc-qv-modal' );
            const options = {};

            if ( form ) {
                // Each [data-option-id] wrapper is one product option (rendered by
                // templates/product/option.php). Type may be Select / Radio /
                // Checkbox / Input / Textarea / File. Collect by input shape so we
                // stay tolerant of template overrides.
                //
                // Wire shapes (S137):
                //   Select   → int variant_id
                //   Radio    → int variant_id
                //   Checkbox → array of int variant_ids (one entry per checked box)
                //   Input    → string (free text, e.g. engraving)
                //   Textarea → string (free text)
                //   File     → not yet wired; multipart upload pipeline pending
                form.querySelectorAll( '[data-option-id]' ).forEach( function ( wrapper ) {
                    const optId    = wrapper.dataset.optionId;
                    const select   = wrapper.querySelector( 'select' );
                    const radio    = wrapper.querySelector( 'input[type="radio"]:checked' );
                    const checks   = wrapper.querySelectorAll( 'input[type="checkbox"]:checked' );
                    const textIn   = wrapper.querySelector( 'input[type="text"]' );
                    const textarea = wrapper.querySelector( 'textarea' );

                    if ( select && select.value ) {
                        options[ optId ] = parseInt( select.value, 10 );
                    } else if ( radio && radio.value ) {
                        options[ optId ] = parseInt( radio.value, 10 );
                    } else if ( checks && checks.length ) {
                        const ids = Array.from( checks )
                            .map( function ( c ) { return parseInt( c.value, 10 ); } )
                            .filter( function ( v ) { return v > 0; } );
                        if ( ids.length ) options[ optId ] = ids;
                    } else if ( textIn && typeof textIn.value === 'string' && textIn.value.trim() !== '' ) {
                        options[ optId ] = textIn.value;
                    } else if ( textarea && typeof textarea.value === 'string' && textarea.value.trim() !== '' ) {
                        options[ optId ] = textarea.value;
                    }
                    // File-type options (type F) are deliberately not collected here —
                    // multipart upload + storage pipeline is a separate feature.
                } );
            }

            const qtyInput = form?.querySelector( '.tc-qty-input, .tc-pdp__qty-input, #tc-pdp-qty, .tc-qv-qty' );
            const quantity = qtyInput ? Math.max( 1, parseInt( qtyInput.value, 10 ) || 1 ) : 1;

            // Get selected variation (for grouped products).
            const variationId = form?.querySelector( '[data-selected-variation]' )?.dataset?.selectedVariation;
            const body = { product_id: productId, quantity, options };
            if ( variationId ) body.variation_id = parseInt( variationId, 10 );

            // Collect shared variation selections (e.g. Size: Medium on a single product).
            if ( form ) {
                const sharedVariants = {};
                form.querySelectorAll( '.tc-pdp__shared-variation' ).forEach( function ( group ) {
                    const fid    = group.dataset.featureId;
                    // S478: display type "Dropdown" renders a <select> instead of buttons.
                    const select = group.querySelector( '.tc-pdp__shared-select' );
                    if ( fid && select && select.value ) {
                        sharedVariants[ fid ] = parseInt( select.value, 10 );
                        return;
                    }
                    const active = group.querySelector( '.tc-pdp__shared-btn--active' );
                    if ( fid && active && active.dataset.variantId ) {
                        sharedVariants[ fid ] = parseInt( active.dataset.variantId, 10 );
                    }
                } );
                if ( Object.keys( sharedVariants ).length ) {
                    body.shared_variants = sharedVariants;
                }
            }

            // Variation group (separate-product model, KI-21): thread the loaded
            // member's combination so the server can confirm a deliberate PDP
            // selection. #tc-variations[data-current] holds the member's
            // {feature_id: variant_id} and is present on every member PDP. A card
            // / quick-view "Add to cart" has no such element, so it sends nothing
            // and is correctly rejected by the CartManager guard.
            const varWrap = form?.querySelector( '#tc-variations' );
            if ( varWrap && varWrap.dataset.current ) {
                try {
                    const combo = JSON.parse( varWrap.dataset.current );
                    if ( combo && typeof combo === 'object' && Object.keys( combo ).length ) {
                        body.variation_combo = combo;
                    }
                } catch ( e ) { /* malformed — let the server guard reject */ }
            }

            // JFY deal discount — validated server-side against Customizer config.
            const jfyDiscount = parseInt( btn.dataset.jfyDiscount, 10 );
            if ( jfyDiscount > 0 ) body.jfy_discount = jfyDiscount;

            // Subscription plan — set by subscriptions.js on the PDP plan selector.
            const subPlan = parseInt( btn.dataset.subPlan, 10 );
            if ( subPlan > 0 ) body.subscription_plan_id = subPlan;

            try {
                const result = await apiRequest( `${API}/items`, {
                    method: 'POST',
                    body:   JSON.stringify( body ),
                } );

                const cart = result.cart || result;
                updateAllCounts( cart.item_count || 0 );
                renderDrawerItems( cart );
                openDrawer();

                // KI-123 (S478): the server trimmed the quantity (stock cap / max
                // per product) — say so instead of a bare "Added".
                if ( result.adjusted && result.message ) {
                    const adjEl = form?.querySelector( '.tc-cart__msg' ) || createMsgEl();
                    showMessage( adjEl, result.message, true );
                }

                // S316 — generic "item added" event so extensions (e.g. the Essentials
                // Buy Now button) can react to a successful add: detail.button is the
                // clicked trigger, detail.cart is the hydrated cart.
                document.dispatchEvent( new CustomEvent( 'tc:cart-item-added', {
                    detail: { cart: cart, button: btn }
                } ) );

                // Brief "Added" feedback on the button.
                btn.innerHTML = '&#10003; Added';
                btn.classList.add( 'tc-btn--added' );
                setTimeout( function () {
                    btn.innerHTML = origHTML;
                    btn.classList.remove( 'tc-btn--added' );
                    btn.disabled = false;
                }, 1800 );
            } catch ( err ) {
                btn.innerHTML = origHTML;
                btn.disabled    = false;
                // Show error near the button if possible.
                const msgEl = form?.querySelector( '.tc-cart__msg' ) || createMsgEl();
                showMessage( msgEl, err.message, true );
            }
        } );

        // ── Delegated add-bundle handler (S126 — wires Bundles B12) ───────────
        // Single atomic round-trip per bundle. The server (CartManager::
        // add_bundle) validates status + date window, then loops through
        // show_on_page=1 items applying per-item discount via the existing
        // titancart_add_to_cart_pre hook. Returns the full hydrated cart.
        document.addEventListener( 'click', async function ( e ) {
            const btn = e.target.closest( '.tc-add-bundle-btn' );
            if ( ! btn ) return;

            const bundleId = parseInt( btn.dataset.bundleId, 10 );
            if ( ! bundleId ) return;

            e.preventDefault();
            const origHTML = btn.innerHTML;
            btn.disabled   = true;
            btn.innerHTML  = '&#8230; Adding';

            try {
                const result = await apiRequest( `${API}/bundles/${bundleId}`, {
                    method: 'POST',
                    body:   JSON.stringify( {} ),
                } );

                const cart = result.cart || result;
                updateAllCounts( cart.item_count || 0 );
                renderDrawerItems( cart );
                openDrawer();

                // Brief "Added" feedback on the button.
                btn.innerHTML = '&#10003; Added';
                btn.classList.add( 'tc-btn--added' );
                setTimeout( function () {
                    btn.innerHTML = origHTML;
                    btn.classList.remove( 'tc-btn--added' );
                    btn.disabled = false;
                }, 1800 );

                // If the server reported partial success, surface that
                // alongside the open drawer so the customer can see what
                // landed vs what didn't.
                if ( Array.isArray( result.errors ) && result.errors.length > 0 ) {
                    const msgEl = createMsgEl();
                    showMessage(
                        msgEl,
                        result.message || 'Bundle partially added to cart.',
                        true
                    );
                }
            } catch ( err ) {
                btn.innerHTML = origHTML;
                btn.disabled  = false;
                const msgEl   = createMsgEl();
                showMessage( msgEl, err.message, true );
            }
        } );

        // ── Header cart icon click opens drawer ──────────────────────────────
        document.addEventListener( 'click', function ( e ) {
            const trigger = e.target.closest( '.tc-cart-trigger' );
            if ( ! trigger ) return;
            e.preventDefault();
            refreshDrawer().then( openDrawer );
        } );

        // ── PDP "Copy Link" button — clipboard with execCommand fallback ─────
        //
        // Robust copy that survives:
        //   - Insecure-context blocks (navigator.clipboard requires HTTPS or
        //     localhost; falls back to execCommand which works on http://)
        //   - "NotAllowedError: Write permission denied" — the clipboard
        //     write may reject when transient-user-activation is consumed by
        //     intermediate work, or when the document policy blocks writes
        //   - Older Safari / mobile browsers without Clipboard API
        //
        // The button is rendered by templates/product/single.php with a
        // data-copy-url attribute; click swaps the icon to a check then back
        // after 1.5s to give the customer feedback that the copy succeeded.
        document.addEventListener( 'click', function ( e ) {
            const btn = e.target.closest( '.tc-pdp__copy-link-btn' );
            if ( ! btn ) return;
            e.preventDefault();
            const url = btn.getAttribute( 'data-copy-url' ) || window.location.href;

            const showSuccess = function () {
                const original = btn.innerHTML;
                btn.innerHTML = '<i class="bi bi-check2"></i>';
                setTimeout( function () { btn.innerHTML = original; }, 1500 );
            };

            // Strategy 1: Clipboard API (preferred — async, no DOM mutation).
            if ( navigator.clipboard && typeof navigator.clipboard.writeText === 'function' ) {
                navigator.clipboard.writeText( url ).then( showSuccess ).catch( function () {
                    // Falls through to strategy 2 on rejection.
                    if ( legacyCopy( url ) ) showSuccess();
                } );
                return;
            }

            // Strategy 2: textarea + execCommand fallback (synchronous; works
            // on legacy browsers and insecure contexts).
            if ( legacyCopy( url ) ) showSuccess();
        } );

        // Synchronous fallback used by the copy-link handler when the modern
        // Clipboard API is unavailable or rejects. Returns true on success.
        function legacyCopy( text ) {
            try {
                const ta = document.createElement( 'textarea' );
                ta.value = text;
                ta.setAttribute( 'readonly', '' );
                ta.style.position = 'absolute';
                ta.style.left     = '-9999px';
                document.body.appendChild( ta );
                ta.select();
                const ok = document.execCommand( 'copy' );
                document.body.removeChild( ta );
                return ok;
            } catch ( _ ) {
                return false;
            }
        }

        // ── Keyboard: Escape closes drawer ───────────────────────────────────
        document.addEventListener( 'keydown', function ( e ) {
            if ( e.key === 'Escape' && isOpen ) closeDrawer();
        } );
    } );

    // ═════════════════════════════════════════════════════════════════════════
    // CART PAGE — Qty spinners, remove, coupon, notes (only on /cart/)
    // ═════════════════════════════════════════════════════════════════════════

    document.addEventListener( 'DOMContentLoaded', function () {
        const cartEl = document.getElementById( 'tc-cart' );
        if ( ! cartEl ) return;

        // ── Quantity spinners ────────────────────────────────────────────────
        cartEl.addEventListener( 'change', debounce( async function ( e ) {
            const input = e.target.closest( '.tc-qty-spinner' );
            if ( ! input ) return;

            const itemId = input.dataset.itemId;
            const qty    = parseInt( input.value, 10 );

            try {
                const result = await apiRequest( `${API}/items/${itemId}`, {
                    method: 'PUT',
                    body:   JSON.stringify( { quantity: qty } ),
                } );
                const cart = result.cart || result;
                updateTotalsDisplay( cart.totals );
                updateLineTotals( cart.items );
                updateAllCounts( cart.item_count || 0 );
                // KI-123 (S478): trimmed by stock / max — tell the customer why the
                // spinner snapped back.
                if ( result.adjusted && result.message ) {
                    showMessage( document.getElementById( 'tc-coupon-msg' ), result.message, true );
                }
            } catch ( err ) {
                showMessage( document.getElementById( 'tc-coupon-msg' ), err.message, true );
            }
        }, 600 ) );

        // ── Remove item buttons ──────────────────────────────────────────────
        cartEl.addEventListener( 'click', async function ( e ) {
            const removeBtn = e.target.closest( '.tc-cart__remove-btn' );
            if ( ! removeBtn ) return;

            const itemId = removeBtn.dataset.itemId;
            setLoading( removeBtn, true );

            try {
                const result = await apiRequest( `${API}/items/${itemId}`, { method: 'DELETE' } );
                const cart   = result.cart || result;
                const row    = cartEl.querySelector( `[data-item-id="${itemId}"]` )?.closest( 'tr' );
                if ( row ) row.remove();

                updateTotalsDisplay( cart.totals );
                updateAllCounts( cart.item_count || 0 );

                if ( ( cart.item_count || 0 ) === 0 ) {
                    location.reload();
                }
            } catch ( err ) {
                setLoading( removeBtn, false );
                showMessage( document.getElementById( 'tc-coupon-msg' ), err.message, true );
            }
        } );

        // ── Apply coupon ─────────────────────────────────────────────────────
        const applyBtn = cartEl.querySelector( '.tc-apply-coupon-btn' );
        if ( applyBtn ) {
            applyBtn.addEventListener( 'click', async function () {
                const input = document.getElementById( 'tc-coupon-code' );
                const code  = input?.value?.trim();
                if ( ! code ) return;

                setLoading( applyBtn, true );
                const msgEl = document.getElementById( 'tc-coupon-msg' );

                try {
                    const result = await apiRequest( `${API}/coupon`, {
                        method: 'POST',
                        body:   JSON.stringify( { code } ),
                    } );
                    showMessage( msgEl, result.message || 'Coupon applied!' );
                    updateTotalsDisplay( ( result.cart || result ).totals );
                } catch ( err ) {
                    showMessage( msgEl, err.message, true );
                } finally {
                    setLoading( applyBtn, false );
                }
            } );
        }

        // ── Remove coupon ────────────────────────────────────────────────────
        const removeCouponBtn = cartEl.querySelector( '.tc-remove-coupon-btn' );
        if ( removeCouponBtn ) {
            removeCouponBtn.addEventListener( 'click', async function () {
                setLoading( removeCouponBtn, true );
                try {
                    await apiRequest( `${API}/coupon`, { method: 'DELETE' } );
                    location.reload();
                } catch ( err ) {
                    showMessage( document.getElementById( 'tc-coupon-msg' ), err.message, true );
                    setLoading( removeCouponBtn, false );
                }
            } );
        }

        // ── Notes autosave ───────────────────────────────────────────────────
        const notesEl = document.getElementById( 'tc-order-notes' );
        if ( notesEl ) {
            notesEl.addEventListener( 'input', debounce( async function () {
                await apiRequest( `${API}/notes`, {
                    method: 'PATCH',
                    body:   JSON.stringify( { notes: notesEl.value } ),
                } ).catch( () => {} );
            }, 1000 ) );
        }
    } );

    // ═════════════════════════════════════════════════════════════════════════
    // DOM UPDATE HELPERS
    // ═════════════════════════════════════════════════════════════════════════

    /** Update all cart count badges (header + drawer). */
    function updateAllCounts( count ) {
        document.querySelectorAll( '.tc-cart-count' ).forEach( function ( el ) {
            el.textContent = count || 0;
            el.style.display = ( count > 0 ) ? '' : 'none';
        } );
        if ( drawerCount ) {
            drawerCount.textContent = count > 0 ? '(' + count + ')' : '';
        }
    }

    /** Update totals sidebar on the cart page. */
    function updateTotalsDisplay( totals ) {
        if ( ! totals ) return;
        const fmt = fmtPrice;
        setText( 'tc-totals-subtotal',  fmt( totals.subtotal ) );
        setText( 'tc-totals-discount',  totals.coupon_discount > 0 ? '−' + fmt( totals.coupon_discount ) : '' );
        setText( 'tc-totals-shipping',  totals.shipping_cost > 0 ? fmt( totals.shipping_cost ) : '' );
        setText( 'tc-totals-tax',       totals.tax_total > 0 ? fmt( totals.tax_total ) : '' );
        setText( 'tc-totals-surcharge', totals.payment_surcharge > 0 ? fmt( totals.payment_surcharge ) : '' );
        setText( 'tc-totals-grand',     fmt( totals.total ) );

        // S321 — broadcast cart subtotal for extensions (Essentials free-shipping
        // bar). Covers cart-page qty change / remove / coupon apply paths.
        document.dispatchEvent( new CustomEvent( 'tc:cart-updated', {
            detail: { subtotal: parseFloat( totals.subtotal || 0 ) }
        } ) );
    }

    /** Update line totals per item (after qty change on cart page). */
    function updateLineTotals( items ) {
        if ( ! items ) return;
        items.forEach( function ( item ) {
            const el = document.querySelector( `.tc-cart__line-total[data-item-id="${item.item_id}"]` );
            if ( el ) el.textContent = fmtPrice( item.line_total );
            // KI-120 side note (S478): when the server clamps a quantity (max per
            // product, stock, marketplace single-licence), write the accepted
            // quantity back into the spinner so the input never shows a number
            // the line total does not reflect.
            const spinner = document.querySelector( `.tc-qty-spinner[data-item-id="${item.item_id}"]` );
            if ( spinner && item.quantity != null && String( spinner.value ) !== String( item.quantity ) ) {
                spinner.value = item.quantity;
            }
        } );
    }

    /** After a drawer mutation, sync the cart page if it's visible behind the drawer. */
    function syncCartPage( cart ) {
        const cartEl = document.getElementById( 'tc-cart' );
        if ( ! cartEl ) return;
        updateTotalsDisplay( cart?.totals );
        updateLineTotals( cart?.items );
        if ( ( cart?.item_count || 0 ) === 0 ) location.reload();
    }

    function setText( id, html ) {
        const el = document.getElementById( id );
        if ( el ) el.innerHTML = html;
    }

    function createMsgEl() {
        const el     = document.createElement( 'div' );
        el.className = 'tc-cart__msg';
        el.style.display = 'none';
        document.body.appendChild( el );
        return el;
    }

    // ═════════════════════════════════════════════════════════════════════════
    // NEWSLETTER SUBSCRIBE — Footer form handler
    // ═════════════════════════════════════════════════════════════════════════

    function initNewsletterForms() {
        // When titancart-theme is active, its theme.js owns the footer form
        // (different UX: inline status div + form-hide-on-success). Skip here
        // to avoid double-binding both handlers to the same form. cart.js's
        // tc-toast handler is reserved for the plugin's StorefrontModule footer
        // (rendered when a 3rd-party theme is active).
        if ( window.tctTheme ) return;

        document.querySelectorAll( '.tc-footer__nl-form' ).forEach( function ( form ) {
            form.addEventListener( 'submit', function ( e ) {
                e.preventDefault();
                var input = form.querySelector( 'input[type="email"]' );
                var btn   = form.querySelector( 'button[type="submit"]' );
                if ( ! input || ! input.value.trim() ) return;

                var email = input.value.trim();
                if ( btn ) btn.disabled = true;

                fetch( API_BASE + '/subscribe', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-WP-Nonce':   NONCE,
                    },
                    body: JSON.stringify( { email: email } ),
                } )
                .then( function ( res ) { return res.json().then( function ( data ) { return { ok: res.ok, data: data }; } ); } )
                .then( function ( result ) {
                    var msg   = result.data.message || ( result.ok ? 'Thanks for subscribing!' : 'Something went wrong.' );
                    var isErr = ! result.ok && result.data.code !== undefined;
                    nlToast( msg, isErr );
                    if ( result.ok || result.data.subscribed ) input.value = '';
                } )
                .catch( function () {
                    nlToast( 'Network error. Please try again.', true );
                } )
                .finally( function () {
                    if ( btn ) btn.disabled = false;
                } );
            } );
        } );
    }

    function nlToast( message, isError ) {
        var el = document.createElement( 'div' );
        el.className = 'tc-toast' + ( isError ? ' tc-toast--error' : '' );
        el.textContent = message;
        document.body.appendChild( el );
        setTimeout( function () { el.classList.add( 'tc-toast--visible' ); }, 10 );
        setTimeout( function () {
            el.classList.remove( 'tc-toast--visible' );
            setTimeout( function () { el.remove(); }, 300 );
        }, 3000 );
    }

    initNewsletterForms();

} )();
