/**
 * TitanCart — Address Autocomplete (Google Places — New API).
 *
 * Wires Google's modern `PlaceAutocompleteElement` web component to any
 * input marked `data-tc-autocomplete="<fieldset>"`, where `<fieldset>`
 * is one of: billing | shipping | account | <custom>.
 *
 * Architecture: PlaceAutocompleteElement renders its OWN input field
 * (it is NOT attached to an existing input like the legacy API was).
 * So we insert the gmp element next to the original `<input>`, hide
 * the original offscreen, and mirror typing/selection back to it so
 * the form's `name` attribute submits the user's address normally.
 *
 * Why the new API: as of March 1st 2025, Google deprecated the legacy
 * `google.maps.places.Autocomplete` class for any Google Cloud project
 * created after that date. New customers MUST use this element. The
 * new component works for both pre- and post-March-2025 projects, so
 * it's also forward-compatible.
 *
 * Required Google Cloud setup:
 *   1. Places API (New) enabled in APIs & Services → Library
 *   2. Maps JavaScript API enabled in APIs & Services → Library
 *   3. API key allows BOTH (or "Don't restrict key")
 *
 * Failure modes (all graceful — no broken UI, no console errors that
 * affect checkout):
 *   - SDK fails to load (network, key blocked) → init never fires;
 *     forms remain plain text inputs.
 *   - PlaceAutocompleteElement constructor throws → console.warn,
 *     plain text fallback per input.
 *   - fetchFields rejects (quota, scope) → console.warn, no auto-fill
 *     for that selection but the form still works.
 *
 * @package TitanCart
 * @since   1.0.0 (S139)
 */
