/**
 * AIEO Address Autocomplete — WC official provider implementation.
 *
 * Hooks into WooCommerce 10.7's `wc.addressAutocomplete` API per the
 * docs at https://developer.woocommerce.com/docs/features/address-autocomplete/.
 * The PHP side (AIEO_Address_Provider_Google) registers the provider
 * id + branding via the `woocommerce_address_providers` filter; this
 * file owns the runtime search/select logic and calls Google Places.
 *
 * Migrated to Google's NEW Places API (March-2025 cutover):
 *   - `google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions`
 *     replaces the deprecated `AutocompleteService.getPlacePredictions`.
 *   - `Place.fetchFields` (returned via `placePrediction.toPlace()`)
 *     replaces the deprecated `PlacesService.getDetails`.
 *
 * Greek address resolution chain (because Google's
 * administrative_area_level_1 returns the region "Αττική" while
 * operators want νομοί like "Πειραιά" / "Δυτικής Αττικής"):
 *   1. Match Google's level_1/2/3 long_name against WC state list.
 *   2. If no match → infer from postcode prefix (PHP-supplied map
 *      pinned to the operator's country, here Greek νομοί).
 *
 * Server-localised globals:
 *   - aieoAddressProvider.providerId — must equal PHP's PROVIDER_ID
 *   - aieoAddressProvider.country    — ISO-2 country to scope searches to
 *   - aieoAddressProvider.language   — Google Places language hint
 *   - aieoAddressProvider.wcStates   — { code → display_name }
 *   - aieoAddressProvider.postcodeMap — sparse { prefix3 → state_code }
 *                                       for fallback inference
 *
 * @package ai-eshop-optimizer
 * @since 5.4
 */
