Bazaar + TE Info PC Version

Shows Bazaar listing on Item Market with TE Data. Includes Item Market Profit Calculator and uses robust loading logic. Optimizations added for input lag and comma formatting.

// ==UserScript==
// @name          Bazaar + TE Info PC Version
// @namespace     https://weav3r.dev/
// @version       2.4.2
// @description   Shows Bazaar listing on Item Market with TE Data. Includes Item Market Profit Calculator and uses robust loading logic. Optimizations added for input lag and comma formatting.
// @author        WTV [3281931]
// @match         https://www.torn.com/*
// @grant         GM_xmlhttpRequest
// @grant         GM_addStyle
// @connect       weav3r.dev
// @connect       tornexchange.com
// @run-at        document-idle
// @license       MIT
// ==/UserScript==

(function() {
    'use strict';

    // Global State Variables
    window._visitedBazaars = new Set();
    window._cachedListings = {};
    window._activeSort = { type: 'price', dir: 'asc' };
    window._marketValueCache = {};
    window._currentMarketNetPrice = 0;

    // --- CSS INJECTION (No functional changes from v2.9.4) ---
    GM_addStyle(`
        /* General Styles (No Change) */
        .bazaar-info-container { border: 1px solid #888; margin: 10px 0; padding: 5px; background: #222; color: #fff; }
        .bazaar-info-header { font-weight: bold; margin-bottom: 5px; display: flex; flex-wrap: nowrap; justify-content: space-between; align-items: center; font-size: 14px; }
        .bazaar-title { flex-grow: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .bazaar-market-value { color: #FFD700; flex-shrink: 0; font-size: 14px; }
        .bazaar-count-info { font-size: 13px; color: #aaa; font-weight: normal; flex-shrink: 0; margin-left: 10px; }
        .best-buyer-line { font-weight: bold; margin-bottom: 5px; color: #FFA500; display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: center; gap: 5px; font-size: 14px; }
        .best-buyer-line .price-display { color: lime; font-weight: bold; white-space: nowrap; font-size: 16px; }
        .best-buyer-line .trader-link { color: #1E90FF; text-decoration: none; font-weight: bold; cursor: pointer; }
        .best-buyer-line .te-listings-link { color: #00BFFF; font-size: 14px; text-decoration: none; white-space: nowrap; margin-left: 5px; font-weight: bold; }
        .best-buyer-line .trader-link:visited { color: #800080; }
        .bazaar-item-id { color: #aaa; font-size: 13px; font-weight: bold; white-space: nowrap; margin-left: auto; }
        .bazaar-control-row { display: flex; flex-direction: column; gap: 8px; margin: 5px 0 8px 0; }
        .bazaar-control-line { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
        .bazaar-price-controls, .bazaar-quantity-controls { display: flex; gap: 4px; align-items: center; flex-shrink: 0; white-space: nowrap; }
        .bazaar-filter-toggle-btn { background: #555; color: white; border: none; padding: 4px 8px; cursor: pointer; font-weight: bold; height: 28px; width: 65px; font-size: 12px; transition: background 0.2s; }
        .bazaar-sort-visuals { display: flex; flex-direction: column; height: 28px; justify-content: center; align-items: center; padding-right: 4px; }
        .bazaar-sort-btn { color: #555; font-weight: bold; cursor: pointer; font-size: 14px; line-height: 1; margin: 0; padding: 0; transition: color 0.2s; }
        .bazaar-filter-inputs-group { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
        .bazaar-filter-group-price, .bazaar-filter-group-quantity { display: none; align-items: center; gap: 4px; flex-shrink: 0; }
        .bazaar-filter-input { width: 70px; padding: 4px; background: #333; border: 1px solid #444; color: white; height: 28px; box-sizing: border-box; font-size: 12px; }
        .bazaar-filter-group-quantity .bazaar-filter-input { width: 60px; }
        .bazaar-apply-btn { background: #28a745; color: white; border: none; padding: 4px 8px; cursor: pointer; font-weight: bold; height: 28px; flex-shrink: 0; font-size: 12px; display: none; }
        .bazaar-reset-all-btn { background: #444; color: white; border: none; padding: 4px 8px; cursor: pointer; font-weight: bold; height: 28px; flex-shrink: 0; font-size: 14px; margin-left: 5px; }

        /* Market Calculator Styles */
        .bazaar-market-calc {
            display: flex; align-items: center; gap: 10px; padding: 5px 0 10px 0;
            border-top: 1px dashed #444;
            margin-top: 10px;
            overflow-x: auto;
            padding-bottom: 5px;
        }
        .bazaar-calc-label { font-weight: bold; color: #ddd; font-size: 14px; white-space: nowrap; }

        /* Arrow Removal */
        .bazaar-calc-input {
            width: 100px; padding: 4px; background: #333; border: 1px solid #444; color: white; height: 28px; box-sizing: border-box; font-size: 12px;
            -moz-appearance: textfield;
        }
        .bazaar-calc-input::-webkit-outer-spin-button,
        .bazaar-calc-input::-webkit-inner-spin-button {
            -webkit-appearance: none;
            margin: 0;
        }

        .bazaar-net-profit { font-weight: bold; color: limegreen; font-size: 14px; white-space: nowrap; }

        /* Spinner Styles (No Change) */
        .bazaar-loading-overlay {
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
            color: #aaa;
            font-size: 16px;
            font-weight: bold;
        }
        .bazaar-loader {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #3498db;
            border-radius: 50%;
            width: 20px;
            height: 20px;
            animation: spin 1s linear infinite;
            margin-right: 10px;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .bazaar-card-container { display: flex; overflow-x: auto; padding: 5px; gap: 5px; }
        .bazaar-card-container::-webkit-scrollbar { height: 8px; }
        .bazaar-card-container::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; }
        .bazaar-card { border: 1px solid #444; background: #222; color: #eee; padding: 10px; margin: 2px; width: 125px; flex-shrink: 0; cursor: pointer; display: flex; flex-direction: column; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 15px; transition: transform 0.2s, border 0.2s, background 0.2s; position: relative; gap: 3px; }
        .bazaar-card:not(.is-best-buyer):hover { border-color: #555 !important; background: #2a2a2a !important; }
        .bazaar-card.is-best-buyer { border: 2px solid #28a745 !important; background: #333 !important; }
        .bazaar-card.is-best-buyer:hover { background: #3a3a3a !important; }
        .bazaar-card a { font-weight: bold; text-decoration: none; cursor: pointer; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .bazaar-card a:link { color: #1E90FF; }
        .bazaar-card a:visited { color: #800080; }
        .bazaar-card a:hover { text-decoration: underline; }
        .bazaar-card .price-info { font-size: 14px; white-space: nowrap; }
        .bazaar-card .qty-diff-info { font-size: 14px; display: flex; justify-content: space-between; align-items: baseline; line-height: 1; margin-bottom: 0; }
        .bazaar-card .diff-text-positive { color: red; font-weight: bold; }
        .bazaar-card .diff-text-negative { color: limegreen; font-weight: bold; }
        .bazaar-card .diff-text-neutral { color: gold; font-weight: bold; }
    `);
    // --- END CSS INJECTION ---

    // --- UTILITY & API FUNCTIONS (No changes) ---

    function fetchApi(url) {
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data && data.status === 'success') {
                            resolve(data);
                        } else {
                            resolve(null);
                        }
                    } catch (e) {
                        resolve(null);
                    }
                },
                onerror: function() {
                    resolve(null);
                }
            });
        });
    }

    function fetchUpvoteCountAndId(userName) {
        return new Promise(resolve => {
            const url = `https://tornexchange.com/prices/${userName}/`;
            GM_xmlhttpRequest({
                method: "GET", url: url,
                onload: function(response) {
                    let upvoteCount = null;
                    let tornXID = null;
                    try {
                        const doc = new DOMParser().parseFromString(response.responseText, "text/html");
                        const upvoteElement = doc.querySelector('#vote-score');
                        if (upvoteElement) {
                             upvoteCount = upvoteElement.textContent.trim().replace(/[^\d]/g, '');
                        }

                        const profileLink = doc.querySelector('a[href*="profiles.php?XID="]');
                        if (profileLink) {
                            const href = profileLink.getAttribute('href');
                            const match = href.match(/XID=(\d+)/);
                            if (match && match[1]) { tornXID = match[1]; }
                        }
                    } catch (e) { /* silent fail */ }
                    resolve({ upvoteCount, tornXID });
                },
                onerror: function() { resolve({ upvoteCount: null, tornXID: null }); }
            });
        });
    }

    async function fetchTornExchangeData(itemId) {
        let marketValue = '';
        let bestBuyer = null;
        let upvoteCount = null;
        let tornXID = null;

        const [tePriceData, bestListingData] = await Promise.all([
            fetchApi(`https://tornexchange.com/api/te_price?item_id=${itemId}`),
            fetchApi(`https://tornexchange.com/api/best_listing?item_id=${itemId}`)
        ]);

        if (tePriceData && tePriceData.data && tePriceData.data.te_price) {
            const price = tePriceData.data.te_price;
            window._marketValueCache[itemId] = price;
            marketValue = `$${Math.round(price).toLocaleString()}`;
        }

        if (bestListingData && bestListingData.data && bestListingData.data.price) {
            bestBuyer = {
                price: bestListingData.data.price,
                trader: bestListingData.data.trader || null
            };

            if (bestBuyer.trader) {
                const profile = await fetchUpvoteCountAndId(bestBuyer.trader);
                upvoteCount = profile.upvoteCount ? parseInt(profile.upvoteCount) : 0;
                tornXID = profile.tornXID;
            }
        }

        return { marketValue, bestBuyer, upvoteCount, tornXID };
    }


    // --- RENDERING FUNCTIONS (MODIFIED HTML TEMPLATE) ---

    // Removes the spinner from the final info container (Bazaar data fetch)
    function removeSpinner(container) {
        const spinner = container.querySelector('.bazaar-loading-overlay');
        if (spinner) {
            spinner.remove();
        }
    }

    // Removes the initial spinner from the wrapper (TE data fetch)
    function removeInitialSpinner(wrapper) {
        const initialSpinner = wrapper.querySelector('[data-loading-phase="initial"]');
        if (initialSpinner) {
            initialSpinner.remove();
        }
    }

    // Shows initial spinner in the wrapper
    function showInitialSpinner(wrapper) {
         const loadingHTML = `
            <div class="bazaar-loading-overlay" data-loading-phase="initial" style="margin-top: 10px; border: 1px solid #888; background: #222;">
                <div class="bazaar-loader"></div>
                Loading TE & Bazaar Data...
            </div>
        `;
        // Insert right at the top of the wrapper
        wrapper.insertAdjacentHTML('afterbegin', loadingHTML);
    }

    function renderMessage(container, isError){
        removeSpinner(container);
        const cardContainer = container.querySelector('.bazaar-card-container');
        if(!cardContainer) return;
        cardContainer.innerHTML = '';
        const msg = document.createElement('div');
        msg.className = 'bazaar-message';
        msg.style.cssText='color:#fff;text-align:center;padding:20px;width:100%;';
        msg.innerHTML = isError ? "API Error<br><span style='font-size:12px;color:#ccc;'>Could not fetch bazaar data. (Weaver API)</span>"
                                 : "No bazaar listings available for this item.";
        cardContainer.appendChild(msg);

        const countSpan = container.querySelector('.bazaar-count-info');
        if(countSpan) countSpan.textContent = '';
    }

    function createBestBuyerFragment(bestBuyer, upvoteCount, tornXID, encodedItemName) {
        const fragment = document.createDocumentFragment();
        const listingsLink = `https://tornexchange.com/listings?model_name_contains=${encodedItemName}&order_by=&status=`;

        const teListingsLink = document.createElement('a');
        teListingsLink.href = listingsLink;
        teListingsLink.target = '_blank';
        teListingsLink.className = 'te-listings-link';
        teListingsLink.textContent = '(TE Listings)';


        if (bestBuyer && bestBuyer.price && bestBuyer.trader) {
            const formattedPrice = `$${Math.round(bestBuyer.price).toLocaleString()}`;
            const traderName = bestBuyer.trader;

            const priceSpan = document.createElement('span');
            priceSpan.style.whiteSpace = 'nowrap';
            priceSpan.innerHTML = `Best Trader: <span class="price-display">${formattedPrice}</span>`;

            const traderSpan = document.createElement('span');
            traderSpan.style.whiteSpace = 'nowrap';
            traderSpan.textContent = 'by ';

            if (tornXID) {
                const profileLink = `https://www.torn.com/profiles.php?XID=${tornXID}`;
                const traderLink = document.createElement('a');
                traderLink.href = profileLink;
                traderLink.target = '_blank';
                traderLink.className = 'trader-link';
                traderLink.textContent = traderName;
                traderLink.setAttribute('rel', 'noopener noreferrer');
                traderLink.setAttribute('data-linkclump', 'false');
                traderLink.addEventListener('click', (e) => e.stopPropagation());

                traderSpan.appendChild(traderLink);
            } else {
                const nameText = document.createElement('span');
                nameText.style.color = '#1E90FF';
                nameText.textContent = traderName;
                traderSpan.appendChild(nameText);
            }

            if (upvoteCount !== null && upvoteCount !== undefined) {
                const upvoteText = document.createTextNode(` (⭐ ${upvoteCount} Upvotes)`);
                traderSpan.appendChild(upvoteText);
            }

            fragment.appendChild(priceSpan);
            fragment.appendChild(traderSpan);
        }

        fragment.appendChild(teListingsLink);

        return fragment;
    }

    function createInfoContainer(itemName, itemId, marketValue, bestBuyer, upvoteCount, tornXID) {
        const container = document.createElement('div');
        container.className = 'bazaar-info-container';
        container.dataset.itemid = itemId;
        if (marketValue) container.dataset.marketValue = marketValue.replace(/\$|,/g, '');
        if (tornXID) container.dataset.bestBuyerId = tornXID;

        const marketText = marketValue ? ` <span class="bazaar-market-value">(Market Value: ${marketValue})</span>` : '';
        const encodedItemName = encodeURIComponent(itemName);

        const bestBuyerFragment = createBestBuyerFragment(bestBuyer, upvoteCount, tornXID, encodedItemName);
        const itemIdHTML = `<span class="bazaar-item-id">Item #: ${itemId}</span>`;

        // 1. Best Buyer Line
        const bestBuyerLine = document.createElement('div');
        bestBuyerLine.className = 'best-buyer-line';
        bestBuyerLine.appendChild(bestBuyerFragment);
        bestBuyerLine.insertAdjacentHTML('beforeend', itemIdHTML);

        // --- Filter and Sort Controls HTML (No changes) ---
        const filterControlsHTML = `
            <div class="bazaar-control-row">
                <div class="bazaar-control-line">
                    <div class="bazaar-price-controls">
                        <button class="bazaar-filter-toggle-btn" data-filter-type="price">Price</button>
                        <div class="bazaar-sort-visuals"><span class="bazaar-sort-btn" data-sort-by="price" data-sort-dir="asc">🔼</span><span class="bazaar-sort-btn" data-sort-by="price" data-sort-dir="desc">🔽</span></div>
                    </div>
                    <div class="bazaar-quantity-controls">
                        <button class="bazaar-filter-toggle-btn" data-filter-type="quantity">Qty</button>
                       <div class="bazaar-sort-visuals"><span class="bazaar-sort-btn" data-sort-by="quantity" data-sort-dir="asc">🔼</span><span class="bazaar-sort-btn" data-sort-by="quantity" data-sort-dir="desc">🔽</span></div>
                    </div>
                    <div class="bazaar-filter-inputs-group">
                        <div class="bazaar-filter-group-price">
                            <input type="number" placeholder="Min Price" class="bazaar-filter-input" data-filter-type="minPrice">
                            <input type="number" placeholder="Max Price" class="bazaar-filter-input" data-filter-type="maxPrice">
                        </div>
                        <div class="bazaar-filter-group-quantity">
                            <input type="number" placeholder="Min Qty" class="bazaar-filter-input" data-filter-type="minQty">
                            <input type="number" placeholder="Max Qty" class="bazaar-filter-input" data-filter-type="maxQty">
                        </div>
                       <button class="bazaar-apply-btn">Apply</button>
                        <button class="bazaar-reset-all-btn" title="Reset Filters and Visited Links">↺</button>
                    </div>
                </div>
            </div>
        `;

        // --- Market Fee Calculation HTML (MODIFIED input type to "text") ---
        const marketCalcHTML = `
            <div class="bazaar-market-calc">
                <span class="bazaar-calc-label" style="font-size: 16px; color: #fff;">Item Market Profit:</span>

                <span class="bazaar-calc-label">Sell Price:</span>
                <input type="text" placeholder="Enter Price" class="bazaar-calc-input" data-calc-type="sellingPrice" pattern="[0-9,]*">

                <span class="bazaar-calc-label">Net after 5% Fee:</span>
                <span class="bazaar-net-profit" data-calc-type="netProfit">$0</span>
            </div>
        `;

         // --- Container Spinner HTML (for Bazaar data fetch only) ---
        const loadingHTML = `
            <div class="bazaar-loading-overlay">
                <div class="bazaar-loader"></div>
                Fetching Bazaar Listings...
            </div>
        `;


        container.innerHTML = `
            <div class="bazaar-info-header">
                <span class="bazaar-title">Bazaar Listings for ${itemName}${marketText}</span>
                <span class="bazaar-count-info"></span>
            </div>
        `;

        container.appendChild(bestBuyerLine);
        container.insertAdjacentHTML('beforeend', filterControlsHTML);
        container.insertAdjacentHTML('beforeend', marketCalcHTML);
        container.insertAdjacentHTML('beforeend', loadingHTML);
        container.insertAdjacentHTML('beforeend', '<div class="bazaar-card-container"></div>');

        const cardContainer = container.querySelector('.bazaar-card-container');
        if (cardContainer) {
            cardContainer.addEventListener("wheel", e => {
                if (e.deltaY !== 0) { e.preventDefault(); cardContainer.scrollLeft += e.deltaY; }
            });
        }

        addFilterListeners(container, itemId);
        addMarketFeeListener(container);
        return container;
    }


    // --- Core Logic (MODIFIED Helper Function) ---

    // New function to handle comma formatting on the text input
    function formatNumberInput(input) {
        // Get the cursor position before cleaning
        const start = input.selectionStart;
        const end = input.selectionEnd;
        const previousValue = input.value;
        const previousLength = previousValue.length;

        // 1. Clean value: Remove all non-digit characters (including old commas)
        const cleanValue = previousValue.replace(/[^\d]/g, '');

        // 2. Format value: Apply formatting using locale settings (creates new commas)
        let formattedValue = '';
        if (cleanValue) {
            // Use Number.toLocaleString to add commas based on user's locale
            formattedValue = Number(cleanValue).toLocaleString('en-US', { maximumFractionDigits: 0 });
        }

        // 3. Update the input field display
        input.value = formattedValue;

        // 4. Adjust the cursor position to keep it stable
        const newLength = formattedValue.length;
        const diff = newLength - previousLength;
        const newCursorPosition = start + diff;

        // Only set the selection if the input is actively focused
        if (document.activeElement === input) {
            input.setSelectionRange(newCursorPosition, newCursorPosition);
        }
    }


    function sortAndFilterListings(itemId, container) {
        let listings = window._cachedListings[itemId];
        if (!listings) return;
        const sortType = window._activeSort.type;
        const sortDir = window._activeSort.dir;
        const minPrice = parseFloat(container.querySelector('[data-filter-type="minPrice"]').value) || null;
        const maxPrice = parseFloat(container.querySelector('[data-filter-type="maxPrice"]').value) || null;
        const minQty = parseInt(container.querySelector('[data-filter-type="minQty"]').value) || null;
        const maxQty = parseInt(container.querySelector('[data-filter-type="maxQty"]').value) || null;
        const isFiltered = minPrice !== null || maxPrice !== null || minQty !== null || maxQty !== null;

        let filteredListings = listings.slice().filter(listing => {
            const price = parseFloat(listing.price.toString().replace(/,/g, ''));
            const qty = parseInt(listing.quantity);
            if (minPrice !== null && price < minPrice) return false;
            if (maxPrice !== null && price > maxPrice) return false;
            if (minQty !== null && qty < minQty) return false;
            if (maxQty !== null && qty > maxQty) return false;
            return true;
        });

        filteredListings.sort((a, b) => {
            let primaryValA, primaryValB;
            if (sortType === 'price') {
                primaryValA = parseFloat(a.price.toString().replace(/,/g, ''));
                primaryValB = parseFloat(b.price.toString().replace(/,/g, ''));
            } else {
                primaryValA = parseInt(a.quantity);
                primaryValB = parseInt(b.quantity);
            }
            let comparison = 0;
            if (sortDir === 'asc') { comparison = primaryValA - primaryValB; }
            else if (sortDir === 'desc') { comparison = primaryValB - primaryValA; }
            if (comparison === 0) {
                 const priceA = parseFloat(a.price.toString().replace(/,/g, ''));
                 const priceB = parseFloat(b.price.toString().replace(/,/g, ''));
                 return priceA - priceB;
            }
            return comparison;
        });

        const marketNum = window._marketValueCache[itemId] || null;
        renderCards(container, filteredListings, marketNum, isFiltered);
    }

    function resetAllState(container, itemId) {
        window._visitedBazaars.clear();
        container.querySelector('[data-filter-type="minPrice"]').value = '';
        container.querySelector('[data-filter-type="maxPrice"]').value = '';
        container.querySelector('[data-filter-type="minQty"]').value = '';
        container.querySelector('[data-filter-type="maxQty"]').value = '';
        sortAndFilterListings(itemId, container);
    }

    function addFilterListeners(container, itemId) {
        const priceFilterGroup = container.querySelector('.bazaar-filter-group-price');
        const quantityFilterGroup = container.querySelector('.bazaar-filter-group-quantity');
        const sortBtns = container.querySelectorAll('.bazaar-sort-btn');
        const filterToggleBtns = container.querySelectorAll('.bazaar-filter-toggle-btn');
        const applyBtn = container.querySelector('.bazaar-apply-btn');
        const resetAllBtn = container.querySelector('.bazaar-reset-all-btn');
        const defaultColor = '#555';
        const activeColor = '#00BFFF';
        const reverseColor = '#dc3545';

        const updateSortVisuals = () => {
             filterToggleBtns.forEach(btn => {
                 const filterType = btn.dataset.filterType;
                 if (filterType === window._activeSort.type) { btn.style.background = activeColor; } else { btn.style.background = defaultColor; }
             });
             sortBtns.forEach(btn => {
                 const type = btn.dataset.sortBy;
                 const dir = btn.dataset.sortDir;
                 let color = defaultColor;
                 if (type === window._activeSort.type && dir === window._activeSort.dir) { color = dir === 'asc' ? activeColor : reverseColor; } else if (type === window._activeSort.type) { color = '#777'; }
                 btn.style.color = color;
             });
        };

        sortBtns.forEach(btn => { btn.addEventListener('click', () => { window._activeSort.type = btn.dataset.sortBy; window._activeSort.dir = btn.dataset.sortDir; updateSortVisuals(); sortAndFilterListings(itemId, container); }); });
        filterToggleBtns.forEach(btn => {
            btn.addEventListener('click', () => {
                const filterType = btn.dataset.filterType;
                const targetGroup = filterType === 'price' ? priceFilterGroup : quantityFilterGroup;
                const wasActive = btn.classList.contains('active-filter');
                priceFilterGroup.style.display = 'none'; quantityFilterGroup.style.display = 'none'; applyBtn.style.display = 'none';
                filterToggleBtns.forEach(b => b.classList.remove('active-filter'));
                if (!wasActive) { targetGroup.style.display = 'flex'; applyBtn.style.display = 'block'; btn.classList.add('active-filter'); }
                updateSortVisuals();
                sortAndFilterListings(itemId, container);
            });
        });

        applyBtn.addEventListener('click', () => { sortAndFilterListings(itemId, container); });
        if (resetAllBtn) { resetAllBtn.addEventListener('click', () => { resetAllState(container, itemId); }); }
        updateSortVisuals();
        const initialPriceToggle = container.querySelector('.bazaar-filter-toggle-btn[data-filter-type="price"]');
        if (initialPriceToggle) {
             initialPriceToggle.style.background = activeColor;
             initialPriceToggle.classList.add('active-filter');
             priceFilterGroup.style.display = 'flex';
             applyBtn.style.display = 'block';
        }
        container.querySelectorAll('.bazaar-filter-input').forEach(input => {
            input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); input.blur(); sortAndFilterListings(itemId, container); } });
            input.addEventListener('blur', () => sortAndFilterListings(itemId, container));
        });
    }

    function addMarketFeeListener(container) {
        const sellingPriceInput = container.querySelector('[data-calc-type="sellingPrice"]');
        const netProfitSpan = container.querySelector('[data-calc-type="netProfit"]');

        function updateProfit() {
            // IMPORTANT: Remove commas from the displayed input value before parsing
            let price = parseInt(sellingPriceInput.value.replace(/[^\d]/g, ''));
            let netProfit = 0;

            // Only proceed if a valid price > 0 is input
            if (isNaN(price) || price <= 0) {
                netProfitSpan.textContent = '$0';
                netProfitSpan.style.color = 'limegreen';
                window._currentMarketNetPrice = 0;

                // If the calculator is reset/empty, re-render cards to clear margin visuals
                if (window._cachedListings[container.dataset.itemid]) {
                    renderCards(container, window._cachedListings[container.dataset.itemid], window._marketValueCache[container.dataset.itemid] || null, false);
                }
                return;
            }

            netProfit = Math.floor(price * 0.95);
            window._currentMarketNetPrice = netProfit;

            const itemId = container.dataset.itemid;
            const listings = window._cachedListings[itemId];
            let cheapestBazaarPrice = Infinity;

            if (listings && listings.length > 0) {
                 cheapestBazaarPrice = listings.reduce((min, listing) => {
                     const currentPrice = parseFloat(listing.price.toString().replace(/,/g, ''));
                     return currentPrice < min ? currentPrice : min;
                 }, Infinity);
            }

            const formattedProfit = `$${netProfit.toLocaleString()}`;
            netProfitSpan.textContent = formattedProfit;

            if (cheapestBazaarPrice > 0 && cheapestBazaarPrice < netProfit) {
                netProfitSpan.style.color = 'red';
            } else if (netProfit > 0) {
                netProfitSpan.style.color = 'limegreen';
            } else {
                netProfitSpan.style.color = 'limegreen';
            }

            // Re-render cards only after a valid price is committed
            if (window._cachedListings[itemId]) {
                 sortAndFilterListings(itemId, container);
            }
        }

        // --- ADDED INPUT LISTENERS FOR FORMATTING ---
        // Format on every keystroke
        sellingPriceInput.addEventListener('input', () => formatNumberInput(sellingPriceInput));

        // Format on focus loss and trigger calculation
        sellingPriceInput.addEventListener('blur', () => {
            formatNumberInput(sellingPriceInput);
            updateProfit();
        });

        // Explicitly handle the 'Enter' key for a quick calculation trigger (remains the same)
        sellingPriceInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                sellingPriceInput.blur(); // Triggers the 'blur' event, which calls updateProfit
            }
        });

        // Initial trigger for change event listener
        sellingPriceInput.addEventListener('change', updateProfit);

        // Initial run to set profit to $0 (still needed to initialize state)
        updateProfit();
    }

    function renderCards(container, listings, marketNum, isFiltered){
        removeSpinner(container);
        const cardContainer=container.querySelector('.bazaar-card-container');
        if(!cardContainer || !listings) return;
        cardContainer.innerHTML='';
        if(listings.length===0){
            const msg = document.createElement('div');
            msg.style.cssText='color:#fff;text-align:center;padding:20px;width:100%;';
            msg.innerHTML = isFiltered ? "No bazaar listings match the current filters." : "No bazaar listings available for this item.";
            cardContainer.appendChild(msg);
            return;
        }

        const marketNetPrice = window._currentMarketNetPrice || 0;

        const countSpan = container.querySelector('.bazaar-count-info');
        if (countSpan) {
            const totalListings = window._cachedListings[container.dataset.itemid].length;
            if (isFiltered && listings.length < totalListings) { countSpan.innerHTML = `(Displaying ${listings.length} Listings - Filtered)`; countSpan.style.color = '#FFA500'; }
            else if (totalListings > 100) { countSpan.innerHTML = `(Displaying 100+ Listings)`; countSpan.style.color = 'orange'; }
            else if (totalListings > 0) { countSpan.innerHTML = `(Displaying ${totalListings} Listings)`; countSpan.style.color = '#aaa'; }
            else { countSpan.textContent = ''; }
        }

        const bestBuyerId = container.dataset.bestBuyerId;
        listings.forEach(listing=>{
            const card=document.createElement('div');
            card.className = 'bazaar-card';
            card.dataset.playerId = listing.player_id;
            const isVisited=window._visitedBazaars.has(listing.player_id);
            const isBestBuyer = bestBuyerId && listing.player_id == bestBuyerId;
            if (isBestBuyer) card.classList.add('is-best-buyer');

            const bazaarLink = `https://www.torn.com/bazaar.php?userId=${listing.player_id}&highlightItem=${listing.item_id}#/`;
            if(!isVisited){
                card.addEventListener('mouseenter', ()=>card.style.transform='scale(1.03)');
                card.addEventListener('mouseleave', ()=>card.style.transform='scale(1)');
            }

            const priceNum = parseFloat(listing.price.toString().replace(/,/g, ''));
            const formattedPrice = `$${Math.round(priceNum).toLocaleString()}`;
            let diffTextHTML = '';

            if(marketNum){
                const percent = ((priceNum - marketNum)/marketNum*100).toFixed(1);
                let diffClass = 'diff-text-neutral';
                if (percent < -0.5) { diffClass = 'diff-text-negative'; }
                else if (percent > 0.5) { diffClass = 'diff-text-positive'; }
                const sign = percent > 0 ? '+' : '';
                diffTextHTML = `<span class="${diffClass}">${sign}${percent}%</span>`;
            }

            let marginHTML = '';
            // Only show margin calculation if the user has entered a valid sell price (> 0)
            if (marketNetPrice > 0) {
                if (marketNetPrice >= priceNum) {
                    const margin = ((marketNetPrice - priceNum) / marketNetPrice) * 100;
                    const marginClass = margin > 0 ? 'diff-text-negative' : 'diff-text-neutral';
                    marginHTML = `
                        <div class="margin-info" style="font-size: 14px; white-space: nowrap;">
                            <b>Margin:</b> <span class="${marginClass}">${margin.toFixed(2)}%</span>
                        </div>`;
                } else {
                     marginHTML = `<div class="margin-info" style="font-size: 14px; white-space: nowrap;"><b>Margin:</b> <span class="diff-text-positive">Loss</span></div>`;
                }
            }


            card.innerHTML=`
                <a href="${bazaarLink}" target="_blank" data-linkclump="true" class="player-link">
                    ${listing.player_name || 'Unknown'}
                </a>
                <div class="price-info"><b>Price:</b> ${formattedPrice}</div>
                <div class="qty-diff-info">
                    <span style="white-space: nowrap;"><b>Qty:</b> ${listing.quantity}</span>
                    <span style="white: nowrap;">${diffTextHTML}</span>
                </div>
                ${marginHTML}
            `;
            card.addEventListener('click', (e)=>{
                const link = e.currentTarget.querySelector('a:first-child');
                if(listing.player_id && link){
                    window._visitedBazaars.add(listing.player_id);
                    link.style.color='#800080';
                }
            });
            cardContainer.appendChild(card);
        });
    }

    // --- MAIN EXECUTION FLOW (No changes) ---

    async function updateInfoContainer(wrapper,itemId,itemName){
        let infoContainer=document.querySelector(`.bazaar-info-container[data-itemid="${itemId}"]`);

        if(!infoContainer){
            // Initial fetch starts (TE data)
            const { marketValue, bestBuyer, upvoteCount, tornXID } = await fetchTornExchangeData(itemId);

            // Remove the initial spinner once TE data is done
            removeInitialSpinner(wrapper);

            infoContainer = createInfoContainer(itemName, itemId, marketValue, bestBuyer, upvoteCount, tornXID);
            wrapper.insertBefore(infoContainer, wrapper.firstChild);

            fetchBazaarListings(itemId, infoContainer);

        } else {
             if (window._cachedListings[itemId]) {
                 sortAndFilterListings(itemId, infoContainer);
             }
        }
    }

    function fetchBazaarListings(itemId, infoContainer){
        GM_xmlhttpRequest({
            method:"GET",
            url:`https://weav3r.dev/api/marketplace/${itemId}`,
            onload:function(response){
                removeSpinner(infoContainer);
                try{
                    const data = JSON.parse(response.responseText);
                    const listingsReceived = data.listings ? data.listings.length : 0;
                    const countSpan = infoContainer.querySelector('.bazaar-count-info');
                    if (countSpan) {
                          if (listingsReceived > 100) { countSpan.innerHTML = `(Displaying 100+ Listings)`; countSpan.style.color = 'orange'; }
                          else if (listingsReceived > 0) { countSpan.innerHTML = `(Displaying ${listingsReceived} Listings)`; countSpan.style.color = '#aaa'; }
                          else { countSpan.textContent = ''; }
                    }

                    if(!data || !data.listings || listingsReceived === 0){
                        renderMessage(infoContainer,false);
                        return;
                    }

                    const allListings = data.listings.map(l=>({
                        player_name:l.player_name, player_id:l.player_id, quantity:l.quantity, price:l.price, item_id:l.item_id
                    }));

                    window._cachedListings[itemId] = allListings;
                    sortAndFilterListings(itemId, infoContainer);

                } catch(e){
                    console.error(`%c[BazaarScript Error] Failed to process Weaver API response for item ${itemId}:`, 'color: red; font-weight: bold;', e);
                    renderMessage(infoContainer,true);
                }
            },
            onerror:function(error){
                removeSpinner(infoContainer);
                console.error(`%c[BazaarScript Error] GM_xmlhttpRequest failed for item ${itemId}:`, 'color: red; font-weight: bold;', error);
                renderMessage(infoContainer,true);
            }
        });
    }

    function processSellerWrapper(wrapper){
        if(!wrapper || wrapper.dataset.bazaarProcessed) return;
        const itemTile = wrapper.closest('[class*="itemTile"]') || wrapper.previousElementSibling;
        if(!itemTile) return;
        let nameEl = itemTile.querySelector('div[class*="name"]') || itemTile.querySelector('div');
        const btn = itemTile.querySelector('button[aria-controls*="itemInfo"]');
        if(!nameEl || !btn) return;
        const itemName = nameEl.textContent.trim();
        const idParts = btn.getAttribute('aria-controls').split('-');
        const itemId = idParts[idParts.length-1];
        wrapper.dataset.bazaarProcessed='true';

        // Show the initial loading spinner immediately
        showInitialSpinner(wrapper);

        updateInfoContainer(wrapper,itemId,itemName);
    }

    // --- SCRIPT EXECUTION AND MONITORS (No changes) ---

    function processAllSellerWrappers(root = document.body) {
        const sellerWrappers = root.querySelectorAll('[class*="sellerListWrapper"]');
        sellerWrappers.forEach(wrapper => processSellerWrapper(wrapper));
    }

    const observer = new MutationObserver(() => {
        processAllSellerWrappers();
    });

    observer.observe(document.body, { childList: true, subtree: true });

    processAllSellerWrappers();

})();

// --- Bazaar Page Green Highlight (No changes) ---
(function(){
    const params=new URLSearchParams(window.location.search);
    const itemIdToHighlight=params.get('highlightItem');
    if(!itemIdToHighlight) return;
    const observer=new MutationObserver(()=>{
        const imgs=document.querySelectorAll('img');
        imgs.forEach(img=>{
            if(img.src.includes(`/images/items/${itemIdToHighlight}/`)){
                 img.closest('div')?.style.setProperty('outline','3px solid green','important');
                 img.scrollIntoView({behavior:'smooth', block:'center'});
            }
            const itemDetailsContainer = document.querySelector('[aria-labelledby*="itemInfo"]');
            if (itemDetailsContainer) {
                const itemImg = itemDetailsContainer.querySelector(`img[src*="/images/items/${itemIdToHighlight}/"]`);
                if (itemImg) {
                    itemImg.closest('div')?.style.setProperty('outline','3px solid green','important');
                }
            }
        });
    });
    observer.observe(document.body,{childList:true,subtree:true});
})();