(function () {
    'use strict';

    // ──────────────────────────────────────────────────────────────────
    // Field-set → sibling-ID mapping.
    // The fieldset value comes from the [data-tc-autocomplete] attribute
    // on the address-1 input. Known fieldsets have explicit ID maps;
    // unknown fieldsets fall through to suffix-derivation.
    // ──────────────────────────────────────────────────────────────────
    var FIELD_SETS = {
        billing: {
            city:     'billing_city',
            state:    'billing_state',
            postcode: 'billing_postcode',
            country:  'billing_country'
        },
        shipping: {
            city:     'shipping_city',
            state:    'shipping_state',
            postcode: 'shipping_postcode',
            country:  'shipping_country'
        },
        account: {
            city:     'tc-addr-city',
            state:    'tc-addr-state',
            postcode: 'tc-addr-postcode',
            country:  'tc-addr-country'
        }
    };

    // Countries where state codes are short ISO 3166-2 codes commonly
    // used in their short form (PA, ON, NSW, etc.). Elsewhere we use
    // the long_name (e.g. "Bavaria").
    var SHORT_STATE_COUNTRIES = ['US','CA','AU','BR','MX','IN','MY','AR'];

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

    function getEl(id) {
        if (!id) return null;
        return document.getElementById(id);
    }

    /**
     * Set a value on either an <input> or a <select>, then fire `change`
     * so any downstream listeners (validation, conditional rendering,
     * shipping/tax re-fetch) react. Handles the case where the select
     * has no matching option by leaving the previous selection alone
     * — better than blanking it.
     */
    function setValue(el, value) {
        if (!el || value == null) return;

        if (el.tagName === 'SELECT') {
            var matched = false;
            for (var i = 0; i < el.options.length; i++) {
                if (el.options[i].value === value) {
                    el.selectedIndex = i;
                    matched = true;
                    break;
                }
            }
            if (!matched) {
                var lower = String(value).toLowerCase();
                for (var j = 0; j < el.options.length; j++) {
                    if (el.options[j].value.toLowerCase() === lower) {
                        el.selectedIndex = j;
                        matched = true;
                        break;
                    }
                }
            }
        } else {
            el.value = value;
        }

        var event;
        try {
            event = new Event('change', { bubbles: true });
        } catch (e) {
            event = document.createEvent('HTMLEvents');
            event.initEvent('change', true, false);
        }
        el.dispatchEvent(event);
    }

    /**
     * Find an addressComponent matching the given Google place type.
     * The new API exposes each component as
     *   { types: [...], longText: "...", shortText: "...", languageCode }
     * — first match wins.
     */
    function findComponent(components, type) {
        if (!components) return null;
        for (var i = 0; i < components.length; i++) {
            var c = components[i];
            if (c.types && c.types.indexOf(type) !== -1) {
                return c;
            }
        }
        return null;
    }

    /**
     * City lookup with cascading priority. Different countries / Google
     * data quality use different component types — first match wins.
     */
    function findCity(components) {
        var preferred = [
            'locality',                    // most countries
            'postal_town',                 // UK
            'sublocality_level_1',         // some Asian / Latin American
            'administrative_area_level_3', // smaller civil divisions
            'administrative_area_level_2'  // counties / boroughs (last-ditch)
        ];
        for (var i = 0; i < preferred.length; i++) {
            var c = findComponent(components, preferred[i]);
            if (c) return c.longText || c.shortText || '';
        }
        return '';
    }

    function pickStateName(stateComp, countryCode) {
        if (!stateComp) return '';
        if (SHORT_STATE_COUNTRIES.indexOf(countryCode) !== -1) {
            return stateComp.shortText || stateComp.longText || '';
        }
        return stateComp.longText || stateComp.shortText || '';
    }

    /**
     * Build address-line-1 from street_number + route. If only one is
     * available, use it alone. If neither, return empty (the user's
     * prior input stays in place).
     */
    function buildAddressLine1(components) {
        var num   = findComponent(components, 'street_number');
        var route = findComponent(components, 'route');
        var n     = num   ? (num.longText   || '') : '';
        var r     = route ? (route.longText || '') : '';
        if (n && r) return n + ' ' + r;
        if (r) return r;
        if (n) return n;
        return '';
    }

    /**
     * Derive sibling field IDs for an unknown fieldset by stripping
     * common address-1 suffixes from the input's id. Falls back
     * gracefully if the input has no id or no recognizable suffix.
     */
    function deriveFieldIds(inputId) {
        if (!inputId) return null;
        var snakePattern = /_address_1$/;
        var kebabPattern = /-address-?1$/;
        if (snakePattern.test(inputId)) {
            var snakePrefix = inputId.replace(snakePattern, '');
            return {
                city:     snakePrefix + '_city',
                state:    snakePrefix + '_state',
                postcode: snakePrefix + '_postcode',
                country:  snakePrefix + '_country'
            };
        }
        if (kebabPattern.test(inputId)) {
            var kebabPrefix = inputId.replace(kebabPattern, '');
            return {
                city:     kebabPrefix + '-city',
                state:    kebabPrefix + '-state',
                postcode: kebabPrefix + '-postcode',
                country:  kebabPrefix + '-country'
            };
        }
        return null;
    }

    // ──────────────────────────────────────────────────────────────────
    // Per-input attach
    // ──────────────────────────────────────────────────────────────────

    function attachAutocomplete(input, fieldset) {
        if (!window.google || !window.google.maps || !window.google.maps.places) {
            return;
        }

        var PlaceAutocompleteElement = window.google.maps.places.PlaceAutocompleteElement;
        if (!PlaceAutocompleteElement) {
            console.warn('[TitanCart] Address autocomplete: PlaceAutocompleteElement not available. Ensure "Places API (New)" is enabled in Google Cloud Console and the API key allows it.');
            return;
        }

        var fields = FIELD_SETS[fieldset];
        if (!fields) {
            fields = deriveFieldIds(input.id);
            if (!fields) {
                console.warn('[TitanCart] Address autocomplete: unknown fieldset "' + fieldset + '" and unable to derive sibling field IDs from input id "' + input.id + '"');
                return;
            }
        }

        var gmpEl;
        try {
            gmpEl = new PlaceAutocompleteElement();
        } catch (err) {
            console.warn('[TitanCart] Address autocomplete failed to construct PlaceAutocompleteElement:', err);
            return;
        }

        // Mirror the placeholder for visual continuity with the original
        // input. Both the property and the attribute paths are tried —
        // SDK versions differ in which one sticks.
        var placeholder = input.placeholder || input.getAttribute('placeholder') || '';
        if (placeholder) {
            try { gmpEl.placeholder = placeholder; } catch (e) {}
            try { gmpEl.setAttribute('placeholder', placeholder); } catch (e) {}
        }

        // Host-level layout styling. The element's internal input lives
        // in shadow DOM and won't inherit our `.tc-input` rules, but
        // the host element does respect display/width/box-sizing.
        gmpEl.classList.add('tc-input', 'tc-pac-element');
        gmpEl.style.display = 'block';
        gmpEl.style.width = '100%';
        gmpEl.style.boxSizing = 'border-box';

        // Re-point the <label for=...> at the new element so screen
        // readers and click-on-label still focus the right thing.
        var newId = (input.id || 'tc-aa-' + Math.random().toString(36).slice(2, 8)) + '-gmp';
        gmpEl.id = newId;
        if (input.id) {
            var label = document.querySelector('label[for="' + input.id + '"]');
            if (label) label.setAttribute('for', newId);
        }

        // Insert immediately AFTER the original input.
        if (input.parentNode) {
            input.parentNode.insertBefore(gmpEl, input.nextSibling);
        }

        // Hide the original input but keep it in the DOM so its `name`
        // submits with the form. Offscreen positioning instead of
        // display:none avoids tripping any validation library that
        // checks display.
        input.style.position = 'absolute';
        input.style.left = '-9999px';
        input.style.opacity = '0';
        input.style.height = '0';
        input.style.width = '0';
        input.style.padding = '0';
        input.style.border = '0';
        input.tabIndex = -1;
        input.setAttribute('aria-hidden', 'true');
        // Drop `required` since the user no longer interacts with this
        // input. Native HTML5 validation can't see offscreen-but-required
        // fields reliably across browsers, and the visible gmp element
        // has its own behavior. The mirrored value is present at submit.
        input.removeAttribute('required');

        // Pre-populate the gmp element with the saved value (logged-in
        // user with a saved address). Property and attribute paths both
        // tried; whichever sticks wins. If neither sticks on this SDK
        // version, the field starts blank and the user re-enters or
        // picks an address — preferable to a stale-looking display.
        if (input.value) {
            try { gmpEl.value = input.value; } catch (e) {}
            try { gmpEl.setAttribute('value', input.value); } catch (e) {}
        }

        // Mirror typing in the gmp element back to the original input
        // so a manually-typed address (no suggestion picked) still
        // submits with the form.
        gmpEl.addEventListener('input', function () {
            var v = '';
            try { v = gmpEl.value || ''; } catch (e) {}
            input.value = v;
        });

        // Selection handler — user picked a suggestion from the dropdown.
        gmpEl.addEventListener('gmp-select', function (event) {
            if (!event || !event.placePrediction) return;
            var place;
            try { place = event.placePrediction.toPlace(); } catch (e) { return; }
            if (!place || typeof place.fetchFields !== 'function') return;

            place.fetchFields({ fields: ['addressComponents'] }).then(function () {
                var comps = place.addressComponents || [];
                if (!comps.length) return;

                var line1     = buildAddressLine1(comps);
                var city      = findCity(comps);
                var pcComp    = findComponent(comps, 'postal_code');
                var stateComp = findComponent(comps, 'administrative_area_level_1');
                var ctryComp  = findComponent(comps, 'country');
                var ctryCode  = ctryComp ? (ctryComp.shortText || '') : '';

                if (line1) {
                    input.value = line1;
                    try { gmpEl.value = line1; } catch (e) {}
                }
                if (city) {
                    setValue(getEl(fields.city), city);
                }
                if (pcComp) {
                    setValue(getEl(fields.postcode), pcComp.longText || '');
                }
                if (stateComp) {
                    setValue(getEl(fields.state), pickStateName(stateComp, ctryCode));
                }
                if (ctryComp) {
                    setValue(getEl(fields.country), ctryCode);
                }

                // Fire change on the original (hidden) input so any
                // validation / shipping-tax re-fetch listeners react to
                // the auto-fill. The mirrored value is already in place.
                try {
                    input.dispatchEvent(new Event('change', { bubbles: true }));
                } catch (e) {}
            }).catch(function (err) {
                console.warn('[TitanCart] Address autocomplete fetchFields failed:', err);
            });
        });
    }

    // ──────────────────────────────────────────────────────────────────
    // Init / public surface
    // ──────────────────────────────────────────────────────────────────

    function initAll() {
        var inputs = document.querySelectorAll('[data-tc-autocomplete]');
        for (var i = 0; i < inputs.length; i++) {
            var input = inputs[i];
            if (input.dataset.tcAutocompleteAttached === '1') continue;
            input.dataset.tcAutocompleteAttached = '1';
            var fieldset = input.getAttribute('data-tc-autocomplete') || '';
            attachAutocomplete(input, fieldset);
        }
    }

    /**
     * Called by the Google Maps SDK when the Places library finishes
     * loading (via the `?callback=` URL parameter). Also exposed on
     * window for manual re-scan after dynamic DOM changes — a future
     * module that injects a new address form at runtime can call this
     * to wire up the new inputs.
     */
    window.tcAddressAutocompleteInit = function () {
        initAll();
    };
})();
