Tophness / RealEstate.com.au Enhanced Buy Listings & Filters (Modified)

// ==UserScript==
// @name         RealEstate.com.au Enhanced Buy Listings & Filters (Modified)
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  Adds descriptions, walking time, and address/description keyword filters on realestate.com.au search results.
// @author       Chris Malone
// @license      MIT
// @match        https://www.realestate.com.au/buy/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      nominatim.openstreetmap.org
// @connect      overpass-api.de
// @connect      api.openrouteservice.org
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    let ORS_API_KEY = '';

    let allListingsData = [];
    let domListingsMap = new Map();
    const ADDRESS_FILTER_KEY = 'rea_address_filter_keywords';
    const DESCRIPTION_FILTER_KEY = 'rea_description_filter_keywords';
    const WALKING_TIME_ENABLED_KEY = 'rea_walking_time_enabled';

    const DEFAULT_ADDRESS_KEYWORDS = "";
    const DEFAULT_DESCRIPTION_KEYWORDS = "";

    GM_addStyle(`
        .surrounding-results-title {
            display: none;
        }
        .rea-custom-description {
            font-size: 0.9em;
            color: #555;
            margin-top: 8px;
            padding-top: 8px;
            border-top: 1px dashed #eee;
            /* max-height: 100px; */
            /* overflow-y: auto; */
            line-height: 1.4;
        }
        .rea-custom-description br {
            margin-bottom: 0.5em;
            display: block;
            content: "";
        }
        .rea-custom-filter-container {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            align-items: center;
            margin-top: 10px;
            margin-bottom: 10px;
            padding: 10px;
            background-color: #f9f9f9;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .rea-custom-filter-group {
            display: flex;
            flex-direction: column;
            flex-grow: 1;
            min-width: 200px;
        }
        .rea-custom-filter-group label {
            font-weight: bold;
            margin-bottom: 5px;
            font-size: 0.9em;
            color: #333;
        }
        .rea-custom-filter-group input[type="text"] {
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 3px;
            font-size: 0.9em;
        }
        .listing-hidden-by-filter {
            /* display: none !important; */
            opacity: 0.3;
            border-left: 5px solid red;
            padding-left: 5px;
        }
        .rea-checkbox-group {
            padding-top: 20px;
        }
        .rea-checkbox-group label {
            display: flex;
            align-items: center;
            cursor: pointer;
            font-weight: bold;
            font-size: 0.9em;
            color: #333;
        }
        .rea-checkbox-group input[type="checkbox"] {
            margin-right: 8px;
        }
        .residential-card__address-heading {
            display: flex;
            align-items: center;
            flex-wrap: wrap;
        }
        .rea-walking-time-container {
            display: flex;
            align-items: center;
            margin-left: 8px;
        }
        .walking-time-status {
            white-space: nowrap;
        }
        .walking-time-status.checking {
            font-style: italic;
            color: #888;
        }
        .walking-time-status.success {
            font-weight: bold;
            color: #007882;
        }
        .walking-time-status.error {
            font-style: italic;
            color: #999;
            font-size: 0.9em;
        }
        .transport-toggle-btn {
            background: none;
            border: none;
            padding: 0;
            font-family: inherit;
            font-size: 0.85em;
            margin-left: 8px;
            color: #005A64;
            text-decoration: none;
            cursor: pointer;
            border-bottom: 1px dotted #005A64;
            white-space: nowrap;
        }
        .residential-card {
            cursor: revert !important;
            position: revert !important;
        }
    `);

    function promptForApiKey() {
        if (document.getElementById('rea-api-key-modal')) return;

        GM_addStyle(`
            .rea-modal-overlay {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                background-color: rgba(0,0,0,0.6); z-index: 99999;
                display: flex; align-items: center; justify-content: center;
            }
            .rea-modal-content {
                background-color: white; padding: 25px; border-radius: 8px;
                max-width: 550px; box-shadow: 0 4px 15px rgba(0,0,0,0.2);
                font-family: sans-serif; text-align: left;
            }
            .rea-modal-content h3 { margin-top: 0; color: #333; }
            .rea-modal-content p { line-height: 1.6; color: #555; }
            .rea-modal-content a { color: #007882; text-decoration: none; font-weight: bold; }
            .rea-modal-content a:hover { text-decoration: underline; }
            .rea-modal-content input[type="text"] { width: 100%; padding: 10px; margin-top: 10px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; }
            .rea-modal-content button {
                background-color: #007882; color: white; border: none;
                padding: 10px 20px; border-radius: 4px; cursor: pointer;
                margin-top: 15px; font-weight: bold; font-size: 1em;
            }
            .rea-modal-content button:hover { background-color: #005A64; }
        `);

        const overlay = document.createElement('div');
        overlay.id = 'rea-api-key-modal';
        overlay.className = 'rea-modal-overlay';

        const content = document.createElement('div');
        content.className = 'rea-modal-content';
        content.innerHTML = `
            <h3>API Key Required for Walking Times</h3>
            <p>
                To calculate walking times to public transport, this script uses the
                <a href="https://openrouteservice.org" target="_blank" rel="noopener noreferrer">OpenRouteService</a> API.
                <a href="https://openrouteservice.org/dev/#/signup" target="_blank" rel="noopener noreferrer">Click here to sign up</a>.
            </p>
            <p>
                1. Click the link above to sign up.
                <br>
                2. After signing up, click the <b>copy button</b> next to "Basic Key".
                <br>
                3. Paste the key below and click Save. The page will reload.
            </p>
        `;

        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = 'Paste your OpenRouteService API key here';

        const saveButton = document.createElement('button');
        saveButton.textContent = 'Save Key and Reload';

        content.appendChild(input);
        content.appendChild(saveButton);
        overlay.appendChild(content);
        document.body.appendChild(overlay);

        saveButton.onclick = async () => {
            const key = input.value.trim();
            if (key) {
                await GM_setValue('ORS_API_KEY', key);
                ORS_API_KEY = key;
                document.body.removeChild(overlay);
                location.reload();
            } else {
                alert('Please enter a valid API key.');
            }
        };

        overlay.onclick = function(e) {
            if (e.target === overlay) {
                document.body.removeChild(overlay);
            }
        };
    }

    const apiCache = {
        async get(key) {
            try {
                const value = await GM_getValue(key, null);
                return value ? JSON.parse(value) : null;
            } catch { return null; }
        },
        async set(key, value) {
            await GM_setValue(key, JSON.stringify(value));
        }
    };

    function robustRequest(options) {
        return new Promise(resolve => {
            options.onload = (response) => {
                if (response.status >= 200 && response.status < 300) {
                    try { resolve({ data: JSON.parse(response.responseText), error: null }); }
                    catch (e) { resolve({ data: null, error: 'Parse error' }); }
                } else { resolve({ data: null, error: `HTTP ${response.status}` }); }
            };
            options.onerror = () => resolve({ data: null, error: 'Network error' });
            options.ontimeout = () => resolve({ data: null, error: 'Timeout' });
            GM_xmlhttpRequest(options);
        });
    }

    function haversineDistance(lat1, lon1, lat2, lon2) {
        const R = 6371; const dLat = (lat2 - lat1) * Math.PI / 180;
        const dLon = (lon2 - lon1) * Math.PI / 180;
        const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
        return R * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)));
    }

    async function performGeocodeRequest(address) {
        let cleanedAddress = address.replace(/^\s*([a-z0-9-]+\/|unit\s+[a-z0-9-]+)\s*/i, '');
        cleanedAddress = cleanedAddress.replace(/^(\d+)-(\d+)\s/, '$2 ');

        const { data } = await robustRequest({
            method: "GET", url: `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(cleanedAddress)}&format=json&limit=1`
        });
        return (data && data.length > 0) ? { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) } : null;
    }

    async function geocodeAddress(address, statusElement) {
        const cacheKey = 'geocode_' + address;
        const cached = await apiCache.get(cacheKey);
        if (cached && cached.coords) {
            return { result: cached.coords, fromCache: true };
        }

        let coords = await performGeocodeRequest(address);

        if (!coords) {
            if (statusElement) statusElement.textContent = 'Retrying...';
            await new Promise(res => setTimeout(res, 5000)); // Wait 5 seconds before retry
            coords = await performGeocodeRequest(address);
        }

        if (coords) {
            await apiCache.set(cacheKey, { coords });
        }

        return { result: coords, fromCache: false };
    }


    async function findNearestStop(mode, lat, lon) {
        const cacheKey = `${mode}_${lat.toFixed(4)}_${lon.toFixed(4)}`;
        const cached = await apiCache.get(cacheKey);
        if (cached && cached.stop) return { result: cached.stop, fromCache: true };

        const query = mode === 'train'
            ? `[out:json];node(around:5000,${lat},${lon})[railway=station];out;`
            : `[out:json];(node(around:2000,${lat},${lon})[highway=bus_stop];node(around:2000,${lat},${lon})[amenity=bus_station];);out;`;

        const { data } = await robustRequest({
            method: "GET", url: `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(query)}`
        });

        if (!data || !data.elements || data.elements.length === 0) {
            return { result: null, fromCache: false };
        }
        const closest = data.elements.reduce((closest, current) => {
            const dist = haversineDistance(lat, lon, current.lat, current.lon);
            return (dist < closest.distance) ? { stop: current, distance: dist } : closest;
        }, { stop: null, distance: Infinity });

        if(closest.stop) await apiCache.set(cacheKey, { stop: closest.stop });
        return { result: closest.stop, fromCache: false };
    }

    async function getWalkingTime(mode, startCoords, stopCoords) {
        const cacheKey = `walk_${mode}_${startCoords.lat.toFixed(4)}_${startCoords.lon.toFixed(4)}_to_${stopCoords.lat.toFixed(4)}_${stopCoords.lon.toFixed(4)}`;
        const cached = await apiCache.get(cacheKey);
        if (cached && typeof cached.time === 'number') return { result: cached.time, fromCache: true };

        if (!ORS_API_KEY) {
            console.error("[REA Enhancer] Missing API Key for OpenRouteService. The walking time feature is disabled until a key is provided.");
            return { result: null, fromCache: false };
        }

        const { data } = await robustRequest({
            method: "POST", url: "https://api.openrouteservice.org/v2/directions/foot-walking/json",
            headers: { "Authorization": ORS_API_KEY, "Content-Type": "application/json" },
            data: JSON.stringify({ "coordinates": [[startCoords.lon, startCoords.lat], [stopCoords.lon, stopCoords.lat]] })
        });

        const time = (data && typeof data.routes?.[0]?.summary?.duration === 'number')
            ? Math.round(data.routes[0].summary.duration / 60)
            : null;

        if (typeof time === 'number') await apiCache.set(cacheKey, { time });
        return { result: time, fromCache: false };
    }

    async function updateWalkingTime(listingData, addressEl, mode) {
        const headingEl = addressEl.parentElement;
        if (!headingEl) return;

        headingEl.querySelector('.rea-walking-time-container')?.remove();
        if (!addressEl.dataset.originalText) addressEl.dataset.originalText = addressEl.textContent;
        addressEl.textContent = addressEl.dataset.originalText;

        const container = document.createElement('span');
        container.className = 'rea-walking-time-container';
        const statusElement = document.createElement('span');
        statusElement.className = 'walking-time-status checking';
        statusElement.textContent = `Checking ${mode}...`;
        container.appendChild(statusElement);
        headingEl.appendChild(container);

        const fullAddress = listingData.address?.display?.fullAddress;
        if (!fullAddress) { statusElement.textContent = 'Address unavailable'; statusElement.className = 'walking-time-status error'; return; }

        const throttleIfNeeded = (fromCache) => !fromCache ? new Promise(res => setTimeout(res, 300)) : Promise.resolve();

        const { result: coords, fromCache: cCache } = await geocodeAddress(fullAddress, statusElement);
        await throttleIfNeeded(cCache);
        if (!coords) { statusElement.textContent = 'Could not locate address'; statusElement.className = 'walking-time-status error'; return; }

        const { result: stop, fromCache: sCache } = await findNearestStop(mode, coords.lat, coords.lon);
        await throttleIfNeeded(sCache);
        if (!stop) { statusElement.textContent = `No nearby ${mode} stop found`; statusElement.className = 'walking-time-status error'; return; }

        const { result: time, fromCache: tCache } = await getWalkingTime(mode, coords, { lat: stop.lat, lon: stop.lon });
        await throttleIfNeeded(tCache);
        if (typeof time !== 'number') { statusElement.textContent = 'Walking time unavailable'; statusElement.className = 'walking-time-status error'; return; }

        const stopName = stop.tags?.name || `${mode} stop`;
        const icon = mode === 'train' ? '🚆' : '🚌';
        statusElement.textContent = `🚶 ${time} min to ${icon} ${stopName}`;
        statusElement.className = 'walking-time-status success';

        const otherMode = mode === 'train' ? 'bus' : 'train';
        const otherIcon = otherMode === 'train' ? '🚆' : '🚌';
        const toggleButton = document.createElement('button');
        toggleButton.className = 'transport-toggle-btn';
        toggleButton.textContent = `(Switch to ${otherIcon})`;
        toggleButton.onclick = (e) => {
            e.preventDefault(); e.stopPropagation();
            updateWalkingTime(listingData, addressEl, otherMode);
        };
        container.appendChild(toggleButton);
    }


    function waitForElement(selector, callback, timeout = 30000, interval = 100) {
        let elapsedTime = 0;
        const timer = setInterval(() => {
            const element = document.querySelector(selector);
            if (element) {
                clearInterval(timer);
                callback(element);
            }
            elapsedTime += interval;
            if (elapsedTime >= timeout) {
                clearInterval(timer);
                console.warn(`[REA Enhancer] Element "${selector}" not found after ${timeout/1000}s.`);
            }
        }, interval);
    }

    function parsePriceFromDisplay(priceDisplay) {
        if (!priceDisplay || typeof priceDisplay !== 'string') {
            return null;
        }
        const match = priceDisplay.match(/\$?(\d{1,3}(?:,\d{3})*|\d+)/);
        if (match && match[1]) {
            return parseInt(match[1].replace(/,/g, ''), 10);
        }
        return null;
    }

    let SCRIPT_CURRENT_SORT_KEY = (() => {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('activeSort') || 'price-asc';
    })();

    function getCurrentlySelectedSortKey() {
        let selectedSort = 'price-asc';

        const largeScreenSortInput = document.querySelector('input[name="filters-sort-types"]');
        if (largeScreenSortInput && largeScreenSortInput.value) {
            selectedSort = largeScreenSortInput.value;
        } else {
            const smallScreenSelect = document.getElementById('small-screen-sort-type-filter');
            if (smallScreenSelect && smallScreenSelect.value) {
                selectedSort = smallScreenSelect.value;
            } else {
                console.warn("[REA Enhancer] Could not determine current sort key from UI. Using default/previous:", SCRIPT_CURRENT_SORT_KEY);
                return SCRIPT_CURRENT_SORT_KEY;
            }
        }
        SCRIPT_CURRENT_SORT_KEY = selectedSort;
        return selectedSort;
    }

    function sortAllListingsDataGlobally() {
        if (!allListingsData || allListingsData.length === 0) {
            console.warn("[REA Enhancer] No allListingsData to sort.");
            return;
        }

        const sortKey = getCurrentlySelectedSortKey();
        console.log(`[REA Enhancer] Sorting allListingsData by: ${sortKey}`);

        allListingsData.sort((a, b) => {
            if (!a || !b) return 0;

            switch (sortKey) {
                case 'price-asc': {
                    const priceA = parsePriceFromDisplay(a.price?.display);
                    const priceB = parsePriceFromDisplay(b.price?.display);
                    if (priceA === null && priceB === null) return 0;
                    if (priceA === null) return 1;
                    if (priceB === null) return -1;
                    return priceA - priceB;
                }
                case 'price-desc': {
                    const priceA = parsePriceFromDisplay(a.price?.display);
                    const priceB = parsePriceFromDisplay(b.price?.display);
                    if (priceA === null && priceB === null) return 0;
                    if (priceA === null) return 1;
                    if (priceB === null) return -1;
                    return priceB - priceA;
                }
                default:
                    return 0;
            }
        });
    }

    function reorderListingElementsInDOM() {
        if (!allListingsData || allListingsData.length === 0) {
            console.warn("[REA Enhancer] No sorted data available to reorder DOM elements.");
            return;
        }

        const listContainerParent = document.querySelector('.results-list');
        const exactUl = document.querySelector('.tiered-results--exact');
        const surroundingUl = document.querySelector('.tiered-results--surrounding');
        const resultsCardListUl = document.querySelector('.results-card-list');

        let primaryUlForReappending = exactUl || resultsCardListUl || surroundingUl;

        if (!primaryUlForReappending && listContainerParent) {
            console.warn("[REA Enhancer] Target ULs not found, creating a new one in .results-list");
            primaryUlForReappending = document.createElement('ul');
            primaryUlForReappending.className = 'tiered-results tiered-results--exact rea-custom-sorted-list'; // Give it a class
            listContainerParent.appendChild(primaryUlForReappending);
        } else if (!primaryUlForReappending) {
            console.error("[REA Enhancer] No suitable UL container found to append sorted listings.");
            return;
        }

        const liElementsMap = new Map();
        document.querySelectorAll('.tiered-results--exact > li, .tiered-results--surrounding > li, .results-card-wrapper > li, .results-card-list > li').forEach(li => {
            const listingData = li._listingData || domListingsMap.get(li);
            if (listingData && listingData.id) {
                liElementsMap.set(listingData.id, li);
                li.remove();
            } else {
                // li.remove();
            }
        });

        if (exactUl) exactUl.innerHTML = '';
        if (surroundingUl) surroundingUl.innerHTML = '';
        if (resultsCardListUl) resultsCardListUl.innerHTML = '';
        if (primaryUlForReappending.className.includes('rea-custom-sorted-list')) {
            primaryUlForReappending.innerHTML = '';
        }

        let appendedCount = 0;
        allListingsData.forEach(listingData => {
            const liElement = liElementsMap.get(listingData.id);
            if (liElement) {
                primaryUlForReappending.appendChild(liElement);
                appendedCount++;
            }
        });

        console.log(`[REA Enhancer] DOM reordered: ${appendedCount} listings appended to primary UL.`);
    }

    function extractRealEstateListings(htmlContent) {
        try {
            let scriptTextContent = null;
            const scriptStartMarker = "window.ArgonautExchange=";
            const scriptTagOpen = "<script>";
            const scriptTagClose = "</script>";

            let scriptStartIndex = htmlContent.indexOf(scriptStartMarker);
            if (scriptStartIndex === -1) {
                const scriptTagsRegex = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
                let match;
                while ((match = scriptTagsRegex.exec(htmlContent)) !== null) {
                    if (match[1] && match[1].includes(scriptStartMarker)) {
                        scriptTextContent = match[1];
                        break;
                    }
                }
            } else {
                const actualScriptStart = htmlContent.lastIndexOf(scriptTagOpen, scriptStartIndex);
                const scriptEnd = htmlContent.indexOf(scriptTagClose, scriptStartIndex);
                if (actualScriptStart !== -1 && scriptEnd !== -1) {
                    scriptTextContent = htmlContent.substring(actualScriptStart + scriptTagOpen.length, scriptEnd);
                }
            }
            if (!scriptTextContent) {
                console.error("[REA Enhancer] Could not find the target script tag containing 'window.ArgonautExchange'.");
                return [];
            }
            const jsonLikeString = scriptTextContent
                .replace(scriptStartMarker, "")
                .replace(/;\s*$/, "");

            const argonautData = JSON.parse(jsonLikeString);
            const urqlPath1 = argonautData?.["resi-property_listing-experience-web"]?.urqlClientCache;
            const urqlPath2 = argonautData?.["resi-property_buy_listing-experience-web"]?.urqlClientCache;
            const urqlClientCacheString = urqlPath1 || urqlPath2;

            if (!urqlClientCacheString) {
                console.error("[REA Enhancer] Path to 'urqlClientCache' not found in ArgonautExchange data. Checked 'resi-property_listing-experience-web' and 'resi-property_buy_listing-experience-web'.");
                return [];
            }
            const urqlClientCache = JSON.parse(urqlClientCacheString);
            const cacheEntryKey = Object.keys(urqlClientCache)[0];
            const cacheEntry = urqlClientCache[cacheEntryKey];

            if (!cacheEntry || !cacheEntry.data) {
                console.error("[REA Enhancer] Cache entry or its 'data' property not found in urqlClientCache.");
                return [];
            }
            const dataString = cacheEntry.data;
            const searchData = JSON.parse(dataString);

            let rawItems = [];

            if (searchData?.rentSearch?.results) {
                const results = searchData.rentSearch.results;
                if (Array.isArray(results.exact?.items)) {
                    rawItems = rawItems.concat(results.exact.items);
                    console.log(`[REA Enhancer] Found ${results.exact.items.length} items in rentSearch.results.exact.items`);
                }
                if (Array.isArray(results.surrounding?.items)) {
                    rawItems = rawItems.concat(results.surrounding.items);
                    console.log(`[REA Enhancer] Found ${results.surrounding.items.length} items in rentSearch.results.surrounding.items`);
                }
            }
            else if (searchData?.buySearch?.results) {
                const results = searchData.buySearch.results;
                if (Array.isArray(results.exact?.items)) {
                    rawItems = rawItems.concat(results.exact.items);
                    console.log(`[REA Enhancer] Found ${results.exact.items.length} items in buySearch.results.exact.items`);
                }
                if (Array.isArray(results.surrounding?.items)) {
                    rawItems = rawItems.concat(results.surrounding.items);
                    console.log(`[REA Enhancer] Found ${results.surrounding.items.length} items in buySearch.results.surrounding.items`);
                }
            }
            else if (searchData?.projectSearch?.results) {
                const results = searchData.projectSearch.results;
                if (Array.isArray(results.exact?.items)) {
                    rawItems = rawItems.concat(results.exact.items);
                    console.log(`[REA Enhancer] Found ${results.exact.items.length} items in projectSearch.results.exact.items`);
                }
                if (Array.isArray(results.surrounding?.items)) {
                    rawItems = rawItems.concat(results.surrounding.items);
                    console.log(`[REA Enhancer] Found ${results.surrounding.items.length} items in projectSearch.results.surrounding.items`);
                }
            }

            if (rawItems.length === 0) {
                console.error("[REA Enhancer] No listing items found in 'exact' or 'surrounding' paths for rent, buy, or project search results.", searchData);
                return [];
            }

            const extractedListings = rawItems.map(item => item.listing).filter(Boolean); // Filter out any undefined/null listings

            if (extractedListings.length === 0 && rawItems.length > 0) {
                console.warn("[REA Enhancer] Found raw items, but no valid 'listing' property in them.", rawItems);
            }

            console.log(`[REA Enhancer] Extracted ${extractedListings.length} listings from ${rawItems.length} raw items.`);
            return extractedListings;

        } catch (error) {
            console.error("[REA Enhancer] Error during extraction or parsing:", error);
            return [];
        }
    }

    function normalizeListingSize(liElement) {
        const card = liElement.querySelector('.residential-card');
        if (card) {
            card.classList.remove('residential-card--compressed-view');
        }

        const carousel = liElement.querySelector('.property-card-hero');
        if (carousel) {
            carousel.classList.remove('property-card-hero--extra-small');
            carousel.classList.add('property-card-hero--large');
        }
    }

    function addDescriptionsAndLinkData() {
        if (allListingsData.length === 0) {
            console.warn("[REA Enhancer] No listings data to process for adding descriptions.");
            return;
        }

        const listingElements = document.querySelectorAll('.tiered-results--exact > li, .tiered-results--surrounding > li, .results-card-wrapper > li');
        console.log(`[REA Enhancer] Found ${listingElements.length} listing card elements in DOM.`);

        listingElements.forEach(liElement => {
            const linkElement = liElement.querySelector('a.details-link[href^="/property-"]');
            if (!linkElement) return;

            const href = linkElement.getAttribute('href');
            const idMatch = href.match(/-(\d+)$/);
            if (!idMatch) return;

            const listingId = idMatch[1];
            const listingData = allListingsData.find(l => l.id === listingId);

            if (listingData) {
                liElement._listingData = listingData;
                domListingsMap.set(liElement, listingData);

                const contentWrapper = liElement.querySelector('.residential-card__content');
                if (contentWrapper && listingData.description) {
                    let descElement = contentWrapper.querySelector('.rea-custom-description');
                    if (!descElement) {
                        descElement = document.createElement('div');
                        descElement.className = 'rea-custom-description';
                        contentWrapper.appendChild(descElement);
                    }
                    descElement.innerHTML = listingData.description.replace(/\n/g, '<br>');
                }

                if (document.getElementById('reaWalkingTimeCheckbox')?.checked) {
                    const addressEl = liElement.querySelector('.residential-card__address-heading a');
                    if (addressEl) {
                         // Avoid re-adding if it's already there from a previous run
                        if (!addressEl.parentElement.querySelector('.rea-walking-time-container')) {
                            updateWalkingTime(listingData, addressEl, 'train'); // Default to train
                        }
                    }
                }

                updateListingImage(liElement, listingData);
                normalizeListingSize(liElement);
            } else {
                console.warn(`[REA Enhancer] No data found for listing ID ${listingId} from DOM element.`);
            }
        });
         console.log(`[REA Enhancer] Linked ${domListingsMap.size} DOM elements to listing data.`);
    }

    function updateListingImage(liElement, listingData) {
        const floorplan = listingData.media?.floorplans?.[0];
        if (!floorplan?.templatedUrl) return;

        const floorplanUrl = floorplan.templatedUrl.replace('{size}', '2000x1600-resized');

        const thumbnailElement = liElement.querySelector('.residential-card__image img');
        if (thumbnailElement && thumbnailElement.src !== floorplanUrl) {
            thumbnailElement.removeAttribute('srcset');
            thumbnailElement.src = floorplanUrl;
        }

        const sourceElement = liElement.querySelector('.residential-card__image picture source');
        if (sourceElement && sourceElement.srcset !== floorplanUrl) {
            sourceElement.srcset = floorplanUrl;
        }

        const propertyImageDiv = liElement.querySelector('.residential-card__image .property-image');
        if (propertyImageDiv && propertyImageDiv.getAttribute('data-url') !== floorplanUrl) {
            propertyImageDiv.setAttribute('data-url', floorplanUrl);
        }
    }

    function applyFilters() {
        const addressKeywordsInput = document.getElementById('reaAddressFilterInput');
        const descriptionKeywordsInput = document.getElementById('reaDescriptionFilterInput');
        const whitelistInput = document.getElementById('reaAddressWhitelistInput');

        if (!addressKeywordsInput || !descriptionKeywordsInput || !whitelistInput) {
            console.warn("[REA Enhancer] Filter input fields not found. Cannot apply filters.");
            return;
        }

        const addressKeywordsRaw = addressKeywordsInput.value.toLowerCase();
        const descriptionKeywordsRaw = descriptionKeywordsInput.value.toLowerCase();
        const whitelistRaw = whitelistInput.value.toLowerCase();

        const addressFilterTerms = addressKeywordsRaw ? addressKeywordsRaw.split(',').map(k => k.trim()).filter(k => k) : [];
        const descriptionFilterTerms = descriptionKeywordsRaw ? descriptionKeywordsRaw.split(',').map(k => k.trim()).filter(k => k) : [];
        const whitelistTerms = whitelistRaw ? whitelistRaw.split(',').map(k => k.trim()).filter(k => k) : [];

        let visibleCount = 0;
        const listingElements = document.querySelectorAll('.tiered-results--exact > li, .tiered-results--surrounding > li, .results-card-wrapper > li');

        listingElements.forEach(liElement => {
            const listingData = liElement._listingData || domListingsMap.get(liElement);
            let hide = false;

            if (listingData) {
                const fullAddress = (listingData.address?.display?.fullAddress || "").toLowerCase();
                if (whitelistTerms.length > 0 && !whitelistTerms.some(term => fullAddress.includes(term))) {
                    hide = true;
                }
                if (addressFilterTerms.length > 0) {
                    const fullAddress = (listingData.address?.display?.fullAddress || "").toLowerCase();
                    if (addressFilterTerms.some(term => fullAddress.includes(term))) {
                        hide = true;
                    }
                }
                if (!hide && descriptionFilterTerms.length > 0) {
                    const description = (listingData.description || "").toLowerCase();
                    if (descriptionFilterTerms.some(term => description.includes(term))) {
                        hide = true;
                    }
                }
            } else {
                // If no data linked, can't filter, so keep visible or decide a default
                // console.warn("[REA Enhancer] No data found for a listing element during filtering:", liElement);
            }

            if (hide) {
                liElement.classList.add('listing-hidden-by-filter');
                 liElement.style.display = 'none';
            } else {
                liElement.classList.remove('listing-hidden-by-filter');
                liElement.style.display = '';
                visibleCount++;
            }
        });
        console.log(`[REA Enhancer] Filtering complete. ${visibleCount} listings visible.`);

        const resultsCountElement = document.querySelector('.results-count__wrapper .Text__Typography-sc-1103tao-0'); // Example, might need adjustment
        if (resultsCountElement) {
            if (addressFilterTerms.length > 0 || descriptionFilterTerms.length > 0) {
                 if (!resultsCountElement.dataset.originalText) {
                    resultsCountElement.dataset.originalText = resultsCountElement.textContent;
                 }
                 resultsCountElement.textContent = `${visibleCount} listings (filtered)`;
            } else if (resultsCountElement.dataset.originalText) {
                resultsCountElement.textContent = resultsCountElement.dataset.originalText;
            }
        }
    }

    async function createFilterUI(targetElement) {
        const filterContainer = document.createElement('div');
        filterContainer.className = 'rea-custom-filter-container';

        const whitelistGroup = document.createElement('div');
        whitelistGroup.className = 'rea-custom-filter-group';
        const whitelistLabel = document.createElement('label');
        whitelistLabel.setAttribute('for', 'reaAddressWhitelistInput');
        whitelistLabel.textContent = 'Only show if address contains (comma-separated):';
        const whitelistInput = document.createElement('input');
        whitelistInput.type = 'text';
        whitelistInput.id = 'reaAddressWhitelistInput';
        whitelistInput.placeholder = 'e.g., sydney, bondi, melbourne';
        whitelistInput.addEventListener('input', () => {
            applyFilters();
        });
        whitelistGroup.appendChild(whitelistLabel);
        whitelistGroup.appendChild(whitelistInput);
        filterContainer.appendChild(whitelistGroup);

        const addressGroup = document.createElement('div');
        addressGroup.className = 'rea-custom-filter-group';
        const addressLabel = document.createElement('label');
        addressLabel.setAttribute('for', 'reaAddressFilterInput');
        addressLabel.textContent = 'Hide if address contains (comma-separated):';
        const addressInput = document.createElement('input');
        addressInput.type = 'text';
        addressInput.id = 'reaAddressFilterInput';
        addressInput.placeholder = 'e.g., room, unit 10a, level';
        addressInput.value = DEFAULT_ADDRESS_KEYWORDS;
        addressInput.addEventListener('input', () => {
            applyFilters();
        });
        addressGroup.appendChild(addressLabel);
        addressGroup.appendChild(addressInput);

        const descriptionGroup = document.createElement('div');
        descriptionGroup.className = 'rea-custom-filter-group';
        const descriptionLabel = document.createElement('label');
        descriptionLabel.setAttribute('for', 'reaDescriptionFilterInput');
        descriptionLabel.textContent = 'Hide if description contains (comma-separated):';
        const descriptionInput = document.createElement('input');
        descriptionInput.type = 'text';
        descriptionInput.id = 'reaDescriptionFilterInput';
        descriptionInput.placeholder = 'e.g., shared, room for rent';
        descriptionInput.value = DEFAULT_DESCRIPTION_KEYWORDS;
        descriptionInput.addEventListener('input', () => {
            applyFilters();
        });
        descriptionGroup.appendChild(descriptionLabel);
        descriptionGroup.appendChild(descriptionInput);

        filterContainer.appendChild(addressGroup);
        filterContainer.appendChild(descriptionGroup);

        const walkGroup = document.createElement('div');
        walkGroup.className = 'rea-checkbox-group rea-custom-filter-group';
        const walkLabel = document.createElement('label');
        const walkCheck = document.createElement('input');
        walkCheck.type = 'checkbox';
        walkCheck.id = 'reaWalkingTimeCheckbox';
        walkCheck.checked = await GM_getValue(WALKING_TIME_ENABLED_KEY, true);
        walkLabel.append(walkCheck, 'Show walking time to nearest public transport');

        walkCheck.addEventListener('change', async (event) => {
            const isChecked = event.target.checked;
            await GM_setValue(WALKING_TIME_ENABLED_KEY, isChecked);

            if (isChecked && !ORS_API_KEY) {
                promptForApiKey();
            }

            document.querySelectorAll('.tiered-results--exact > li, .tiered-results--surrounding > li, .results-card-wrapper > li').forEach((li) => {
                const addressEl = li.querySelector('.residential-card__address-heading a');
                if (!addressEl) return;
                const container = addressEl.parentElement.querySelector('.rea-walking-time-container');
                if (walkCheck.checked && !container) {
                    if (li._listingData) updateWalkingTime(li._listingData, addressEl, 'train');
                } else if (!walkCheck.checked && container) {
                    container.remove();
                    addressEl.textContent = addressEl.dataset.originalText || '';
                }
            });
        });
        walkGroup.appendChild(walkLabel);
        filterContainer.appendChild(walkGroup);
        // --- End walking time checkbox ---

        if (targetElement.firstChild) {
            targetElement.insertBefore(filterContainer, targetElement.firstChild);
        } else {
            targetElement.appendChild(filterContainer);
        }
         console.log("[REA Enhancer] Filter UI created.");
    }

    async function init() {
        console.log("[REA Enhancer] Initializing script...");
        ORS_API_KEY = await GM_getValue('ORS_API_KEY', '');
        const walkingTimeEnabled = await GM_getValue(WALKING_TIME_ENABLED_KEY, true);

        if (walkingTimeEnabled && !ORS_API_KEY) {
            promptForApiKey();
            return;
        }

        allListingsData = extractRealEstateListings(document.documentElement.outerHTML);

        if (allListingsData.length === 0) {
            console.warn("[REA Enhancer] No listings data extracted. Aborting further processing.");
            const observer = new MutationObserver((mutationsList, obs) => {
                for(const mutation of mutationsList) {
                    if (mutation.type === 'childList') {
                        for (const node of mutation.addedNodes) {
                            if (node.tagName === 'SCRIPT' && node.textContent.includes("window.ArgonautExchange=")) {
                                console.log("[REA Enhancer] ArgonautExchange script detected after initial load. Re-attempting extraction.");
                                obs.disconnect();
                                allListingsData = extractRealEstateListings(document.documentElement.outerHTML);
                                if (allListingsData.length > 0) {
                                    mainLogic();
                                }
                                return;
                            }
                        }
                    }
                }
            });
            observer.observe(document.head, { childList: true, subtree: true });
            setTimeout(async () => {
                 if (allListingsData.length === 0) {
                     console.log("[REA Enhancer] Retrying extraction after a delay.");
                     allListingsData = extractRealEstateListings(document.documentElement.outerHTML);
                     if (allListingsData.length > 0) mainLogic();
                     else console.warn("[REA Enhancer] Still no data after delay.");
                 }
            }, 3000);
            return;
        }
        mainLogic();
    }

    function handleSortUiChange() {
        console.log("[REA Enhancer] Sort UI change detected.");
        setTimeout(() => {
            sortAllListingsDataGlobally();
            reorderListingElementsInDOM();
            applyFilters();
        }, 200);
    }

    function setupSortChangeListeners() {
        const smallScreenSort = document.getElementById('small-screen-sort-type-filter');
        if (smallScreenSort) {
            smallScreenSort.addEventListener('change', handleSortUiChange);
        }

        const largeScreenSortWrapper = document.querySelector('.LargeScreenFilter__LargeScreenWrapper-sc-1u61mdy-0');
        if (largeScreenSortWrapper) {
            const sortDisplayElement = largeScreenSortWrapper.querySelector('.styles__DisplayContainer-sc-l614ls-4 span.sort-control');
            if (sortDisplayElement) {
                const sortObserver = new MutationObserver((mutationsList) => {
                    for (let mutation of mutationsList) {
                        if (mutation.type === 'childList' || mutation.type === 'characterData') {
                            handleSortUiChange();
                            return;
                        }
                    }
                });
                sortObserver.observe(sortDisplayElement, { childList: true, characterData: true, subtree: true });
                console.log("[REA Enhancer] Observer attached to large screen sort display.");
            } else {
                largeScreenSortWrapper.addEventListener('click', (event) => {
                    const target = event.target;
                    if (target.closest('ul[role="listbox"] li') || target.closest('.styles__InputBorder-sc-w8zjqe-1')) {
                        handleSortUiChange();
                    }
                }, true);
                console.log("[REA Enhancer] Click listener attached to large screen sort wrapper (fallback).");
            }
        }
        console.log("[REA Enhancer] Sort change listeners setup.");
    }

    function mainLogic() {
        const filterInsertionPointSelector = '.View__StyledBottomWrapper-sc-5bovee-2';
        const listContainerSelector = '.tiered-results--exact, .tiered-results--surrounding, .results-card-list';

        waitForElement(filterInsertionPointSelector, async (insertionElement) => {
            await createFilterUI(insertionElement.parentElement);

            waitForElement(listContainerSelector, (listElement) => {
                addDescriptionsAndLinkData();
                sortAllListingsDataGlobally();
                reorderListingElementsInDOM();
                applyFilters();
                setupSortChangeListeners();

                const observer = new MutationObserver((mutationsList) => {
                    let needsReProcess = false;
                    for (const mutation of mutationsList) {
                        if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                            const addedListItems = Array.from(mutation.addedNodes).some(node => node.tagName === 'LI');
                            if (addedListItems) {
                                needsReProcess = true;
                                break;
                            }
                        }
                        if (mutation.type === 'attributes' && (mutation.attributeName === 'src' || mutation.attributeName === 'srcset')) {
                            const img = mutation.target;
                            const liElement = img.closest('li');
                            if (liElement && (liElement._listingData || domListingsMap.get(liElement))) {
                                updateListingImage(liElement, liElement._listingData || domListingsMap.get(liElement));
                            }
                        }
                    }
                    if (needsReProcess) {
                        console.log("[REA Enhancer] List items changed, re-processing descriptions and filters.");
                        if (window.location.href !== observer.currentUrl) {
                            console.log("[REA Enhancer] URL changed, attempting to re-extract all data.");
                            allListingsData = extractRealEstateListings(document.documentElement.outerHTML);
                            observer.currentUrl = window.location.href;
                        }

                        addDescriptionsAndLinkData();
                        sortAllListingsDataGlobally();
                        reorderListingElementsInDOM();
                        applyFilters();
                    }
                });
                observer.observe(listElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'srcset'] });
                observer.currentUrl = window.location.href;
            });
        });
    }

    // Start the process
    init();

})();