/**
 * TitanCart Checkout — Frontend JavaScript
 *
 * Handles:
 *   - "Ship to different address" toggle
 *   - Real-time address field validation (on blur)
 *   - Payment method selection / panel switching
 *   - Place Order button → POST to /checkout/place-order
 *   - Error display (global banner + per-field inline errors)
 *   - Redirect to order-received page on success
 *
 * Config injected via wp_localize_script('tc-checkout', 'TitancartCheckout', {...})
 *
 * @package TitanCart
 */

( function () {
    'use strict';

    const API   = window.TitancartCheckout?.apiUrl   || '/wp-json/titancart/v1';
    const NONCE = window.TitancartCheckout?.nonce    || '';
    const I18N  = window.TitancartCheckout?.i18n     || {};
    const SYM   = window.TitancartCheckout?.currency || '$';
    const RC    = window.TitancartCheckout?.recaptcha || null;

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

    /**
     * Obtain a reCAPTCHA token for guest checkout, if the surface is protected.
     * Returns '' when unprotected so the caller omits the field.
     * v3 — execute inline; v2 — read the auto-rendered g-recaptcha-response.
     */
    async function recaptchaToken() {
        if ( ! RC || ! RC.siteKey ) return '';
        if ( RC.version === 'v3' ) {
            return new Promise( function ( resolve ) {
                function run() {
                    if ( window.grecaptcha && window.grecaptcha.execute ) {
                        window.grecaptcha.ready( function () {
                            window.grecaptcha.execute( RC.siteKey, { action: RC.action } )
                                .then( function ( t ) { resolve( t || '' ); } )
                                .catch( function () { resolve( '' ); } );
                        } );
                    } else {
                        setTimeout( run, 150 );
                    }
                }
                run();
            } );
        }
        const field = document.querySelector( '#tc-checkout [name="g-recaptcha-response"], .tc-recaptcha [name="g-recaptcha-response"]' );
        return field ? ( field.value || '' ) : '';
    }

    async function apiRequest( url, options = {} ) {
        const res  = await fetch( url, {
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce':   NONCE,
            },
            ...options,
        } );
        const data = await res.json();
        if ( ! res.ok ) {
            const err    = new Error( data?.message || I18N.error || 'Error' );
            err.errors   = data?.data?.errors || {};
            err.status   = res.status;
            throw err;
        }
        return data;
    }

    function setLoading( btn, loading ) {
        if ( ! btn ) return;
        btn.disabled         = loading;
        btn.dataset.orig     = btn.dataset.orig || btn.textContent;
        btn.textContent      = loading ? ( I18N.placing || 'Placing order…' ) : btn.dataset.orig;
        btn.classList.toggle( 'tc-btn--loading', loading );
    }

    function showBanner( msg, clear = false ) {
        const el = document.getElementById( 'tc-checkout-error' );
        if ( ! el ) return;
        if ( clear ) { el.style.display = 'none'; el.textContent = ''; return; }
        el.textContent  = msg;
        el.style.display = 'block';
        el.scrollIntoView( { behavior: 'smooth', block: 'center' } );
    }

    function clearAllErrors() {
        document.querySelectorAll( '.tc-field__error' ).forEach( el => {
            el.textContent = '';
        } );
        document.querySelectorAll( '.tc-input--error' ).forEach( el => {
            el.classList.remove( 'tc-input--error' );
        } );
        showBanner( '', true );
    }

    function showErrors( errors ) {
        Object.entries( errors ).forEach( ( [ field, msg ] ) => {
            const errorEl = document.querySelector( `.tc-field__error[data-field="${field}"]` );
            if ( errorEl ) {
                errorEl.textContent = msg;
            }
            const inputEl = document.getElementById( field );
            if ( inputEl ) {
                inputEl.classList.add( 'tc-input--error' );
                inputEl.setAttribute( 'aria-describedby', `error-${field}` );
            }
        } );

        // Scroll to the first visible error field.
        const firstError = document.querySelector( '.tc-input--error' );
        if ( firstError ) {
            firstError.scrollIntoView( { behavior: 'smooth', block: 'center' } );
            firstError.focus();
        }
    }

    // ── Collect form data ─────────────────────────────────────────────────────

    function collectFormData() {
        const checkout = document.getElementById( 'tc-checkout' );
        if ( ! checkout ) return {};

        const data = {};

        // Collect all named inputs, selects, textareas.
        checkout.querySelectorAll( '[name]' ).forEach( el => {
            const name = el.name;
            if ( ! name ) return;

            if ( el.type === 'radio' || el.type === 'checkbox' ) {
                if ( el.checked ) {
                    data[ name ] = el.type === 'checkbox' ? ( el.value || '1' ) : el.value;
                }
                // If checkbox unchecked, don't include (falsy = not set).
            } else {
                data[ name ] = el.value;
            }
        } );

        data.ship_to_different_address = document.getElementById( 'ship_to_different_address' )?.checked ? 1 : 0;

        return data;
    }

    // ── DOM wiring ────────────────────────────────────────────────────────────

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

        const checkout = document.getElementById( 'tc-checkout' );
        if ( ! checkout ) return;

        // ── Ship to different address toggle ──────────────────────────────────
        const shipToggle  = document.getElementById( 'ship_to_different_address' );
        const shipSection = document.getElementById( 'tc-step-shipping-addr' );
        const shipToBillOnly  = checkout.dataset.shipToBillOnly === '1';
        const sameAsBilling   = checkout.dataset.sameAsBilling  === '1';

        function toggleShippingSection() {
            if ( ! shipSection ) return;

            // If admin set "ship to billing only", never show shipping form.
            if ( shipToBillOnly ) {
                shipSection.style.display = 'none';
                return;
            }

            const show = shipToggle && shipToggle.checked;
            shipSection.style.display = show ? '' : 'none';

            // Require/un-require shipping fields.
            shipSection.querySelectorAll( 'input[id^="shipping_"]' ).forEach( el => {
                if ( show ) {
                    if ( [ 'shipping_first_name', 'shipping_last_name',
                           'shipping_address_1', 'shipping_city',
                           'shipping_postcode', 'shipping_country' ].includes( el.id ) ) {
                        el.setAttribute( 'required', '' );
                    }
                } else {
                    el.removeAttribute( 'required' );
                }
            } );
        }

        if ( shipToggle ) {
            shipToggle.addEventListener( 'change', toggleShippingSection );
        }
        toggleShippingSection(); // Initial state.

        // ── Fetch shipping methods when address changes ──────────────────────
        const shipMethodsList = document.getElementById( 'tc-shipping-methods-list' );
        const shipPlaceholder = document.getElementById( 'tc-shipping-placeholder' );
        const summaryShipping = document.getElementById( 'tc-summary-shipping' );
        const summaryTax      = document.getElementById( 'tc-summary-tax' );
        const summaryTotal    = document.getElementById( 'tc-summary-total' );
        let shipFetchTimer    = null;
        let selectedShipCost  = 0;

        /**
         * Determine which address to use for shipping rate lookup:
         * - If "ship to different" is checked → shipping_ fields
         * - Otherwise → billing_ fields
         */
        function getShippingAddress() {
            const useDifferent = shipToggle && shipToggle.checked;
            const prefix       = useDifferent ? 'shipping_' : 'billing_';
            return {
                country:  ( document.getElementById( prefix + 'country' )?.value  || '' ).trim(),
                state:    ( document.getElementById( prefix + 'state' )?.value    || '' ).trim(),
                postcode: ( document.getElementById( prefix + 'postcode' )?.value || '' ).trim(),
            };
        }

        async function fetchShippingMethods() {
            // S138 Phase 5: digital-only carts have no shipping section in
            // the DOM. Skip the API call entirely — server-side gate forces
            // shipping_cost = 0 regardless of any client state, but skipping
            // here saves a wasted round-trip on every address-field change.
            if ( ! shipMethodsList ) {
                return;
            }

            const addr = getShippingAddress();

            // Need at least a country before we query.
            if ( ! addr.country ) {
                if ( shipMethodsList ) {
                    shipMethodsList.innerHTML = '';
                    if ( shipPlaceholder ) {
                        shipMethodsList.appendChild( shipPlaceholder );
                        shipPlaceholder.style.display = '';
                    }
                }
                return;
            }

            // Show loading indicator.
            if ( shipMethodsList ) {
                shipMethodsList.innerHTML = '<p style="color:#71797E;font-size:13px;margin:0;">Calculating shipping…</p>';
            }

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

                const methods = result.methods || [];

                if ( ! shipMethodsList ) return;

                if ( methods.length === 0 ) {
                    shipMethodsList.innerHTML = '<p style="color:#71797E;font-size:13px;margin:0;">No shipping methods available for this address.</p>';
                    updateShippingCost( 0 );
                    return;
                }

                // Render radio buttons.
                let html = '';
                methods.forEach( function ( m, i ) {
                    const priceText = m.price > 0
                        ? ( SYM + parseFloat( m.price ).toFixed( 2 ) )
                        : 'Free';
                    const checked = i === 0 ? ' checked' : '';
                    // Realtime carrier services (KI-32) share one base method_id, so the
                    // radio group uses a UI-only name and each option carries its
                    // method_id + service_code in data-* attributes. The Place Order
                    // handler splits the checked option into shipping_method_id +
                    // shipping_service_code (empty service_code for core zone methods).
                    const svc = m.service_code || '';
                    html += '<label class="tc-shipping-method">'
                         +  '<input type="radio" name="tc_shipping_choice" value="' + escAttr( String( m.id ) + '|' + svc ) + '"'
                         +      ' data-method-id="' + escAttr( String( m.id ) ) + '"'
                         +      ' data-service-code="' + escAttr( svc ) + '"'
                         +      ' class="tc-radio"' + checked + ' />'
                         +  '<span class="tc-shipping-method__name">' + escHtml( m.name ) + '</span>'
                         +  '<span class="tc-shipping-method__price">' + priceText + '</span>'
                         +  '</label>';
                } );
                shipMethodsList.innerHTML = html;

                // Auto-select first method's cost.
                updateShippingCost( methods[0].price || 0 );

                // Listen for method change.
                shipMethodsList.querySelectorAll( 'input[name="tc_shipping_choice"]' ).forEach( function ( radio ) {
                    radio.addEventListener( 'change', function () {
                        const sel = methods.find( function ( m ) {
                            return String( m.id ) === radio.dataset.methodId
                                && String( m.service_code || '' ) === ( radio.dataset.serviceCode || '' );
                        } );
                        updateShippingCost( sel ? sel.price : 0 );
                    } );
                } );

            } catch ( err ) {
                if ( shipMethodsList ) {
                    shipMethodsList.innerHTML = '<p style="color:#C84646;font-size:13px;margin:0;">Could not load shipping options. Please try again.</p>';
                }
            }
        }

        function updateShippingCost( cost ) {
            selectedShipCost = parseFloat( cost ) || 0;
            if ( summaryShipping ) {
                summaryShipping.textContent = selectedShipCost > 0
                    ? ( SYM + selectedShipCost.toFixed( 2 ) )
                    : 'Free';
            }
            // Recalculate total.
            recalcTotal();
        }

        function recalcTotal() {
            if ( ! summaryTotal ) return;
            const num = function ( id ) {
                const el = document.getElementById( id );
                if ( ! el ) return 0;
                const v = parseFloat( ( el.textContent || '0' ).replace( /[^0-9.\-]/g, '' ) );
                return isNaN( v ) ? 0 : v;
            };
            const subtotal = num( 'tc-summary-subtotal' );
            const coupon   = Math.abs( num( 'tc-summary-discount' ) );  // coupon discount row (if present)
            const points   = Math.abs( num( 'tc-summary-points' ) );    // reward points discount (if applied)
            const giftCard = Math.abs( num( 'tc-summary-gc' ) );        // gift card discount (S255, if applied)
            const tax      = num( 'tc-summary-tax' );                   // 0 if cell still shows "Calculated above"

            // Generic positive adjustment lines (S328). Any summary cell carrying a
            // data-tc-total-adjust attribute contributes its amount to the live total,
            // letting extensions (e.g. Tip / Donation) reflect their line in the preview
            // with no further core edits. The attribute value is parsed (falling back to
            // the cell text); non-numeric entries contribute 0. The server stays
            // authoritative — this only keeps the on-screen estimate in sync.
            var adjust = 0;
            var adjEls = document.querySelectorAll( '[data-tc-total-adjust]' );
            for ( var ai = 0; ai < adjEls.length; ai++ ) {
                var rawAdj = adjEls[ ai ].getAttribute( 'data-tc-total-adjust' );
                if ( rawAdj === null || rawAdj === '' ) { rawAdj = adjEls[ ai ].textContent || '0'; }
                var av = parseFloat( String( rawAdj ).replace( /[^0-9.\-]/g, '' ) );
                if ( ! isNaN( av ) ) { adjust += av; }
            }

            const total    = subtotal - coupon - points - giftCard + selectedShipCost + tax + adjust;
            summaryTotal.textContent = SYM + Math.max( 0, total ).toFixed( 2 );
        }

        /**
         * Fetch the current tax total for the cart at the entered address and
         * write it to the #tc-summary-tax cell.  The cart itself is loaded
         * server-side from the session — we only send the address, the same
         * way fetchShippingMethods does.
         */
        async function fetchTax() {
            const addr = getShippingAddress();
            if ( ! addr.country ) {
                // No address yet — clear the tax cell to a definite zero so the
                // total math is correct, and let the customer see we're not
                // hiding tax.
                if ( summaryTax ) summaryTax.textContent = SYM + '0.00';
                recalcTotal();
                return;
            }
            try {
                const result = await apiRequest( `${API}/checkout/tax`, {
                    method: 'POST',
                    body:   JSON.stringify( { address: addr } ),
                } );
                const taxAmt = parseFloat( result.tax_total ) || 0;
                if ( summaryTax ) summaryTax.textContent = SYM + taxAmt.toFixed( 2 );
                recalcTotal();
            } catch ( err ) {
                // On error, leave the cell as-is.  recalcTotal will treat
                // a non-numeric cell as zero, so the customer total still adds up
                // (they just won't see a tax line that didn't compute).
            }
        }

        /**
         * Re-fetch shipping + tax in parallel after an address change.
         * Each updates its own summary cell and calls recalcTotal independently;
         * recalcTotal is idempotent so the eventual ordering doesn't matter.
         */
        function refetchTotals() {
            fetchShippingMethods();
            fetchTax();
        }

        function escHtml( str ) {
            const div = document.createElement( 'div' );
            div.textContent = str;
            return div.innerHTML;
        }

        // Attribute-safe escape: escHtml handles & < >; also neutralise quotes so a
        // value can sit inside a double-quoted HTML attribute.
        function escAttr( str ) {
            return escHtml( str ).replace( /"/g, '&quot;' ).replace( /'/g, '&#39;' );
        }

        // Trigger re-fetch (shipping + tax) when address fields change (debounced).
        const addressFields = [
            'billing_country', 'billing_state', 'billing_postcode',
            'shipping_country', 'shipping_state', 'shipping_postcode',
        ];
        addressFields.forEach( function ( id ) {
            const el = document.getElementById( id );
            if ( ! el ) return;
            el.addEventListener( 'change', function () {
                clearTimeout( shipFetchTimer );
                shipFetchTimer = setTimeout( refetchTotals, 400 );
            } );
            // Also catch typing in text fields.
            if ( el.tagName === 'INPUT' ) {
                el.addEventListener( 'input', function () {
                    clearTimeout( shipFetchTimer );
                    shipFetchTimer = setTimeout( refetchTotals, 800 );
                } );
            }
        } );

        // Also re-fetch when "ship to different address" toggles.
        if ( shipToggle ) {
            shipToggle.addEventListener( 'change', function () {
                setTimeout( refetchTotals, 200 );
            } );
        }

        // If address is already pre-populated (saved address), fetch immediately.
        const prefilledCountry = document.getElementById( 'billing_country' )?.value;
        if ( prefilledCountry ) {
            refetchTotals();
        }

        // ── Real-time field validation on blur ────────────────────────────────
        checkout.addEventListener( 'blur', async function ( e ) {
            const input = e.target.closest( 'input, select, textarea' );
            if ( ! input || ! input.name ) return;

            const data   = collectFormData();
            const result = await apiRequest( `${API}/checkout/validate`, {
                method: 'POST',
                body:   JSON.stringify( data ),
            } ).catch( () => null );

            if ( ! result ) return;

            // Clear error for this field, then re-apply if still invalid.
            const errorEl = document.querySelector( `.tc-field__error[data-field="${input.name}"]` );
            if ( errorEl ) {
                errorEl.textContent = result.errors?.[ input.name ] || '';
            }
            input.classList.toggle( 'tc-input--error', !! result.errors?.[ input.name ] );

        }, true ); // useCapture for blur bubbling.

        // ── Payment method selection ──────────────────────────────────────────
        checkout.addEventListener( 'change', function ( e ) {
            const radio = e.target.closest( '.tc-payment-radio' );
            if ( ! radio ) return;

            // Emit hook for Module 6 payment gateways to inject their fields.
            const event = new CustomEvent( 'titancart:payment_method_selected', {
                detail: {
                    method_id:  radio.value,
                    processor:  radio.dataset.processor,
                    container:  document.getElementById( 'tc-payment-fields' ),
                },
                bubbles: true,
            } );
            document.dispatchEvent( event );
        } );

        // ── Place Order ───────────────────────────────────────────────────────
        const placeBtn = document.getElementById( 'tc-place-order-btn' );
        if ( placeBtn ) {
            placeBtn.addEventListener( 'click', async function () {
                clearAllErrors();
                setLoading( placeBtn, true );

                const data = collectFormData();

                // Shipping selection (KI-32): the picker uses a UI-only radio group, so
                // translate the checked option into the two fields the checkout API
                // expects — shipping_method_id and shipping_service_code (empty for core
                // zone methods). Set here (before the before_place_order event below) so
                // gateway interceptors that reuse e.detail.data inherit both.
                const shipChoice = document.querySelector( 'input[name="tc_shipping_choice"]:checked' );
                if ( shipChoice ) {
                    data.shipping_method_id    = shipChoice.dataset.methodId || '';
                    data.shipping_service_code = shipChoice.dataset.serviceCode || '';
                }

                // Newsletter opt-in (S241 / Phase 5 block B) — collect the list IDs
                // the customer ticked. Attached before the before_place_order event
                // dispatches so gateway interceptors (Stripe / Authorize.net / Square),
                // which reuse e.detail.data, inherit it too. Honoured server-side by
                // CheckoutController::maybe_subscribe_checkout().
                data.newsletter_optin = Array.from(
                    document.querySelectorAll( '.tc-checkout-optin input[type="checkbox"]:checked' )
                ).map( cb => parseInt( cb.value, 10 ) ).filter( n => n > 0 );

                // reCAPTCHA — attach a token for guest checkout when protected.
                if ( RC && RC.siteKey ) {
                    const token = await recaptchaToken();
                    if ( ! token && RC.version === 'v2' ) {
                        showBanner( I18N.recaptcha || 'Please complete the reCAPTCHA.' );
                        setLoading( placeBtn, false );
                        return;
                    }
                    data.recaptcha_token = token;
                }

                try {
                    // Allow payment gateways to inject extra data (Module 6).
                    const preEvent = new CustomEvent( 'titancart:before_place_order', {
                        detail: { data },
                        bubbles: true,
                        cancelable: true,
                    } );
                    const allowed = document.dispatchEvent( preEvent );
                    if ( ! allowed ) {
                        // A payment gateway cancelled submission (e.g. 3DS pending).
                        setLoading( placeBtn, false );
                        return;
                    }

                    const result = await apiRequest( `${API}/checkout/place-order`, {
                        method: 'POST',
                        body:   JSON.stringify( data ),
                    } );

                    // Gateway-driven redirect (e.g. PayPal approval URL) takes precedence
                    // over the thank-you page URL. The customer must complete the payment
                    // on the external gateway before reaching the thank-you page.
                    //
                    // Response shape:
                    //   result.redirect_url              — thank-you page URL (always set on success)
                    //   result.payment_data.redirect_url — gateway approval URL (PayPal only)
                    //
                    // Stripe's inline template interceptor cancels this handler via
                    // preventDefault() on `titancart:before_place_order`, so we never
                    // reach this code path for Stripe. All other gateways that don't
                    // need an external redirect (Authorize.net, Square, offline methods)
                    // fall through to result.redirect_url → thank-you page.
                    const gatewayRedirect = result.payment_data && result.payment_data.redirect_url;
                    const thankYouUrl     = result.redirect_url;
                    const nextUrl         = gatewayRedirect || thankYouUrl;

                    if ( nextUrl ) {
                        window.location.href = nextUrl;
                    } else {
                        // No redirect URL at all — something's wrong. Don't silently succeed.
                        showBanner( 'Order placed but no redirect URL was returned. Please contact support.' );
                        setLoading( placeBtn, false );
                    }

                } catch ( err ) {
                    setLoading( placeBtn, false );
                    showBanner( err.message );

                    if ( err.errors && Object.keys( err.errors ).length ) {
                        showErrors( err.errors );
                    }
                }
            } );
        }

    } );

} )();
