Customizable Bazaar Filler

On click, auto-fills bazaar item quantities and prices based on your preferences

// ==UserScript==
// @name         Customizable Bazaar Filler
// @namespace    http://tampermonkey.net/
// @version      1.56
// @description  On click, auto-fills bazaar item quantities and prices based on your preferences
// @match        https://www.torn.com/bazaar.php*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @connect      tornpal.com
// ==/UserScript==

(function() {
    'use strict';

    const styleBlock = `
.item-toggle {
    width: 18px;
    height: 18px;
    border-radius: 4px;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    outline: none;
}
.item-toggle::after {
    content: '\\2713';
    position: absolute;
    font-size: 14px;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: none;
}
.item-toggle:checked::after {
    display: block;
}

body:not(.dark-mode) .item-toggle {
    border: 1px solid #ccc;
    background: #fff;
}
body:not(.dark-mode) .item-toggle:checked {
    background: #007bff;
}
body:not(.dark-mode) .item-toggle:checked::after {
    color: #fff;
}

body.dark-mode .item-toggle {
    border: 1px solid #4e535a;
    background: #2f3237;
}
body.dark-mode .item-toggle:checked {
    background: #4e535a;
}
body.dark-mode .item-toggle:checked::after {
    color: #fff;
}

.checkbox-wrapper {
    position: absolute;
    top: 50%;
    right: 8px;
    width: 30px;
    height: 30px;
    transform: translateY(-50%);
    cursor: pointer;
}
.checkbox-wrapper input.item-toggle {
    position: absolute;
    top: 6px;
    left: 6px;
}

.settings-modal-overlay {
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background: rgba(0,0,0,0.5);
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
}
.settings-modal {
    background: #fff;
    padding: 20px;
    border-radius: 8px;
    min-width: 300px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.3);
    color: #000;
}
.settings-modal h2 {
    margin-top: 0;
}
.settings-modal label {
    display: block;
    margin: 10px 0 5px;
}
.settings-modal input, .settings-modal select {
    width: 100%;
    padding: 5px;
    box-sizing: border-box;
}
.settings-modal button {
    margin-top: 15px;
    padding: 5px 10px;
}
.settings-modal div[style*="text-align:right"] {
    text-align: right;
}
body.dark-mode .settings-modal {
    background: #2f3237;
    color: #fff;
    box-shadow: 0 2px 10px rgba(0,0,0,0.7);
}
body.dark-mode .settings-modal input,
body.dark-mode .settings-modal select {
    background: #3c3f41;
    color: #fff;
    border: 1px solid #555;
}
body.dark-mode .settings-modal button {
    background: #555;
    color: #fff;
    border: none;
}

/* Black Friday mode styling */
.black-friday-active {
    color: #28a745 !important;
}
.black-friday-active .black-friday-icon {
    color: #28a745 !important;
    fill: #28a745 !important;
}
.black-friday-icon {
    color: inherit;
    fill: currentColor;
}
    `;
    $('<style>').prop('type', 'text/css').html(styleBlock).appendTo('head');

    let apiKey = GM_getValue("tornApiKey", "");
    let pricingSource = GM_getValue("pricingSource", "Market Value");
    let itemMarketOffset = GM_getValue("itemMarketOffset", -1);
    let itemMarketMarginType = GM_getValue("itemMarketMarginType", "absolute");
    let itemMarketListing = GM_getValue("itemMarketListing", 1);
    let itemMarketClamp = GM_getValue("itemMarketClamp", false);
    let marketMarginOffset = GM_getValue("marketMarginOffset", 0);
    let marketMarginType = GM_getValue("marketMarginType", "absolute");
    let bazaarCalcMethod = GM_getValue("bazaarCalcMethod", "cheapest");
    let bazaarMarginOffset = GM_getValue("bazaarMarginOffset", 0);
    let bazaarMarginType = GM_getValue("bazaarMarginType", "absolute");
    let bazaarClamp = GM_getValue("bazaarClamp", false);
    let blackFridayMode = GM_getValue("blackFridayMode", false);

    let validPages = ["#/add", "#/manage"];
    let currentPage = window.location.hash;

    let itemMarketCache = {};
    let tornPalCache = {};

    function getItemIdByName(itemName) {
        const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
        for (let [id, info] of Object.entries(storedItems)) {
            if (info.name === itemName) return id;
        }
        return null;
    }

    function getPriceColor(listedPrice, marketValue) {
        if (marketValue <= 0) return ""; // Use default text color
        const ratio = listedPrice / marketValue;
        const lowerBound = 0.998;
        const upperBound = 1.002;
        
        // Check if we're in dark mode
        const isDarkMode = document.body.classList.contains('dark-mode');
        
        if (ratio >= lowerBound && ratio <= upperBound) {
            return ""; // Use default text color
        }
        
        if (ratio < lowerBound) {
            // Price is lower than market value (discount) - use red variants
            let diff = lowerBound - ratio;
            let t = Math.min(diff / 0.05, 1.2);
            
            if (isDarkMode) {
                // Lighter red for dark mode
                const r = Math.round(255 - t * (255 - 190));
                const g = Math.round(255 - t * (255 - 70)); 
                const b = Math.round(255 - t * (255 - 70));
                return `rgb(${r},${g},${b})`;
            } else {
                // Darker red for light mode
                const r = Math.round(180 - t * 40);
                const g = Math.round(60 - t * 40);
                const b = Math.round(60 - t * 40);
                return `rgb(${r},${g},${b})`;
            }
        } else {
            // Price is higher than market value (premium) - use green variants
            let diff = ratio - upperBound;
            let t = Math.min(diff / 0.05, 1.2);
            
            if (isDarkMode) {
                // Lighter green for dark mode
                const r = Math.round(255 - t * (255 - 70));
                const g = Math.round(255 - t * (255 - 190));
                const b = Math.round(255 - t * (255 - 70));
                return `rgb(${r},${g},${b})`;
            } else {
                // Darker green for light mode
                const r = Math.round(60 - t * 40);
                const g = Math.round(160 - t * 40);
                const b = Math.round(60 - t * 40);
                return `rgb(${r},${g},${b})`;
            }
        }
    }

    async function fetchItemMarketData(itemId) {
        if (!apiKey) {
            console.error("No API key set for Item Market calls.");
            alert("No API key set. Please set your Torn API key in Bazaar Filler Settings before continuing.");
            return null;
        }
        const now = Date.now();
        if (itemMarketCache[itemId] && (now - itemMarketCache[itemId].time < 30000)) {
            return itemMarketCache[itemId].data;
        }
        const url = `https://api.torn.com/v2/market/${itemId}/itemmarket?comment=wBazaarFiller`;
        try {
            const res = await fetch(url, {
                headers: { 'Authorization': 'ApiKey ' + apiKey }
            });
            const data = await res.json();
            if (data.error) {
                console.error("Item Market API error:", data.error);
                alert("Item Market API error: " + data.error.error);
                return null;
            }
            itemMarketCache[itemId] = { time: now, data };
            return data;
        } catch (err) {
            console.error("Failed fetching Item Market data:", err);
            alert("Failed to fetch Item Market data. Check your API key or try again later.");
            return null;
        }
    }

    async function fetchTornPalData() {
        const now = Date.now();
        if (tornPalCache.data && now - tornPalCache.time < 60000) {
            return tornPalCache.data;
        }
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: "https://tornpal.com/api/v1/markets/allprices?comment=wBazaarFiller",
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        tornPalCache = { time: now, data };
                        resolve(data);
                    } catch (err) {
                        console.error("Parsing error:", err);
                        reject(err);
                    }
                },
                onerror: function(err) {
                    console.error("Failed fetching TornPal data:", err);
                    reject(err);
                }
            });
        });
    }

    function updatePriceFieldColor($priceInput) {
        let $row = $priceInput.closest("li.clearfix");
        let itemName = $row.length ? $row.find(".name-wrap span.t-overflow").text().trim() : "";
        if (!$row.length) {
            $row = $priceInput.closest(".item___jLJcf");
            itemName = $row.length ? $row.find(".desc___VJSNQ b").text().trim() : "";
        }
        if (!itemName) return;
        const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
        const matchedItem = Object.values(storedItems).find(i => i.name === itemName);
        if (!matchedItem || !matchedItem.market_value) return;
        let raw = $priceInput.val().replace(/,/g, "");
        let typedPrice = Number(raw);
        if (isNaN(typedPrice)) {
            $priceInput.css("color", "");
            return;
        }
        $priceInput.css("color", getPriceColor(typedPrice, Number(matchedItem.market_value)));
    }

    function attachPriceFieldObservers() {
        $(".price input").each(function() {
            if ($(this).data("listenerAttached")) return;
            $(this).on("input", function() {
                updatePriceFieldColor($(this));
            });
            $(this).data("listenerAttached", true);
            updatePriceFieldColor($(this));
        });
        $(".price___DoKP7 .input-money-group.success input.input-money").each(function() {
            if ($(this).data("listenerAttached")) return;
            $(this).on("input", function() {
                updatePriceFieldColor($(this));
            });
            $(this).data("listenerAttached", true);
            updatePriceFieldColor($(this));
        });
        $("[class*=bottomMobileMenu___] [class*=priceMobile___] .input-money-group.success input.input-money").each(function() {
            if ($(this).data("listenerAttached")) return;
            $(this).on("input", function() {
                updatePriceFieldColor($(this));
            });
            $(this).data("listenerAttached", true);
            updatePriceFieldColor($(this));
        });
    }

    async function updateAddRow($row, isChecked) {
        const $qtyInput = $row.find(".amount input").first();
        const $priceInput = $row.find(".price input").first();
        const $choiceCheckbox = $row.find("div.amount.choice-container input");
        if (!isChecked) {
            if ($choiceCheckbox.length && $choiceCheckbox.prop("checked")) {
                $choiceCheckbox.click();
            }
            if ($qtyInput.data("orig") !== undefined) {
                $qtyInput.val($qtyInput.data("orig"));
                $qtyInput.removeData("orig");
            } else {
                $qtyInput.val("");
            }
            $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
            if ($priceInput.data("orig") !== undefined) {
                $priceInput.val($priceInput.data("orig"));
                $priceInput.removeData("orig");
                $priceInput.css("color", "");
            } else {
                $priceInput.val("");
            }
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
            return;
        }
        if (!$qtyInput.data("orig")) $qtyInput.data("orig", $qtyInput.val());
        if (!$priceInput.data("orig")) $priceInput.data("orig", $priceInput.val());

        if (blackFridayMode) {
            if ($choiceCheckbox.length) {
                if (!$choiceCheckbox.prop("checked")) { $choiceCheckbox.click(); }
            } else {
                let qty = $row.find(".item-amount.qty").text().trim();
                $qtyInput.val(qty);
                $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
            }
            $priceInput.val("1");
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
            return;
        }

        const itemName = $row.find(".name-wrap span.t-overflow").text().trim();
        const itemId = getItemIdByName(itemName);
        const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
        const matchedItem = Object.values(storedItems).find(i => i.name === itemName);

        if ($choiceCheckbox.length) {
            if (!$choiceCheckbox.prop("checked")) { $choiceCheckbox.click(); }
        } else {
            let qty = $row.find(".item-amount.qty").text().trim();
            $qtyInput.val(qty);
            $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
        }

        if (pricingSource === "Market Value" && matchedItem) {
            let mv = Number(matchedItem.market_value);
            let finalPrice = mv;
            if (marketMarginType === "absolute") {
                finalPrice += marketMarginOffset;
            } else if (marketMarginType === "percentage") {
                finalPrice = Math.round(mv * (1 + marketMarginOffset / 100));
            }
            $priceInput.val(finalPrice.toLocaleString("en-US"));
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
            $priceInput.css("color", getPriceColor(finalPrice, mv));
        }
        else if (pricingSource === "Item Market" && itemId) {
            const data = await fetchItemMarketData(itemId);
            if (!data || !data.itemmarket?.listings?.length) return;
            let listings = data.itemmarket.listings;
            const $checkbox = $row.find(".checkbox-wrapper input.item-toggle").first();
            const listingsText = listings.slice(0, 5)
                .map((x, i) => `${i + 1}) $${x.price.toLocaleString("en-US")} x${x.amount}`)
                .join('\n');
            $checkbox.attr("title", listingsText);
            setTimeout(() => { $checkbox.removeAttr("title"); }, 30000);
            let baseIndex = Math.min(itemMarketListing - 1, listings.length - 1);
            let listingPrice = listings[baseIndex].price;
            let finalPrice;
            if (itemMarketMarginType === "absolute") {
                finalPrice = listingPrice + Number(itemMarketOffset);
            } else if (itemMarketMarginType === "percentage") {
                finalPrice = Math.round(listingPrice * (1 + Number(itemMarketOffset) / 100));
            }
            if (itemMarketClamp && matchedItem && matchedItem.market_value) {
                finalPrice = Math.max(finalPrice, Number(matchedItem.market_value));
            }
            if (!$choiceCheckbox.length) {
                let qty = $row.find(".item-amount.qty").text().trim();
                $qtyInput.val(qty);
                $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
            } else {
                if (!$choiceCheckbox.prop("checked")) { $choiceCheckbox.click(); }
            }
            $priceInput.val(finalPrice.toLocaleString("en-US"));
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
            if (matchedItem && matchedItem.market_value) {
                let marketVal = Number(matchedItem.market_value);
                $priceInput.css("color", getPriceColor(finalPrice, marketVal));
            }
        }
        else if (pricingSource === "Bazaars/TornPal") {
            const data = await fetchTornPalData();
            if (!data || !data.items) return;
            if (!matchedItem) return;
            const tornPalItem = Object.values(data.items).find(item => item.name === itemName);
            if (!tornPalItem) return;
            let basePrice;
            if (bazaarCalcMethod === "cheapest") {
                basePrice = tornPalItem.bazaar_cheapest;
            } else {
                basePrice = tornPalItem.bazaar_weighted_average_5;
            }
            let finalPrice;
            if (bazaarMarginType === "absolute") {
                finalPrice = basePrice + bazaarMarginOffset;
            } else if (bazaarMarginType === "percentage") {
                finalPrice = Math.round(basePrice * (1 + bazaarMarginOffset / 100));
            }
            if (bazaarClamp && matchedItem.market_value) {
                finalPrice = Math.max(finalPrice, Number(matchedItem.market_value));
            }
            $priceInput.val(finalPrice.toLocaleString("en-US"));
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
            $priceInput.css("color", getPriceColor(finalPrice, Number(matchedItem.market_value)));
        }
    }

    async function updateManageRow($row, isChecked) {
        const $priceInput = $row.find(".price___DoKP7 .input-money-group.success input.input-money").first();
        if (!isChecked) {
            if ($priceInput.data("orig") !== undefined) {
                $priceInput.val($priceInput.data("orig"));
                $priceInput.removeData("orig");
                $priceInput.css("color", "");
            } else {
                $priceInput.val("");
            }
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            return;
        }
        if (!$priceInput.data("orig")) $priceInput.data("orig", $priceInput.val());

        if (blackFridayMode) {
            $priceInput.val("1");
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            return;
        }

        const itemName = $row.find(".desc___VJSNQ b").text().trim();
        const itemId = getItemIdByName(itemName);
        const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
        const matchedItem = Object.values(storedItems).find(i => i.name === itemName);

        if (pricingSource === "Market Value" && matchedItem) {
            let mv = Number(matchedItem.market_value);
            let finalPrice = mv;
            if (marketMarginType === "absolute") {
                finalPrice += marketMarginOffset;
            } else if (marketMarginType === "percentage") {
                finalPrice = Math.round(mv * (1 + marketMarginOffset / 100));
            }
            $priceInput.val(finalPrice.toLocaleString("en-US"));
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            $priceInput.css("color", getPriceColor(finalPrice, mv));
        }
        else if (pricingSource === "Item Market" && itemId) {
            const data = await fetchItemMarketData(itemId);
            if (!data || !data.itemmarket?.listings?.length) return;
            let listings = data.itemmarket.listings;
            const $checkbox = $row.find(".checkbox-wrapper input.item-toggle").first();
            const listingsText = listings.slice(0, 5)
                .map((x, i) => `${i + 1}) $${x.price.toLocaleString("en-US")} x${x.amount}`)
                .join('\n');
            $checkbox.attr("title", listingsText);
            setTimeout(() => { $checkbox.removeAttr("title"); }, 30000);
            let baseIndex = Math.min(itemMarketListing - 1, listings.length - 1);
            let listingPrice = listings[baseIndex].price;
            let finalPrice;
            if (itemMarketMarginType === "absolute") {
                finalPrice = listingPrice + Number(itemMarketOffset);
            } else if (itemMarketMarginType === "percentage") {
                finalPrice = Math.round(listingPrice * (1 + Number(itemMarketOffset) / 100));
            }
            if (itemMarketClamp && matchedItem && matchedItem.market_value) {
                finalPrice = Math.max(finalPrice, Number(matchedItem.market_value));
            }
            $priceInput.val(finalPrice.toLocaleString("en-US"));
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            if (matchedItem && matchedItem.market_value) {
                let marketVal = Number(matchedItem.market_value);
                $priceInput.css("color", getPriceColor(finalPrice, marketVal));
            }
        }
        else if (pricingSource === "Bazaars/TornPal") {
            const data = await fetchTornPalData();
            if (!data || !data.items) return;
            if (!matchedItem) return;
            const tornPalItem = Object.values(data.items).find(item => item.name === itemName);
            if (!tornPalItem) return;
            let basePrice;
            if (bazaarCalcMethod === "cheapest") {
                basePrice = tornPalItem.bazaar_cheapest;
            } else {
                basePrice = tornPalItem.bazaar_weighted_average_5;
            }
            let finalPrice;
            if (bazaarMarginType === "absolute") {
                finalPrice = basePrice + bazaarMarginOffset;
            } else if (bazaarMarginType === "percentage") {
                finalPrice = Math.round(basePrice * (1 + bazaarMarginOffset / 100));
            }
            if (bazaarClamp && matchedItem.market_value) {
                finalPrice = Math.max(finalPrice, Number(matchedItem.market_value));
            }
            $priceInput.val(finalPrice.toLocaleString("en-US"));
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            if (matchedItem && matchedItem.market_value) {
                let marketVal = Number(matchedItem.market_value);
                $priceInput.css("color", getPriceColor(finalPrice, marketVal));
            }
        }
    }

    async function updateManageRowMobile($row, isChecked) {
        const $priceInput = $row.find("[class*=bottomMobileMenu___] [class*=priceMobile___] .input-money-group.success input.input-money").first();
        if (!$priceInput.length) {
            console.error("Mobile price field not found.");
            return;
        }
        if (!isChecked) {
            if ($priceInput.data("orig") !== undefined) {
                $priceInput.val($priceInput.data("orig"));
                $priceInput.removeData("orig");
                $priceInput.css("color", "");
            } else {
                $priceInput.val("");
            }
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            return;
        }
        if (!$priceInput.data("orig")) $priceInput.data("orig", $priceInput.val());

        if (blackFridayMode) {
            $priceInput.val("1");
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            return;
        }

        const itemName = $row.find(".desc___VJSNQ b").text().trim();
        const itemId = getItemIdByName(itemName);
        const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
        const matchedItem = Object.values(storedItems).find(i => i.name === itemName);
        if (pricingSource === "Market Value" && matchedItem) {
            let mv = Number(matchedItem.market_value);
            let finalPrice = mv;
            if (marketMarginType === "absolute") {
                finalPrice += marketMarginOffset;
            } else if (marketMarginType === "percentage") {
                finalPrice = Math.round(mv * (1 + marketMarginOffset / 100));
            }
            $priceInput.val(finalPrice.toLocaleString("en-US"));
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            $priceInput.css("color", getPriceColor(finalPrice, mv));
        }
        else if (pricingSource === "Item Market" && itemId) {
            const data = await fetchItemMarketData(itemId);
            if (!data || !data.itemmarket?.listings?.length) return;
            let listings = data.itemmarket.listings;
            let baseIndex = Math.min(itemMarketListing - 1, listings.length - 1);
            let listingPrice = listings[baseIndex].price;
            let finalPrice;
            if (itemMarketMarginType === "absolute") {
                finalPrice = listingPrice + Number(itemMarketOffset);
            } else if (itemMarketMarginType === "percentage") {
                finalPrice = Math.round(listingPrice * (1 + Number(itemMarketOffset) / 100));
            }
            if (itemMarketClamp && matchedItem && matchedItem.market_value) {
                finalPrice = Math.max(finalPrice, Number(matchedItem.market_value));
            }
            $priceInput.val(finalPrice.toLocaleString("en-US"));
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            if (matchedItem && matchedItem.market_value) {
                let marketVal = Number(matchedItem.market_value);
                $priceInput.css("color", getPriceColor(finalPrice, marketVal));
            }
        }
        else if (pricingSource === "Bazaars/TornPal") {
            const data = await fetchTornPalData();
            if (!data || !data.items) return;
            if (!matchedItem) return;
            const tornPalItem = Object.values(data.items).find(item => item.name === itemName);
            if (!tornPalItem) return;
            let basePrice;
            if (bazaarCalcMethod === "cheapest") {
                basePrice = tornPalItem.bazaar_cheapest;
            } else {
                basePrice = tornPalItem.bazaar_weighted_average_5;
            }
            let finalPrice;
            if (bazaarMarginType === "absolute") {
                finalPrice = basePrice + bazaarMarginOffset;
            } else if (bazaarMarginType === "percentage") {
                finalPrice = Math.round(basePrice * (1 + bazaarMarginOffset / 100));
            }
            if (bazaarClamp && matchedItem.market_value) {
                finalPrice = Math.max(finalPrice, Number(matchedItem.market_value));
            }
            $priceInput.val(finalPrice.toLocaleString("en-US"));
            $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
            if (matchedItem && matchedItem.market_value) {
                let marketVal = Number(matchedItem.market_value);
                $priceInput.css("color", getPriceColor(finalPrice, marketVal));
            }
        }
    }

    function openSettingsModal() {
        $('.settings-modal-overlay').remove();
        const $overlay = $('<div class="settings-modal-overlay"></div>');
        const $modal = $(`
            <div class="settings-modal" style="width:400px; max-width:90%; font-family:Arial, sans-serif;">
                <h2 style="margin-bottom:6px;">Bazaar Filler Settings</h2>
                <hr style="border-top:1px solid #ccc; margin:8px 0;">
                <div style="margin-bottom:15px;">
                    <label for="api-key-input" style="font-weight:bold; display:block;">Torn API Key</label>
                    <input id="api-key-input" type="text" placeholder="Enter API key" style="width:100%; padding:6px; box-sizing:border-box;" value="${apiKey || ''}">
                </div>
                <hr style="border-top:1px solid #ccc; margin:8px 0;">
                <div style="margin-bottom:15px;">
                    <label for="pricing-source-select" style="font-weight:bold; display:block;">Pricing Source</label>
                    <select id="pricing-source-select" style="width:100%; padding:6px; box-sizing:border-box;">
                        <option value="Market Value">Market Value</option>
                        <option value="Bazaars/TornPal">Bazaars/TornPal</option>
                        <option value="Item Market">Item Market</option>
                    </select>
                </div>
                <div id="market-value-options" style="display:none; margin-bottom:15px;">
                    <hr style="border-top:1px solid #ccc; margin:8px 0;">
                    <h3 style="margin:0 0 10px 0; font-size:1em; font-weight:bold;">Market Value Options</h3>
                    <div style="margin-bottom:10px;">
                        <label for="market-margin-offset" style="display:block;">Margin (ie: -1 is either $1 less or 1% less depending on margin type)</label>
                        <input id="market-margin-offset" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${marketMarginOffset}">
                    </div>
                    <div style="margin-bottom:10px;">
                        <label for="market-margin-type" style="display:block;">Margin Type</label>
                        <select id="market-margin-type" style="width:100%; padding:6px; box-sizing:border-box;">
                            <option value="absolute">Absolute ($)</option>
                            <option value="percentage">Percentage (%)</option>
                        </select>
                    </div>
                </div>
                <div id="item-market-options" style="display:none; margin-bottom:15px;">
                    <hr style="border-top:1px solid #ccc; margin:8px 0;">
                    <h3 style="margin:0 0 10px 0; font-size:1em; font-weight:bold;">Item Market Options</h3>
                    <div style="margin-bottom:10px;">
                        <label for="item-market-listing" style="display:block;">Listing Index (1 = lowest, 2 = 2nd lowest, etc)</label>
                        <input id="item-market-listing" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${itemMarketListing}">
                    </div>
                    <div style="margin-bottom:10px;">
                        <label for="item-market-offset" style="display:block;">Margin (ie: -1 is either $1 less or 1% less depending on margin type)</label>
                        <input id="item-market-offset" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${itemMarketOffset}">
                    </div>
                    <div style="margin-bottom:10px;">
                        <label for="item-market-margin-type" style="display:block;">Margin Type</label>
                        <select id="item-market-margin-type" style="width:100%; padding:6px; box-sizing:border-box;">
                            <option value="absolute">Absolute ($)</option>
                            <option value="percentage">Percentage (%)</option>
                        </select>
                    </div>
                    <div style="display:inline-flex; align-items:center; margin-bottom:5px;">
                        <input id="item-market-clamp" type="checkbox" style="margin-right:5px;" ${itemMarketClamp ? "checked" : ""}>
                        <label for="item-market-clamp" style="margin:0; cursor:pointer;">Clamp minimum price to Market Value</label>
                    </div>
                </div>
                <div id="tornpal-options" style="display:none; margin-bottom:15px;">
                    <hr style="border-top:1px solid #ccc; margin:8px 0;">
                    <h3 style="margin:0 0 10px 0; font-size:1em; font-weight:bold;">TornPal Options</h3>
                    <div style="margin-bottom:10px;">
                        <label for="tornpal-calc-method" style="display:block;">Calculation Method</label>
                        <select id="tornpal-calc-method" style="width:100%; padding:6px; box-sizing:border-box;">
                            <option value="cheapest">Cheapest</option>
                            <option value="average">Average</option>
                        </select>
                    </div>
                    <div style="margin-bottom:10px;">
                        <label for="tornpal-margin-offset" style="display:block;">Margin (e.g., -1 for $1 less or 1% less)</label>
                        <input id="tornpal-margin-offset" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${bazaarMarginOffset}">
                    </div>
                    <div style="margin-bottom:10px;">
                        <label for="tornpal-margin-type" style="display:block;">Margin Type</label>
                        <select id="tornpal-margin-type" style="width:100%; padding:6px; box-sizing:border-box;">
                            <option value="absolute">Absolute ($)</option>
                            <option value="percentage">Percentage (%)</option>
                        </select>
                    </div>
                    <div style="display:inline-flex; align-items:center; margin-bottom:5px;">
                        <input id="tornpal-clamp" type="checkbox" style="margin-right:5px;" ${bazaarClamp ? "checked" : ""}>
                        <label for="tornpal-clamp" style="margin:0; cursor:pointer;">Clamp minimum price to Market Value</label>
                    </div>
                </div>
                <hr style="border-top:1px solid #ccc; margin:8px 0;">
                <div style="text-align:right;">
                    <button id="settings-save" style="margin-right:8px; padding:6px 10px; cursor:pointer;">Save</button>
                    <button id="settings-cancel" style="padding:6px 10px; cursor:pointer;">Cancel</button>
                </div>
            </div>
        `);
        $overlay.append($modal);
        $('body').append($overlay);
        $('#pricing-source-select').val(pricingSource);
        $('#item-market-margin-type').val(itemMarketMarginType);
        $('#market-margin-type').val(marketMarginType);
        $('#tornpal-calc-method').val(bazaarCalcMethod);
        $('#tornpal-margin-type').val(bazaarMarginType);
        function toggleFields() {
            let src = $('#pricing-source-select').val();
            $('#market-value-options').toggle(src === 'Market Value');
            $('#item-market-options').toggle(src === 'Item Market');
            $('#tornpal-options').toggle(src === 'Bazaars/TornPal');
        }
        $('#pricing-source-select').change(toggleFields);
        toggleFields();
        $('#settings-save').click(function() {
            const oldPricingSource = pricingSource;
            apiKey = $('#api-key-input').val().trim();
            pricingSource = $('#pricing-source-select').val();
            
            if (oldPricingSource !== pricingSource) {
                itemMarketCache = {};
                tornPalCache = {};
            }
            
            if (pricingSource === "Bazaars/TornPal") {
                bazaarCalcMethod = $('#tornpal-calc-method').val();
                bazaarMarginOffset = Number($('#tornpal-margin-offset').val() || 0);
                bazaarMarginType = $('#tornpal-margin-type').val();
                bazaarClamp = $('#tornpal-clamp').is(':checked');
                GM_setValue("bazaarCalcMethod", bazaarCalcMethod);
                GM_setValue("bazaarMarginOffset", bazaarMarginOffset);
                GM_setValue("bazaarMarginType", bazaarMarginType);
                GM_setValue("bazaarClamp", bazaarClamp);
            }
            if (pricingSource === "Market Value") {
                marketMarginOffset = Number($('#market-margin-offset').val() || 0);
                marketMarginType = $('#market-margin-type').val();
                GM_setValue("marketMarginOffset", marketMarginOffset);
                GM_setValue("marketMarginType", marketMarginType);
            }
            if (pricingSource === "Item Market") {
                itemMarketListing = Number($('#item-market-listing').val() || 1);
                itemMarketOffset = Number($('#item-market-offset').val() || -1);
                itemMarketMarginType = $('#item-market-margin-type').val();
                itemMarketClamp = $('#item-market-clamp').is(':checked');
                GM_setValue("itemMarketListing", itemMarketListing);
                GM_setValue("itemMarketOffset", itemMarketOffset);
                GM_setValue("itemMarketMarginType", itemMarketMarginType);
                GM_setValue("itemMarketClamp", itemMarketClamp);
            }
            GM_setValue("tornApiKey", apiKey);
            GM_setValue("pricingSource", pricingSource);
            $overlay.remove();
        });
        $('#settings-cancel').click(() => $overlay.remove());
    }

    function addBlackFridayToggle() {
        if (document.getElementById('black-friday-toggle')) return;
        let linksContainer = document.querySelector('.linksContainer___LiOTN');
        if (!linksContainer) return;

        let link = document.createElement('a');
        link.id = 'black-friday-toggle';
        link.href = '#';
        link.className = 'linkContainer___X16y4 inRow___VfDnd greyLineV___up8VP iconActive___oAum9';
        if (blackFridayMode) {
            link.classList.add('black-friday-active');
        }
        link.target = '_self';
        link.rel = 'noreferrer';

        const iconSpan = document.createElement('span');
        iconSpan.className = 'iconWrapper___x3ZLe iconWrapper___COKJD svgIcon___IwbJV';
        iconSpan.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" class="black-friday-icon" style="color: ${blackFridayMode ? '#28a745' : 'inherit'}; fill: ${blackFridayMode ? '#28a745' : 'currentColor'};">
                <path d="M4 10.781c.148 1.667 1.513 2.85 3.591 3.003V15h1.043v-1.216c2.27-.179 3.678-1.438 3.678-3.3 0-1.59-.947-2.51-2.956-3.028l-.722-.187V3.467c1.122.11 1.879.714 2.07 1.616h1.47c-.166-1.6-1.54-2.748-3.54-2.875V1H7.591v1.233c-1.939.23-3.27 1.472-3.27 3.156 0 1.454.966 2.483 2.661 2.917l.61.162v4.031c-1.149-.17-1.94-.8-2.131-1.718H4zm3.391-3.836c-1.043-.263-1.6-.825-1.6-1.616 0-.944.704-1.641 1.8-1.828v3.495l-.2-.05zm1.591 1.872c1.287.323 1.852.859 1.852 1.769 0 1.097-.826 1.828-2.2 1.939V8.73l.348.086z"/>
            </svg>
        `;
        link.appendChild(iconSpan);

        const textSpan = document.createElement('span');
        textSpan.className = 'linkTitle____NPyM';
        textSpan.textContent = blackFridayMode ? 'Black Friday: ON' : 'Black Friday: OFF';
        link.appendChild(textSpan);

        link.addEventListener('click', function(e) {
            e.preventDefault();
            blackFridayMode = !blackFridayMode;
            GM_setValue("blackFridayMode", blackFridayMode);
            textSpan.textContent = blackFridayMode ? 'Black Friday: ON' : 'Black Friday: OFF';

            // Update SVG color directly
            const svg = this.querySelector('.black-friday-icon');
            if (svg) {
                svg.style.color = blackFridayMode ? '#28a745' : 'inherit';
                svg.style.fill = blackFridayMode ? '#28a745' : 'currentColor';
            }

            if (blackFridayMode) {
                link.classList.add('black-friday-active');
            } else {
                link.classList.remove('black-friday-active');
            }
        });

        let settingsButton = document.getElementById('pricing-source-button');
        if (settingsButton) {
            linksContainer.insertBefore(link, settingsButton);
        } else {
            linksContainer.insertBefore(link, linksContainer.firstChild);
        }
    }

    function addPricingSourceLink() {
        if (document.getElementById('pricing-source-button')) return;
        let linksContainer = document.querySelector('.linksContainer___LiOTN');
        if (!linksContainer) return;
        let link = document.createElement('a');
        link.id = 'pricing-source-button';
        link.href = '#';
        link.className = 'linkContainer___X16y4 inRow___VfDnd greyLineV___up8VP iconActive___oAum9';
        link.target = '_self';
        link.rel = 'noreferrer';
        const iconSpan = document.createElement('span');
        iconSpan.className = 'iconWrapper___x3ZLe iconWrapper___COKJD svgIcon___IwbJV';
        iconSpan.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
              <path d="M8 4.754a3.246 3.246 0 1 1 0 6.492 3.246 3.246 0 0 1 0-6.492zM5.754 8a2.246 2.246 0 1 0 4.492 0 2.246 2.246 0 0 0-4.492 0z"/>
              <path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.433 2.54 2.54l.292-.16a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.433-.902 2.54-2.541l-.16-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.54-2.54l-.292.16a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.416 1.6.42 1.184 1.185l-.16.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.318.094a1.873 1.873 0 0 0-1.116 2.692l.16.292c.416.764-.42 1.6-1.185 1.184l-.291-.16a1.873 1.873 0 0 0-1.116-2.692l-.318-.094c-.835-.246-.835-1.428 0-1.674l.318-.094a1.873 1.873 0 0 0 1.116-2.692l-.16-.292c-.416-.764.42-1.6 1.185-1.184l.292.16a1.873 1.873 0 0 0 2.693-1.115l.094-.318z"/>
            </svg>
        `;
        link.appendChild(iconSpan);
        const textSpan = document.createElement('span');
        textSpan.className = 'linkTitle____NPyM';
        textSpan.textContent = 'Bazaar Filler Settings';
        link.appendChild(textSpan);
        link.addEventListener('click', function(e) {
            e.preventDefault();
            openSettingsModal();
        });
        linksContainer.insertBefore(link, linksContainer.firstChild);
    }

    function addAddPageCheckboxes() {
        $(".items-cont .title-wrap").each(function () {
            if ($(this).find(".checkbox-wrapper").length) return;
            $(this).css("position", "relative");
            const wrapper = $('<div class="checkbox-wrapper"></div>');
            const checkbox = $('<input>', {
                type: "checkbox",
                class: "item-toggle",
                click: async function (e) {
                    e.stopPropagation();
                    if (!GM_getValue("tornApiKey", "")) {
                        alert("No Torn API key set. Please click the 'Bazaar Filler Settings' button to enter your API key.");
                        $(this).prop("checked", false);
                        openSettingsModal();
                        return;
                    }
                    await updateAddRow($(this).closest("li.clearfix"), this.checked);
                }
            });
            wrapper.append(checkbox);
            $(this).append(wrapper);
        });
        $(document).off("dblclick", ".amount input").on("dblclick", ".amount input", function () {
            const $row = $(this).closest("li.clearfix");
            const qty = $row.find(".item-amount.qty").text().trim();
            if (qty) {
                $(this).val(qty);
                $(this)[0].dispatchEvent(new Event("input", { bubbles: true }));
                $(this)[0].dispatchEvent(new Event("keyup", { bubbles: true }));
            }
        });
    }

    function addManagePageCheckboxes() {
        $(".item___jLJcf").each(function() {
            const $desc = $(this).find(".desc___VJSNQ");
            if (!$desc.length || $desc.find(".checkbox-wrapper").length) return;
            $desc.css("position", "relative");
            const wrapper = $('<div class="checkbox-wrapper"></div>');
            const checkbox = $('<input>', {
                type: "checkbox",
                class: "item-toggle",
                click: async function(e) {
                    e.stopPropagation();
                    if (!GM_getValue("tornApiKey", "")) {
                        alert("No Torn API key set. Please click the 'Bazaar Filler Settings' button to enter your API key.");
                        $(this).prop("checked", false);
                        openSettingsModal();
                        return;
                    }
                    const $row = $(this).closest(".item___jLJcf");
                    if (window.innerWidth <= 784) {
                        const $manageBtn = $row.find('button[aria-label="Manage"]').first();
                        if ($manageBtn.length) {
                            if (!$manageBtn.find('span').hasClass('active___OTFsm')) {
                                $manageBtn.click();
                            }
                            setTimeout(async () => {
                                await updateManageRowMobile($row, this.checked);
                            }, 200);
                            return;
                        }
                    }
                    await updateManageRow($row, this.checked);
                }
            });
            wrapper.append(checkbox);
            $desc.append(wrapper);
        });
    }

    const storedItems = localStorage.getItem("tornItems");
    const lastUpdatedTime = GM_getValue("lastUpdatedTime", 0);
    const now = Date.now();
    const oneDayMs = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
    const lastUpdatedDate = new Date(lastUpdatedTime);
    const todayUTC = new Date().toISOString().split('T')[0];
    const lastUpdatedUTC = lastUpdatedDate.toISOString().split('T')[0];

    if (apiKey && (!storedItems || lastUpdatedUTC < todayUTC || (now - lastUpdatedTime) >= oneDayMs)) {
        fetch(`https://api.torn.com/torn/?key=${apiKey}&selections=items&comment=wBazaarFiller`)
            .then(r => r.json())
            .then(data => {
                if (!data.items) {
                    console.error("Failed to fetch Torn items or no items found. Possibly invalid API key or rate limit.");
                    return;
                }
                let filtered = {};
                for (let [id, item] of Object.entries(data.items)) {
                    if (item.tradeable) {
                        filtered[id] = {
                            name: item.name,
                            market_value: item.market_value
                        };
                    }
                }
                localStorage.setItem("tornItems", JSON.stringify(filtered));
                GM_setValue("lastUpdatedTime", now);
            })
            .catch(err => {
                console.error("Error fetching Torn items:", err);
            });
    }

    const domObserver = new MutationObserver(() => {
        if (window.location.hash === "#/add") {
            addAddPageCheckboxes();
        } else if (window.location.hash === "#/manage") {
            addManagePageCheckboxes();
        }
        addPricingSourceLink();
        addBlackFridayToggle();
        attachPriceFieldObservers();
    });
    domObserver.observe(document.body, { childList: true, subtree: true });

    window.addEventListener('hashchange', () => {
        currentPage = window.location.hash;
        if (currentPage === "#/add") {
            addAddPageCheckboxes();
        } else if (currentPage === "#/manage") {
            addManagePageCheckboxes();
        }
        addPricingSourceLink();
        addBlackFridayToggle();
        attachPriceFieldObservers();
    });

    if (currentPage === "#/add") {
        addAddPageCheckboxes();
    } else if (currentPage === "#/manage") {
        addManagePageCheckboxes();
    }
    addPricingSourceLink();
    addBlackFridayToggle();
    attachPriceFieldObservers();

    $(document).on("click", "button.undo___FTgvP", function(e) {
        e.preventDefault();
        $(".item___jLJcf .checkbox-wrapper input.item-toggle:checked").each(function() {
            $(this).prop("checked", false);
            const $row = $(this).closest(".item___jLJcf");
            updateManageRow($row, false);
        });
    });

    $(document).on("click", ".clear-action", function(e) {
        e.preventDefault();
        $("li.clearfix .checkbox-wrapper input.item-toggle:checked").each(function() {
            $(this).prop("checked", false);
            const $row = $(this).closest("li.clearfix");
            updateAddRow($row, false);
        });
    });

    $(document).ready(function() {
        itemMarketCache = {};
        tornPalCache = {};
    });
})();