(function () {
    'use strict';

    var __ = ( window.wp && window.wp.i18n && window.wp.i18n.__ ) ? window.wp.i18n.__ : function (s) { return s; };
    var _x = ( window.wp && window.wp.i18n && window.wp.i18n._x ) ? window.wp.i18n._x : function (s) { return s; };
    var _n = ( window.wp && window.wp.i18n && window.wp.i18n._n ) ? window.wp.i18n._n : function (s, p, n) { return n === 1 ? s : p; };
    var _sprintf = ( window.wp && window.wp.i18n && window.wp.i18n.sprintf ) ? window.wp.i18n.sprintf : function (s) { return s; };

    if (typeof window === 'undefined') { return; }
    if (!window.aieoAddressProvider || !window.aieoAddressProvider.providerId) { return; }

    var cfg = window.aieoAddressProvider;

    // Lazy handles to the Places library and a per-search session
    // token. Google's billing model treats search+select inside one
    // session token as a single billable unit, so we mint one per
    // user-typing→user-picking cycle.
    var placesLib    = null;
    var sessionToken = null;
    var libLoading   = null;

    function ensurePlacesLib() {
        if (placesLib) { return Promise.resolve(placesLib); }
        if (libLoading) { return libLoading; }
        if (typeof google === 'undefined' || !google.maps || !google.maps.importLibrary) {
            return Promise.reject(new Error('Google Maps SDK not loaded'));
        }
        libLoading = google.maps.importLibrary('places').then(function (lib) {
            placesLib = lib;
            sessionToken = new lib.AutocompleteSessionToken();
            libLoading = null;
            return lib;
        }).catch(function (err) {
            libLoading = null;
            throw err;
        });
        return libLoading;
    }

    function newSessionToken() {
        if (!placesLib) { return null; }
        return new placesLib.AutocompleteSessionToken();
    }

    /* ------------------------------------------------------------------ */
    /* State-code resolution                                              */
    /* ------------------------------------------------------------------ */

    function normaliseStateName(name) {
        if (!name) { return ''; }
        var s = String(name).toLowerCase();
        if (typeof s.normalize === 'function') {
            // Strip Greek tonos + Latin diacritics.
            s = s.normalize('NFD').replace(/[̀-ͯ]/g, '');
        }
        // Strip trailing genitive markers and pluralisations.
        s = s.replace(/[ςσsν\s]+$/u, '');
        // Strip Greek phrasing prefixes Google adds before the νομός
        // name (e.g. "Περιφερειακή ενότητα Δυτικής Αττικής" →
        // "Δυτικής Αττικής"; "Περιφέρεια Αττικής" → "Αττικής").
        s = s.replace(/^(?:περιφερειακη ενοτητα|περιφερεια|νομος|νομαρχια)\s+/iu, '');
        s = s.replace(/\s+/g, ' ').trim();
        return s;
    }

    /**
     * Resolve a name (long_name or short_name from Google) to a WC
     * state code. Returns '' if no match — caller can then try a
     * different admin level or the postcode fallback. Deliberately
     * does NOT return the raw input on miss (the previous version did,
     * which broke the fallback chain because non-empty looked like a
     * successful match).
     */
    function matchWcStateCode(googleLong, googleShort) {
        var states = (cfg.wcStates && typeof cfg.wcStates === 'object') ? cfg.wcStates : null;
        if (!states) { return ''; }

        // 1. Exact code match (Google sometimes returns the ISO code as
        //    short_name; e.g. some US states).
        if (googleShort && Object.prototype.hasOwnProperty.call(states, googleShort)) {
            return googleShort;
        }

        var candidates = [];
        if (googleLong)  { candidates.push(googleLong); }
        if (googleShort && googleShort !== googleLong) { candidates.push(googleShort); }
        if (!candidates.length) { return ''; }

        // 2. Exact display-name match (lowercase).
        for (var code in states) {
            if (!Object.prototype.hasOwnProperty.call(states, code)) { continue; }
            var stateName = String(states[code] || '');
            for (var i = 0; i < candidates.length; i++) {
                if (stateName.toLowerCase() === String(candidates[i]).toLowerCase()) {
                    return code;
                }
            }
        }

        // 3. Normalised fuzzy match (handles Greek genitive case +
        //    "Περιφερειακή ενότητα" prefix + Latin diacritics).
        var normCandidates = candidates.map(normaliseStateName).filter(Boolean);
        if (!normCandidates.length) { return ''; }
        for (var code2 in states) {
            if (!Object.prototype.hasOwnProperty.call(states, code2)) { continue; }
            var normName = normaliseStateName(states[code2]);
            if (!normName) { continue; }
            for (var j = 0; j < normCandidates.length; j++) {
                if (normName === normCandidates[j] ||
                    (normName.length > 3 && normCandidates[j].indexOf(normName) !== -1) ||
                    (normCandidates[j].length > 3 && normName.indexOf(normCandidates[j]) !== -1)) {
                    return code2;
                }
            }
        }

        return '';
    }

    /**
     * Postcode-based fallback for the configured country. The PHP side
     * supplies `postcodeMap` as a sparse 3-digit-prefix → state code
     * lookup; we coarsen to 2 digits when the 3-digit prefix isn't
     * mapped (e.g. small/rare postcodes). Greek-specific by content
     * but country-agnostic by mechanism — flip a different country's
     * postcodeMap and the same code works.
     */
    function inferStateFromPostcode(postcode) {
        var map = cfg.postcodeMap || null;
        if (!map || typeof map !== 'object') { return ''; }
        if (!postcode) { return ''; }
        var digits = String(postcode).replace(/\D+/g, '');
        if (digits.length < 2) { return ''; }
        // Try most specific first (3 digits) then narrow (2 digits).
        if (digits.length >= 3) {
            var p3 = digits.substring(0, 3);
            if (Object.prototype.hasOwnProperty.call(map, p3)) { return map[p3]; }
        }
        var p2 = digits.substring(0, 2);
        if (Object.prototype.hasOwnProperty.call(map, p2)) { return map[p2]; }
        return '';
    }

    /* ------------------------------------------------------------------ */
    /* Google address-component → WC address shape                        */
    /* ------------------------------------------------------------------ */

    function googleComponentsToAddress(components, formattedAddress) {
        var out = {
            address_1: '',
            address_2: '',
            city:      '',
            state:     '',
            postcode:  '',
            country:   ''
        };
        var streetNumber = '', route = '';
        var aaL1Long = '', aaL1Short = '';
        var aaL2Long = '', aaL2Short = '';
        var aaL3Long = '', aaL3Short = '';
        (components || []).forEach(function (c) {
            // The new Place API exposes types as `c.types`; some old
            // payloads use `c.types[]` already; same shape.
            var t = c.types || [];
            // Component fields can be `longText` / `shortText` (new
            // Place API) or `long_name` / `short_name` (legacy + most
            // current payloads). Normalise.
            var longName  = c.longText  || c.long_name  || '';
            var shortName = c.shortText || c.short_name || '';
            if (t.indexOf('street_number') !== -1)              { streetNumber = longName; }
            else if (t.indexOf('route') !== -1)                 { route = longName; }
            else if (t.indexOf('locality') !== -1 ||
                     t.indexOf('postal_town') !== -1)           { out.city = longName; }
            else if (t.indexOf('administrative_area_level_1') !== -1) { aaL1Long = longName; aaL1Short = shortName; }
            else if (t.indexOf('administrative_area_level_2') !== -1) { aaL2Long = longName; aaL2Short = shortName; }
            else if (t.indexOf('administrative_area_level_3') !== -1) { aaL3Long = longName; aaL3Short = shortName; }
            else if (t.indexOf('postal_code') !== -1)           { out.postcode = longName; }
            else if (t.indexOf('country') !== -1)               { out.country  = shortName; }
        });
        // Greek convention: street name first, then number.
        out.address_1 = (route + (streetNumber ? ' ' + streetNumber : '')).trim();
        if (!out.address_1 && formattedAddress) {
            out.address_1 = formattedAddress.split(',')[0].trim();
        }

        // Resolve to WC state code. Walk admin levels 1→2→3, then fall
        // back to postcode-prefix inference (Greek operators want νομοί
        // like "Πειραιά" / "Δυτικής Αττικής" — those land in level_2,
        // not level_1, and Google sometimes prefixes with "Περιφερειακή
        // ενότητα" which the normaliser strips).
        var resolved = matchWcStateCode(aaL1Long, aaL1Short);
        if (!resolved) { resolved = matchWcStateCode(aaL2Long, aaL2Short); }
        if (!resolved) { resolved = matchWcStateCode(aaL3Long, aaL3Short); }
        if (!resolved && out.postcode) { resolved = inferStateFromPostcode(out.postcode); }
        out.state = resolved || '';
        return out;
    }

    /* ------------------------------------------------------------------ */
    /* Provider                                                           */
    /* ------------------------------------------------------------------ */

    var provider = {
        id: cfg.providerId,

        canSearch: function (country) {
            if (!country) { return false; }
            var allowed = (cfg.country || 'GR').toUpperCase();
            return String(country).toUpperCase() === allowed;
        },

        /**
         * New API: AutocompleteSuggestion.fetchAutocompleteSuggestions
         * returns a Promise that resolves to { suggestions: [...] }.
         * Each suggestion has a `placePrediction` with `placeId`,
         * `text` (Formatter), `mainText`, `secondaryText`, etc.
         */
        search: function (query, country, type) {
            if (!query || query.length < 3) { return Promise.resolve([]); }
            return ensurePlacesLib().then(function (lib) {
                var request = {
                    input: query,
                    sessionToken: sessionToken,
                    includedPrimaryTypes: ['street_address', 'route', 'subpremise', 'premise'],
                };
                if (country) {
                    // New API uses `includedRegionCodes` (lowercase).
                    request.includedRegionCodes = [String(country).toLowerCase()];
                }
                return lib.AutocompleteSuggestion
                    .fetchAutocompleteSuggestions(request)
                    .then(function (resp) {
                        var sugs = (resp && resp.suggestions) || [];
                        return sugs.map(function (s) {
                            var p = s.placePrediction;
                            // `p.text` is a `FormattableText` — call
                            // `.toString()` to get the human-readable
                            // form. `p.placeId` is the stable id we
                            // pass back to `select()`.
                            var label = '';
                            if (p && p.text && typeof p.text.toString === 'function') {
                                label = p.text.toString();
                            } else if (p && p.text) {
                                label = String(p.text);
                            }
                            return {
                                id:    p && p.placeId ? p.placeId : '',
                                label: label,
                            };
                        }).filter(function (s) { return !!s.id; });
                    });
            }).catch(function (err) {
                if (window.console) { console.warn('[AIEO addr] search failed', err); }
                return [];
            });
        },

        /**
         * New API: build the Place from the suggestion's placeId,
         * call `fetchFields` with the canonical field names. The new
         * Place class returns `addressComponents` (camelCase) and
         * `formattedAddress`.
         */
        select: function (addressId) {
            if (!addressId) { return Promise.resolve(null); }
            return ensurePlacesLib().then(function (lib) {
                // Place constructor takes a place_id directly.
                var place = new lib.Place({ id: addressId, requestedLanguage: cfg.language || 'el' });
                return place.fetchFields({
                    fields: ['addressComponents', 'formattedAddress']
                }).then(function () {
                    // Mint a fresh session token for the next
                    // search→select cycle.
                    sessionToken = newSessionToken();
                    var components = place.addressComponents || [];
                    var formatted  = place.formattedAddress  || '';
                    var resolved   = googleComponentsToAddress(components, formatted);
                    return resolved;
                });
            }).catch(function (err) {
                if (window.console) { console.warn('[AIEO addr] select failed', err); }
                return null;
            });
        }
    };

    /* ------------------------------------------------------------------ */
    /* Registration                                                       */
    /* ------------------------------------------------------------------ */

    function registerNow() {
        if (!window.wc || !window.wc.addressAutocomplete) { return false; }
        var api = window.wc.addressAutocomplete;
        if (typeof api.registerAddressAutocompleteProvider !== 'function') { return false; }
        try {
            api.registerAddressAutocompleteProvider(provider);
            if (window.console) { console.log('[AIEO] address provider registered: ' + provider.id); }
            return true;
        } catch (e) {
            if (window.console) { console.warn('[AIEO] address provider registration failed', e); }
            return false;
        }
    }

    // Only attempt to register the WC autocomplete provider if the
    // operator opted in to autocomplete (otherwise we're loaded purely
    // for the geolocation button). NB: wp_localize_script stringifies
    // scalars, so this flag arrives as "0"/"1" — coerce with + (in JS
    // the string "0" is truthy, which would defeat the off state).
    if (+cfg.autocompleteEnabled) {
        if (!registerNow()) {
            var attempts = 0;
            var iv = setInterval(function () {
                attempts++;
                if (registerNow() || attempts > 40) { clearInterval(iv); }
            }, 250);
        }
    }

    /* ------------------------------------------------------------------ */
    /* Geolocation: "use my current location" button                      */
    /* ------------------------------------------------------------------ */

    /*
     * Injects a small crosshair button next to the shipping/billing
     * address-1 input on the block checkout. Click → browser
     * geolocation → reverse-geocode via Google → dispatch the parsed
     * address to the WC cart store. We deliberately reuse
     * googleComponentsToAddress() / matchWcStateCode() /
     * inferStateFromPostcode() above so the resolution semantics
     * (Greek νομός handling, postcode fallback) stay in lockstep with
     * the autocomplete path.
     */

    var geocoderLib = null;
    var geocoderInstance = null;

    function ensureGeocoder() {
        if (geocoderInstance) { return Promise.resolve(geocoderInstance); }
        if (typeof google === 'undefined' || !google.maps || !google.maps.importLibrary) {
            return Promise.reject(new Error('Google Maps SDK not loaded'));
        }
        return google.maps.importLibrary('geocoding').then(function (lib) {
            geocoderLib = lib;
            geocoderInstance = new lib.Geocoder();
            return geocoderInstance;
        });
    }

    var GEO_BTN_CLASS = 'aieo-geo-locate-btn';
    var GEO_HOST_CLASS = 'aieo-geo-locate-host';

    function makeGeoButton(section) {
        var btn = document.createElement('button');
        btn.type = 'button';
        btn.className = GEO_BTN_CLASS;
        btn.setAttribute('data-aieo-section', section);
        var label = (cfg && cfg.i18n && cfg.i18n.useMyLocation) || __('Use my current location', 'ai-eshop-optimizer');
        btn.setAttribute('aria-label', label);
        btn.title = label;
        // Text-only label. Right-padding on the button reserves space
        // for WC's autocomplete lens, which sits at the input's right
        // edge — the lens then reads visually as our icon.
        var labelEl = document.createElement('span');
        labelEl.className = 'aieo-geo-locate-btn__label';
        labelEl.textContent = label;
        btn.appendChild(labelEl);
        return btn;
    }

    /**
     * Find the visual container for an address-1 input. WC blocks wrap
     * the input in `.wc-block-components-text-input` (or similar); we
     * mark that container so CSS can position our overlay button at
     * its right edge, on top of the input.
     */
    function findFieldWrapper(input) {
        if (!input) { return null; }
        if (typeof input.closest === 'function') {
            var c = input.closest('.wc-block-components-text-input, .wc-block-components-address-form__address_1, .form-row');
            if (c) { return c; }
        }
        return input.parentNode || null;
    }

    function findAddress1Inputs() {
        // Both classic (#shipping_address_1 / #billing_address_1) and
        // block checkout (id starts with `shipping-address_1` or
        // `billing-address_1`, or the unified `address_1` when only one
        // address is shown). Selector union covers all three.
        var selectors = [
            '#shipping_address_1',
            '#billing_address_1',
            'input[id^="shipping-address_1"]',
            'input[id^="billing-address_1"]',
            'input[id^="shipping_address_1"]',
            'input[id^="billing_address_1"]',
            'input[id="address_1"]'
        ];
        return Array.from(document.querySelectorAll(selectors.join(',')));
    }

    function sectionForInput(inp) {
        var id = (inp && inp.id) || '';
        if (id.indexOf('billing') !== -1) { return 'billing'; }
        return 'shipping';
    }

    function injectGeoButton(input) {
        if (!input || input.dataset.aieoGeoMounted === '1') { return; }
        var host = findFieldWrapper(input);
        if (!host) { return; }
        var section = sectionForInput(input);
        // Already mounted on this host? Bail.
        if (host.querySelector('.' + GEO_BTN_CLASS + '[data-aieo-section="' + section + '"]')) {
            input.dataset.aieoGeoMounted = '1';
            return;
        }
        host.classList.add(GEO_HOST_CLASS);
        var btn = makeGeoButton(section);
        // mousedown fires BEFORE focus shifts to the input, so even
        // when the user is mid-tap on the input area the click is
        // captured here and we don't lose the activation to a focus
        // race. preventDefault on mousedown also keeps the input from
        // stealing focus, so the icon stays visible during the geocode.
        btn.addEventListener('mousedown', function (ev) {
            ev.preventDefault();
            ev.stopPropagation();
        });
        btn.addEventListener('click', function (ev) {
            ev.preventDefault();
            ev.stopPropagation();
            handleGeoClick(btn);
        });
        host.appendChild(btn);
        input.dataset.aieoGeoMounted = '1';
    }

    function setBusy(btn, busy) {
        if (!btn) { return; }
        if (busy) {
            btn.classList.add('is-busy');
            btn.setAttribute('aria-busy', 'true');
            btn.disabled = true;
        } else {
            btn.classList.remove('is-busy');
            btn.removeAttribute('aria-busy');
            btn.disabled = false;
        }
    }

    function t(key, fallback) {
        var pack = (cfg && cfg.i18n) || {};
        return (pack && pack[key]) || fallback;
    }

    function flashError(btn, message) {
        if (!btn) { return; }
        btn.classList.add('is-error');
        btn.title = message || t('genericError', __('Could not get your location', 'ai-eshop-optimizer'));
        setTimeout(function () {
            btn.classList.remove('is-error');
            btn.title = btn.getAttribute('aria-label') || '';
        }, 2500);
        if (window.console) { console.warn('[AIEO geo]', message); }
    }

    /**
     * Push the resolved address into the block checkout's cart store.
     * Falls back to filling input values directly on classic checkout
     * (where wc/store/cart isn't dispatched).
     */
    function applyResolvedAddress(resolved, section) {
        if (!resolved) { return; }
        var hasBlocks = window.wp && window.wp.data && typeof window.wp.data.dispatch === 'function';
        if (hasBlocks) {
            try {
                var dispatch = window.wp.data.dispatch('wc/store/cart');
                if (dispatch) {
                    var payload = {
                        address_1: resolved.address_1 || '',
                        address_2: resolved.address_2 || '',
                        city:      resolved.city || '',
                        state:     resolved.state || '',
                        postcode:  resolved.postcode || '',
                        country:   resolved.country || (cfg.country || 'GR')
                    };
                    if (section === 'billing' && typeof dispatch.setBillingAddress === 'function') {
                        dispatch.setBillingAddress(payload);
                    } else if (typeof dispatch.setShippingAddress === 'function') {
                        dispatch.setShippingAddress(payload);
                        // Mirror to billing when shipping is the only
                        // address surface (single-address checkout).
                        if (typeof dispatch.setBillingAddress === 'function') {
                            dispatch.setBillingAddress(payload);
                        }
                    }
                    return;
                }
            } catch (e) {
                if (window.console) { console.warn('[AIEO geo] dispatch failed', e); }
            }
        }
        // Classic-checkout fallback — set input values + fire change
        // so WC's update_checkout listener picks them up.
        var prefix = section === 'billing' ? 'billing' : 'shipping';
        var pairs = {};
        pairs[prefix + '_address_1'] = resolved.address_1 || '';
        pairs[prefix + '_city']      = resolved.city || '';
        pairs[prefix + '_state']     = resolved.state || '';
        pairs[prefix + '_postcode']  = resolved.postcode || '';
        pairs[prefix + '_country']   = resolved.country || (cfg.country || 'GR');
        Object.keys(pairs).forEach(function (id) {
            var el = document.getElementById(id);
            if (!el) { return; }
            el.value = pairs[id];
            el.dispatchEvent(new Event('change', { bubbles: true }));
        });
    }

    function handleGeoClick(btn) {
        if (!('geolocation' in navigator)) {
            flashError(btn, t('unsupported', __('Geolocation not supported by this browser', 'ai-eshop-optimizer')));
            return;
        }
        setBusy(btn, true);
        navigator.geolocation.getCurrentPosition(
            function (pos) {
                var lat = pos.coords.latitude;
                var lng = pos.coords.longitude;
                ensureGeocoder().then(function (geocoder) {
                    return geocoder.geocode({
                        location: { lat: lat, lng: lng },
                        language: cfg.language || 'el'
                    });
                }).then(function (resp) {
                    var results = (resp && resp.results) || [];
                    if (!results.length) {
                        throw new Error(t('noResults', __('No address found for these coordinates', 'ai-eshop-optimizer')));
                    }
                    // Prefer a `street_address`/`premise` result when
                    // available; Google sorts by precedence already, so
                    // results[0] is usually right.
                    var pick = null;
                    for (var i = 0; i < results.length; i++) {
                        var types = results[i].types || [];
                        if (types.indexOf('street_address') !== -1 || types.indexOf('premise') !== -1) {
                            pick = results[i];
                            break;
                        }
                    }
                    if (!pick) { pick = results[0]; }
                    var components = pick.address_components || [];
                    var formatted  = pick.formatted_address || '';
                    var resolved = googleComponentsToAddress(components, formatted);
                    applyResolvedAddress(resolved, btn.getAttribute('data-aieo-section'));
                    setBusy(btn, false);
                }).catch(function (err) {
                    setBusy(btn, false);
                    flashError(btn, (err && err.message) || t('reverseGeocodeFail', __('Reverse geocoding failed', 'ai-eshop-optimizer')));
                });
            },
            function (err) {
                setBusy(btn, false);
                var msg = t('genericError', __('Could not get your location', 'ai-eshop-optimizer'));
                if (err && err.code === 1) { msg = t('permissionDenied', __('Permission denied for location access', 'ai-eshop-optimizer')); }
                else if (err && err.code === 3) { msg = t('timeout', __('Location request timed out', 'ai-eshop-optimizer')); }
                flashError(btn, msg);
            },
            { enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 }
        );
    }

    function scanAndInject() {
        findAddress1Inputs().forEach(injectGeoButton);
    }

    // Block checkout re-renders on store updates — observe the body so
    // we re-mount when the field is destroyed/recreated.
    function startObserver() {
        if (typeof MutationObserver === 'undefined') { return; }
        var mo = new MutationObserver(function () { scanAndInject(); });
        mo.observe(document.body, { childList: true, subtree: true });
    }

    // Only mount the geolocation button when the operator opted in.
    // The button itself is inert until clicked — `navigator.geolocation`
    // and Google's geocoding library are NOT touched on page load.
    // NB: wp_localize_script stringifies scalars, so cfg.geolocationEnabled
    // is "0"/"1", and the string "0" is TRUTHY in JS — coerce with + so an
    // unchecked ("Use my location" off) setting actually suppresses the button.
    if (+cfg.geolocationEnabled) {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', function () {
                scanAndInject();
                startObserver();
            });
        } else {
            scanAndInject();
            startObserver();
        }
    }
})();
