Torn Trading Assistant

Track purchases, sales, trades, museum sets & profits for Torn City. Syncs with Google Sheets.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Torn Trading Assistant
// @namespace    https://oatshead.dev/torn-trading-assistant
// @version      1.3.5
// @description  Track purchases, sales, trades, museum sets & profits for Torn City. Syncs with Google Sheets.
// @author       Oatshead
// @match        https://www.torn.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @connect      trading-assistant.oatshead.workers.dev
// @connect      api.torn.com
// @connect      script.google.com
// @connect      script.googleusercontent.com
// @icon         https://www.torn.com/images/items/186/large.png
// @run-at       document-idle
// ==/UserScript==

/*
╔══════════════════════════════════════════════════════════════════╗
║               TORN TRADING ASSISTANT — ToS                       ║
╠══════════════════════════════════════════════════════════════════╣
║ Data Storage     │ Only locally (GM storage in browser/app)      ║
║ Data Sharing     │ Only to YOUR Google Sheet (via YOUR web app)  ║
║ Purpose of Use   │ Personal profit/loss tracking & museum sets   ║
║ Key Storage      │ Stored locally / Not shared                   ║
║ Key Access Level │ Limited Access (user → log, money, travel)    ║
╠══════════════════════════════════════════════════════════════════╣
║ This script ONLY uses the Torn API and monitors pages you are   ║
║ currently viewing. It makes NO automated non-API requests to    ║
║ Torn. All data is stored locally in your browser/app.           ║
║ API calls are triggered by YOU navigating pages.                ║
╚══════════════════════════════════════════════════════════════════╝
*/

(function () {
    "use strict";

    // ========================================================================
    // CONFIGURATION & CONSTANTS
    // ========================================================================

    const WORKER_URL = "https://trading-assistant.oatshead.workers.dev";
    const SHARED_SECRET = "k7Hs9Qp2mX4wR8vL1nB6jT3yC0fA5eD";
    const VERSION = "1.0.0";

    // Log type IDs we care about
    const LOG_TYPES = {
        ITEM_MARKET_BUY: 1112,
        ITEM_MARKET_SELL: 1113,
        BAZAAR_BUY: 1225,
        BAZAAR_SELL: 1226,
        ABROAD_BUY: 4201,
        TRADE_ITEMS_OUT: 4445,
        TRADE_ITEMS_IN: 4446,
        TRADE_COMPLETED: 4430,
        TRADE_ACCEPTED: 4431,
        TRADE_MONEY_OUT: 4440,
        TRADE_MONEY_IN: 4441,
        MUSEUM_EXCHANGE: 7000,
        POINTS_BUY: 5010,
        POINTS_SELL: 5011,
        SHOP_BUY: 4200,
        CITY_ITEM_FIND: 7011,
    };

    // All known plushies and flowers
    const MUSEUM_ITEM_IDS = new Set([
        186, 187, 215, 258, 618, 261, 266, 268, 269, 273, 274, 384, 281, 260, 617,
        263, 264, 267, 271, 272, 277, 276, 385, 282,
    ]);

    // Complete item name→id map (loaded from storage or built from API)
    let ITEM_MAP = {};

    // ========================================================================
    // STORAGE HELPERS
    // ========================================================================

    function getSetting(key, defaultVal) {
        const v = GM_getValue("tta_" + key);
        return v !== undefined ? v : defaultVal;
    }

    function setSetting(key, val) {
        GM_setValue("tta_" + key, val);
    }

    function getConfig() {
        return {
            licenseKey: getSetting("licenseKey", ""),
            licenseValid: getSetting("licenseValid", false),
            tornApiKey: getSetting("tornApiKey", ""),
            sheetWebAppUrl: getSetting("sheetWebAppUrl", ""),
            sheetSecret: getSetting("sheetSecret", ""),
            whitelist: JSON.parse(getSetting("whitelist", "[]")),
            lastLogCheck: getSetting("lastLogCheck", 0),
            processedLogs: JSON.parse(getSetting("processedLogs", "{}")),
            panelX: getSetting("panelX", 10),
            panelY: getSetting("panelY", 10),
            panelMinimized: getSetting("panelMinimized", true),
            enabled: getSetting("enabled", true),
            pollInterval: getSetting("pollInterval", 30), // seconds
        };
    }

    function saveConfig(cfg) {
        setSetting("licenseKey", cfg.licenseKey);
        setSetting("licenseValid", cfg.licenseValid);
        setSetting("tornApiKey", cfg.tornApiKey);
        setSetting("sheetWebAppUrl", cfg.sheetWebAppUrl);
        setSetting("sheetSecret", cfg.sheetSecret);
        setSetting("whitelist", JSON.stringify(cfg.whitelist));
        setSetting("lastLogCheck", cfg.lastLogCheck);
        setSetting("processedLogs", JSON.stringify(cfg.processedLogs));
        setSetting("panelX", cfg.panelX);
        setSetting("panelY", cfg.panelY);
        setSetting("panelMinimized", cfg.panelMinimized);
        setSetting("enabled", cfg.enabled);
        setSetting("pollInterval", cfg.pollInterval);
    }

    function markLogProcessed(logId) {
        const cfg = getConfig();
        cfg.processedLogs[logId] = Date.now();
        // Keep only last 5000 entries to prevent storage bloat
        const keys = Object.keys(cfg.processedLogs);
        if (keys.length > 5000) {
            const sorted = keys.sort(
                (a, b) => cfg.processedLogs[a] - cfg.processedLogs[b],
            );
            for (let i = 0; i < keys.length - 5000; i++) {
                delete cfg.processedLogs[sorted[i]];
            }
        }
        setSetting("processedLogs", JSON.stringify(cfg.processedLogs));
    }

    function isLogProcessed(logId) {
        const cfg = getConfig();
        return !!cfg.processedLogs[logId];
    }

    // ========================================================================
    // GM_xmlhttpRequest WRAPPER
    // ========================================================================

    function fetchGM(url, opts = {}) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: opts.method || "GET",
                url: url,
                headers: opts.headers || { "Content-Type": "application/json" },
                data: opts.body || undefined,
                timeout: opts.timeout || 15000,
                onload: function (resp) {
                    try {
                        resolve({
                            status: resp.status,
                            data: JSON.parse(resp.responseText),
                        });
                    } catch (e) {
                        resolve({ status: resp.status, data: resp.responseText });
                    }
                },
                onerror: function (err) {
                    reject(err);
                },
                ontimeout: function () {
                    reject(new Error("Request timed out"));
                },
            });
        });
    }

    // ========================================================================
    // DEBUG — Run from Status tab to diagnose sync issues
    // ========================================================================

    async function debugLogFetch() {
        const cfg = getConfig();
        if (!cfg.tornApiKey) {
            alert("No API key set!");
            return;
        }

        const results = [];

        // ── Test 1: Raw log fetch (no filtering) ──
        results.push("=== TEST 1: Raw log (v1, no filter) ===");
        try {
            const url1 = `https://api.torn.com/user/?key=${cfg.tornApiKey}&selections=log`;
            const resp1 = await fetchGM(url1);
            if (resp1.data && resp1.data.log) {
                const entries = Object.entries(resp1.data.log);
                results.push(`Returned ${entries.length} log entries`);

                // Show all log types found
                const typeMap = {};
                entries.forEach(([id, entry]) => {
                    const t = entry.log;
                    if (!typeMap[t])
                        typeMap[t] = { title: entry.title, count: 0, sample: null };
                    typeMap[t].count++;
                    if (!typeMap[t].sample)
                        typeMap[t].sample = { id, data: entry.data, params: entry.params };
                });

                for (const [type, info] of Object.entries(typeMap)) {
                    results.push(`  Type ${type} (${info.title}): ${info.count}x`);
                    results.push(`    Sample data: ${JSON.stringify(info.sample.data)}`);
                }

                // Check for item market buy specifically
                const marketBuys = entries.filter(([id, e]) => e.log === 1112);
                results.push(`\nItem Market Buy (1112) entries: ${marketBuys.length}`);
                if (marketBuys.length > 0) {
                    results.push(`  Full entry: ${JSON.stringify(marketBuys[0][1])}`);
                }

                // Check for ALL purchase/sale related types
                const relevantTypes = [
                    1112, 1113, 1110, 1111, 1225, 1226, 4200, 4201, 4445, 4446, 4430,
                    7000, 5010, 5011,
                ];
                const relevant = entries.filter(([id, e]) =>
                    relevantTypes.includes(e.log),
                );
                results.push(`\nAll relevant entries: ${relevant.length}`);
                relevant.forEach(([id, entry]) => {
                    results.push(
                        `  [${id}] Type ${entry.log} (${entry.title}): ${JSON.stringify(entry.data)}`,
                    );
                });
            } else if (resp1.data && resp1.data.error) {
                results.push(`API Error: ${JSON.stringify(resp1.data.error)}`);
            } else {
                results.push(
                    `Unexpected response: ${JSON.stringify(resp1.data).substring(0, 500)}`,
                );
            }
        } catch (e) {
            results.push(`Fetch error: ${e.message}`);
        }

        // ── Test 2: Try v2 log endpoint ──
        results.push("\n=== TEST 2: v2 log endpoint ===");
        try {
            const url2 = `https://api.torn.com/v2/user/log?key=${cfg.tornApiKey}`;
            const resp2 = await fetchGM(url2);
            if (resp2.data) {
                results.push(`v2 response keys: ${Object.keys(resp2.data).join(", ")}`);
                results.push(
                    `v2 preview: ${JSON.stringify(resp2.data).substring(0, 1000)}`,
                );
            }
        } catch (e) {
            results.push(`v2 error: ${e.message}`);
        }

        // ── Test 3: Try v2 log with category filter ──
        results.push("\n=== TEST 3: v2 log with cat filter ===");
        try {
            const url3 = `https://api.torn.com/v2/user/log?key=${cfg.tornApiKey}&cat=trades`;
            const resp3 = await fetchGM(url3);
            if (resp3.data) {
                results.push(
                    `v2 trades: ${JSON.stringify(resp3.data).substring(0, 1000)}`,
                );
            }
        } catch (e) {
            results.push(`v2 trades error: ${e.message}`);
        }

        // ── Test 4: Check whitelist status ──
        results.push("\n=== TEST 4: Whitelist check ===");
        results.push(`Whitelist IDs: ${JSON.stringify(cfg.whitelist)}`);
        results.push(
            `Sheep Plushie (186) whitelisted: ${cfg.whitelist.includes(186)}`,
        );
        results.push(
            `Sheep Plushie (186) in museum set: ${MUSEUM_ITEM_IDS.has(186)}`,
        );

        // ── Test 5: Check processed logs ──
        results.push("\n=== TEST 5: Processed log IDs ===");
        const processedCount = Object.keys(cfg.processedLogs).length;
        results.push(`${processedCount} log IDs already processed`);

        // ── Test 6: Check sheet connection ──
        results.push("\n=== TEST 6: Sheet connection ===");
        results.push(`Sheet URL set: ${!!cfg.sheetWebAppUrl}`);
        results.push(`Sheet secret set: ${!!cfg.sheetSecret}`);

        // ── Show results ──
        showDebugDialog(results.join("\n"));
        console.log("[TTA DEBUG]\n" + results.join("\n"));
    }

    function showDebugDialog(text) {
        const existing = document.getElementById("tta-debug-dialog");
        if (existing) existing.remove();

        const dialog = document.createElement("div");
        dialog.id = "tta-debug-dialog";
        dialog.style.cssText = `
      position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
      z-index:9999999;background:#1a1a2e;border:2px solid #ff9800;
      border-radius:12px;padding:20px;color:#e0e0e0;
      max-height:80vh;overflow-y:auto;width:600px;
      box-shadow:0 8px 32px rgba(0,0,0,0.6);font-family:monospace;font-size:11px;
    `;
        dialog.innerHTML = `
      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
        <h3 style="margin:0;color:#ff9800;">🔍 Debug Log</h3>
        <div>
          <button class="tta-btn tta-btn-primary tta-btn-sm" id="tta-debug-copy">📋 Copy</button>
          <button class="tta-btn tta-btn-danger tta-btn-sm" id="tta-debug-close">✕ Close</button>
        </div>
      </div>
      <pre style="white-space:pre-wrap;word-break:break-all;background:#0f0f1a;
        padding:12px;border-radius:6px;max-height:60vh;overflow-y:auto;
        border:1px solid #333;">${text}</pre>
    `;

        document.body.appendChild(dialog);

        document
            .getElementById("tta-debug-close")
            .addEventListener("click", () => dialog.remove());
        document.getElementById("tta-debug-copy").addEventListener("click", () => {
            navigator.clipboard.writeText(text).then(() => {
                document.getElementById("tta-debug-copy").textContent = "✅ Copied!";
            });
        });
    }

    // ========================================================================
    // LICENSE VERIFICATION
    // ========================================================================

    async function verifyLicense(key) {
        try {
            const resp = await fetchGM(WORKER_URL + "/api/verify", {
                method: "POST",
                body: JSON.stringify({ licenseKey: key, sharedSecret: SHARED_SECRET }),
            });
            return resp.data && resp.data.valid === true;
        } catch (e) {
            console.error("[TTA] License verification failed:", e);
            return false;
        }
    }

    // ========================================================================
    // TORN API CALLS (all compliant — user-triggered, uses API only)
    // ========================================================================

    async function fetchTornApi(endpoint, params = "") {
        const cfg = getConfig();
        if (!cfg.tornApiKey) return null;
        const url = `https://api.torn.com/${endpoint}?key=${cfg.tornApiKey}${params}`;
        try {
            const resp = await fetchGM(url);
            if (resp.data && resp.data.error) {
                console.warn("[TTA] Torn API error:", resp.data.error);
                return null;
            }
            return resp.data;
        } catch (e) {
            console.error("[TTA] Torn API fetch failed:", e);
            return null;
        }
    }

    // ========================================================================
    // TORN API CALLS (Split to handle 10-ID limit)
    // ========================================================================

    async function fetchUserLog() {
        // We have 12 types, but API only allows 10 per call.
        // Split into two batches.
        const batch1 = [
            LOG_TYPES.ITEM_MARKET_BUY,
            LOG_TYPES.ITEM_MARKET_SELL,
            LOG_TYPES.BAZAAR_BUY,
            LOG_TYPES.BAZAAR_SELL,
            LOG_TYPES.ABROAD_BUY,
            LOG_TYPES.SHOP_BUY,
            LOG_TYPES.TRADE_ITEMS_OUT,
            LOG_TYPES.TRADE_ITEMS_IN,
            LOG_TYPES.TRADE_COMPLETED,
            LOG_TYPES.TRADE_ACCEPTED, // Added just in case
        ];

        const batch2 = [
            LOG_TYPES.MUSEUM_EXCHANGE,
            LOG_TYPES.POINTS_BUY,
            LOG_TYPES.POINTS_SELL,
            // Add any future types here
        ];

        try {
            // Run both requests in parallel
            const [resp1, resp2] = await Promise.all([
                fetchTornApi("user/", "&selections=log&log=" + batch1.join(",")),
                fetchTornApi("user/", "&selections=log&log=" + batch2.join(",")),
            ]);

            // Combine the 'log' objects from both responses
            const combinedLogs = {};

            if (resp1 && resp1.log) {
                Object.assign(combinedLogs, resp1.log);
            }
            if (resp2 && resp2.log) {
                Object.assign(combinedLogs, resp2.log);
            }

            // Return strictly the combined structure expected by pollLogs
            return { log: combinedLogs };
        } catch (e) {
            console.error("[TTA] Error fetching batched logs:", e);
            return null;
        }
    }

    // ========================================================================
    // ITEM MAP BUILDER
    // ========================================================================

    async function buildItemMap() {
        const cached = getSetting("itemMap", null);
        const cacheTime = getSetting("itemMapTime", 0);

        // Cache for 24 hours
        if (cached && Date.now() - cacheTime < 86400000) {
            ITEM_MAP = JSON.parse(cached);
            return;
        }

        const data = await fetchTornApi("torn/", "&selections=items");
        if (!data || !data.items) return;

        ITEM_MAP = {};
        for (const [id, item] of Object.entries(data.items)) {
            ITEM_MAP[id] = {
                name: item.name,
                type: item.type,
                tradeable: item.tradeable,
            };
        }
        setSetting("itemMap", JSON.stringify(ITEM_MAP));
        setSetting("itemMapTime", Date.now());
    }

    function getItemName(id) {
        return ITEM_MAP[id] ? ITEM_MAP[id].name : "Unknown Item #" + id;
    }
    function getItemType(id) {
        return ITEM_MAP[id] ? ITEM_MAP[id].type : "Unknown";
    }

    function reverseItemLookup(nameStr) {
        if (!nameStr) return 0;
        const lower = nameStr.toLowerCase().trim();
        for (const [id, info] of Object.entries(ITEM_MAP)) {
            if (info.name && info.name.toLowerCase() === lower) {
                return Number(id);
            }
        }
        return 0;
    }

    // ========================================================================
    // LOG PROCESSOR — Convert API logs to transactions
    // ========================================================================

    function processLogEntry(logId, entry, whitelist) {
        const whitelistSet = new Set(whitelist.map((id) => Number(id)));
        const logType = entry.log;
        const data = entry.data || {};
        const ts = entry.timestamp;

        // ── Extract item ID from various API data formats ──
        // Item Market & Bazaar use: data.items = [{ id, uid, qty }]
        // Shop Buy & Abroad Buy use: data.item = 269, data.quantity = 29
        // Trades use: data.item or data.items
        let itemId = 0;
        let qty = 1;

        if (data.items && Array.isArray(data.items) && data.items.length > 0) {
            // Item Market Buy/Sell, Bazaar Buy/Sell format
            itemId = data.items[0].id || 0;
            qty = data.items[0].qty || data.quantity || data.amount || 1;
        } else {
            // Shop Buy, Abroad Buy, Trade format
            let rawItem =
                data.item || data.itemid || data.item_id || data.itemID || 0;
            if (typeof rawItem === "number" && rawItem > 0) {
                itemId = rawItem;
            } else if (typeof rawItem === "string") {
                const parsed = Number(rawItem);
                if (!isNaN(parsed) && parsed > 0) {
                    itemId = parsed;
                } else {
                    itemId = reverseItemLookup(rawItem);
                }
            }
            qty = data.quantity || data.amount || 1;
        }

        const costEach = data.cost_each || data.cost || data.price || 0;
        const costTotal = data.cost_total || data.total || costEach * qty;

        // Skip if not whitelisted (unless it's a museum/points/trade event)
        const isMuseumItem = MUSEUM_ITEM_IDS.has(Number(itemId));
        const isWhitelisted = whitelistSet.has(Number(itemId));
        const isPointsOrMuseum = [
            LOG_TYPES.MUSEUM_EXCHANGE,
            LOG_TYPES.POINTS_BUY,
            LOG_TYPES.POINTS_SELL,
        ].includes(logType);
        const isTrade = [
            LOG_TYPES.TRADE_COMPLETED,
            LOG_TYPES.TRADE_ACCEPTED,
            LOG_TYPES.TRADE_ITEMS_IN,
            LOG_TYPES.TRADE_ITEMS_OUT,
        ].includes(logType);

        if (!isWhitelisted && !isMuseumItem && !isPointsOrMuseum && !isTrade) {
            console.log(
                `[TTA] Skipped log ${logId} type=${logType}: itemId=${itemId}, ` +
                `whitelisted=${isWhitelisted}, museumItem=${isMuseumItem}, ` +
                `pointsOrMuseum=${isPointsOrMuseum}, trade=${isTrade}`,
            );
            return null;
        }

        const itemName = getItemName(itemId);
        const itemType = getItemType(itemId);

        let transaction = null;

        switch (logType) {
            // ── Purchases ──
            case LOG_TYPES.ITEM_MARKET_BUY:
                if (!isWhitelisted && !isMuseumItem) return null;
                // Handle multi-item market buys (each items[] entry)
                // For now we handle the first item; multi-item support can be added later
                transaction = {
                    logId,
                    type: "purchase",
                    timestamp: ts,
                    source: "Item Market",
                    sourceDetail: data.seller ? String(data.seller) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                };
                break;

            case LOG_TYPES.BAZAAR_BUY:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "purchase",
                    timestamp: ts,
                    source: "Bazaar",
                    sourceDetail: data.seller ? String(data.seller) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                };
                break;

            case LOG_TYPES.ABROAD_BUY:
                if (!isWhitelisted && !isMuseumItem) return null;
                const areas = {
                    1: "Torn",
                    2: "Mexico",
                    3: "Cayman Islands",
                    4: "Canada",
                    5: "Hawaii",
                    6: "United Kingdom",
                    7: "Argentina",
                    8: "Switzerland",
                    9: "Japan",
                    10: "China",
                    11: "United Arab Emirates",
                    12: "South Africa",
                };
                transaction = {
                    logId,
                    type: "abroad_buy",
                    timestamp: ts,
                    source: "Abroad",
                    sourceDetail: areas[data.area] || "Unknown",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                };
                break;

            case LOG_TYPES.SHOP_BUY:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "purchase",
                    timestamp: ts,
                    source: "City Shop",
                    sourceDetail: "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                };
                break;

            // ── Sales ──
            case LOG_TYPES.ITEM_MARKET_SELL:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "sale",
                    timestamp: ts,
                    source: "Item Market",
                    sourceDetail: data.buyer ? String(data.buyer) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                    fees: data.fee || 0,
                    netAmount: costTotal - (data.fee || 0),
                };
                break;

            case LOG_TYPES.BAZAAR_SELL:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "sale",
                    timestamp: ts,
                    source: "Bazaar",
                    sourceDetail: data.buyer ? String(data.buyer) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                    fees: data.fee || 0,
                    netAmount: costTotal - (data.fee || 0),
                };
                break;

            // ── Trade items ──
            case LOG_TYPES.TRADE_ITEMS_IN:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "trade_in",
                    timestamp: ts,
                    source: "Trade",
                    sourceDetail: data.sender ? String(data.sender) : "",
                    counterparty: data.sender ? String(data.sender) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: 0,
                    totalPrice: 0, // Trade value unknown
                };
                break;

            case LOG_TYPES.TRADE_ITEMS_OUT:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "trade_out",
                    timestamp: ts,
                    source: "Trade",
                    sourceDetail: data.receiver ? String(data.receiver) : "",
                    counterparty: data.receiver ? String(data.receiver) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: 0,
                    totalPrice: 0,
                };
                break;

            // ── Museum exchange ──
            case LOG_TYPES.MUSEUM_EXCHANGE:
                transaction = {
                    logId,
                    type: "museum_exchange",
                    timestamp: ts,
                    setType: data.set || "Unknown",
                    quantity: data.quantity || 1,
                    pointsReceived: data.points_received || (data.quantity || 1) * 10,
                };
                break;

            // ── Points ──
            case LOG_TYPES.POINTS_BUY:
                transaction = {
                    logId,
                    type: "points_buy",
                    timestamp: ts,
                    action: "Points Bought",
                    details: "Bought " + (data.quantity || 0) + " points",
                    quantity: data.quantity || 0,
                    unitPrice: data.cost_each || data.cost || data.price || 0,
                    totalValue:
                        data.cost_total ||
                        data.total ||
                        (data.cost_each || data.cost || 0) * (data.quantity || 0),
                };
                break;

            case LOG_TYPES.POINTS_SELL:
                transaction = {
                    logId,
                    type: "points_sell",
                    timestamp: ts,
                    action: "Points Sold",
                    details: "Sold " + (data.quantity || 0) + " points",
                    quantity: data.quantity || 0,
                    unitPrice: data.cost_each || data.cost || data.price || 0,
                    totalValue:
                        data.cost_total ||
                        data.total ||
                        (data.cost_each || data.cost || 0) * (data.quantity || 0),
                };
                break;
        }

        return transaction;
    }

    // ========================================================================
    // GOOGLE SHEETS SYNC
    // ========================================================================

    async function syncToSheet(transactions) {
        const cfg = getConfig();
        if (!cfg.sheetWebAppUrl || transactions.length === 0) return;

        // Separate by type
        const purchases = [];
        const sales = [];
        const museums = [];
        const points = [];

        for (const tx of transactions) {
            if (tx.type === "museum_exchange") {
                museums.push(tx);
            } else if (tx.type === "points_buy" || tx.type === "points_sell") {
                points.push(tx);
            } else if (["purchase", "abroad_buy", "trade_in"].includes(tx.type)) {
                purchases.push(tx);
            } else if (["sale", "trade_out"].includes(tx.type)) {
                sales.push(tx);
            }
        }

        // Send purchases/sales in one batch
        const allTxns = [...purchases, ...sales];
        if (allTxns.length > 0) {
            try {
                await fetchGM(cfg.sheetWebAppUrl, {
                    method: "POST",
                    body: JSON.stringify({
                        secret: cfg.sheetSecret,
                        action: "logTransactions",
                        transactions: allTxns,
                    }),
                });
            } catch (e) {
                console.error("[TTA] Sheet sync failed:", e);
            }
        }

        // Send museum exchanges
        for (const m of museums) {
            try {
                await fetchGM(cfg.sheetWebAppUrl, {
                    method: "POST",
                    body: JSON.stringify({
                        secret: cfg.sheetSecret,
                        action: "logMuseumExchange",
                        setType: m.setType,
                        quantity: m.quantity,
                        pointsReceived: m.pointsReceived,
                        timestamp: m.timestamp,
                    }),
                });
            } catch (e) {
                console.error("[TTA] Museum sync failed:", e);
            }
        }

        // Send points transactions
        for (const p of points) {
            try {
                await fetchGM(cfg.sheetWebAppUrl, {
                    method: "POST",
                    body: JSON.stringify({
                        secret: cfg.sheetSecret,
                        action: "logPointsTransaction",
                        data: p,
                    }),
                });
            } catch (e) {
                console.error("[TTA] Points sync failed:", e);
            }
        }
    }

    // ========================================================================
    // MAIN LOG POLLER
    // ========================================================================

    async function pollLogs() {
        const cfg = getConfig();
        if (!cfg.enabled || !cfg.licenseValid || !cfg.tornApiKey) return;

        try {
            const logData = await fetchUserLog();
            if (!logData || !logData.log) return;

            const newTransactions = [];

            for (const [logId, entry] of Object.entries(logData.log)) {
                if (isLogProcessed(logId)) continue;

                const tx = processLogEntry(logId, entry, cfg.whitelist);
                if (tx) {
                    newTransactions.push(tx);
                    markLogProcessed(logId);
                } else {
                    // Mark as processed even if not whitelisted (so we don't recheck)
                    markLogProcessed(logId);
                }
            }

            if (newTransactions.length > 0) {
                console.log(`[TTA] Found ${newTransactions.length} new transactions`);
                await syncToSheet(newTransactions);
                updateStatusBadge(newTransactions.length);
            }
        } catch (e) {
            console.error("[TTA] Poll error:", e);
        }
    }

    // ========================================================================
    // UI — STYLES
    // ========================================================================

    GM_addStyle(`
    #tta-panel {
      position: fixed;
      z-index: 999999;
      background: #1a1a2e;
      border: 1px solid #16213e;
      border-radius: 8px;
      color: #e0e0e0;
      font-family: Arial, sans-serif;
      font-size: 12px;
      box-shadow: 0 4px 16px rgba(0,0,0,0.4);
      min-width: 44px;
      max-width: 380px;
      user-select: none;
      transition: width 0.2s, height 0.2s;
    }
    #tta-panel.minimized {
      width: 44px !important;
      height: 44px !important;
      border-radius: 50%;
      overflow: hidden;
      cursor: pointer;
    }
    #tta-panel.minimized #tta-body { display: none; }
    #tta-panel.minimized #tta-header-text { display: none; }
    #tta-panel.minimized #tta-header-buttons { display: none; }

    #tta-header {
      background: #16213e;
      padding: 8px 12px;
      border-radius: 8px 8px 0 0;
      display: flex;
      align-items: center;
      justify-content: space-between;
      cursor: move;
    }
    #tta-panel.minimized #tta-header {
      border-radius: 50%;
      justify-content: center;
      padding: 10px;
    }

    #tta-logo {
      font-size: 18px;
      line-height: 1;
    }
    #tta-header-text {
      font-weight: bold;
      font-size: 13px;
      margin-left: 8px;
      flex: 1;
    }
    #tta-header-buttons button {
      background: none;
      border: none;
      color: #aaa;
      cursor: pointer;
      font-size: 14px;
      padding: 2px 6px;
    }
    #tta-header-buttons button:hover { color: #fff; }

    #tta-body {
      padding: 10px 12px;
      max-height: 500px;
      overflow-y: auto;
    }

    #tta-badge {
      position: absolute;
      top: -4px;
      right: -4px;
      background: #e74c3c;
      color: #fff;
      font-size: 10px;
      font-weight: bold;
      border-radius: 50%;
      width: 18px;
      height: 18px;
      display: flex;
      align-items: center;
      justify-content: center;
      display: none;
    }

    .tta-section {
      margin-bottom: 10px;
    }
    .tta-section-title {
      font-weight: bold;
      font-size: 11px;
      color: #888;
      text-transform: uppercase;
      margin-bottom: 4px;
      border-bottom: 1px solid #333;
      padding-bottom: 2px;
    }

    .tta-input {
      width: 100%;
      padding: 6px 8px;
      background: #0f0f1a;
      border: 1px solid #333;
      border-radius: 4px;
      color: #e0e0e0;
      font-size: 12px;
      margin: 2px 0;
      box-sizing: border-box;
    }
    .tta-input:focus { border-color: #4285F4; outline: none; }

    .tta-btn {
      padding: 6px 12px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 11px;
      font-weight: bold;
      margin: 2px;
      transition: background 0.15s;
    }
    .tta-btn-primary { background: #4285F4; color: #fff; }
    .tta-btn-primary:hover { background: #3367d6; }
    .tta-btn-success { background: #0F9D58; color: #fff; }
    .tta-btn-success:hover { background: #0b7a45; }
    .tta-btn-danger { background: #DB4437; color: #fff; }
    .tta-btn-danger:hover { background: #b8382d; }
    .tta-btn-sm { padding: 3px 8px; font-size: 10px; }

    .tta-status {
      padding: 4px 8px;
      border-radius: 4px;
      font-size: 11px;
      margin: 4px 0;
    }
    .tta-status-ok { background: #0f3d0f; color: #4caf50; }
    .tta-status-warn { background: #3d2f0f; color: #ff9800; }
    .tta-status-err { background: #3d0f0f; color: #f44336; }

    .tta-whitelist-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 3px 0;
      border-bottom: 1px solid #222;
    }
    .tta-whitelist-item span { flex: 1; }

    .tta-tabs {
      display: flex;
      border-bottom: 1px solid #333;
      margin-bottom: 8px;
    }
    .tta-tab {
      padding: 6px 12px;
      cursor: pointer;
      color: #888;
      font-size: 11px;
      border-bottom: 2px solid transparent;
      transition: all 0.15s;
    }
    .tta-tab.active {
      color: #4285F4;
      border-bottom-color: #4285F4;
    }
    .tta-tab:hover { color: #ccc; }

    .tta-log-entry {
      padding: 3px 0;
      font-size: 11px;
      border-bottom: 1px solid #1a1a2e;
    }
    .tta-log-buy { color: #4caf50; }
    .tta-log-sell { color: #f44336; }
    .tta-log-trade { color: #ff9800; }
    .tta-log-museum { color: #9c27b0; }
  `);

    // ========================================================================
    // UI — PANEL BUILDER
    // ========================================================================

    function createPanel() {
        const cfg = getConfig();

        const panel = document.createElement("div");
        panel.id = "tta-panel";
        if (cfg.panelMinimized) panel.classList.add("minimized");
        panel.style.left = cfg.panelX + "px";
        panel.style.top = cfg.panelY + "px";

        panel.innerHTML = `
      <div id="tta-badge">0</div>
      <div id="tta-header">
        <span id="tta-logo">📊</span>
        <span id="tta-header-text">Trading Assistant</span>
        <span id="tta-header-buttons">
          <button id="tta-btn-min" title="Minimize">_</button>
        </span>
      </div>
      <div id="tta-body">
        <div class="tta-tabs">
          <div class="tta-tab active" data-tab="status">Status</div>
          <div class="tta-tab" data-tab="whitelist">Whitelist</div>
          <div class="tta-tab" data-tab="settings">Settings</div>
          <div class="tta-tab" data-tab="log">Log</div>
        </div>
        <div id="tta-tab-status" class="tta-tab-content">
          <div id="tta-status-content"></div>
        </div>
        <div id="tta-tab-whitelist" class="tta-tab-content" style="display:none">
          <div id="tta-whitelist-content"></div>
        </div>
        <div id="tta-tab-settings" class="tta-tab-content" style="display:none">
          <div id="tta-settings-content"></div>
        </div>
        <div id="tta-tab-log" class="tta-tab-content" style="display:none">
          <div id="tta-log-content"></div>
        </div>
      </div>
    `;

        document.body.appendChild(panel);

        // ── Tab switching ──
        panel.querySelectorAll(".tta-tab").forEach((tab) => {
            tab.addEventListener("click", () => {
                panel
                    .querySelectorAll(".tta-tab")
                    .forEach((t) => t.classList.remove("active"));
                panel
                    .querySelectorAll(".tta-tab-content")
                    .forEach((c) => (c.style.display = "none"));
                tab.classList.add("active");
                document.getElementById("tta-tab-" + tab.dataset.tab).style.display =
                    "block";
            });
        });

        // ── Minimize toggle ──
        document.getElementById("tta-btn-min").addEventListener("click", (e) => {
            e.stopPropagation();
            panel.classList.add("minimized");
            const c = getConfig();
            c.panelMinimized = true;
            saveConfig(c);
        });

        panel.addEventListener("click", () => {
            if (panel.classList.contains("minimized")) {
                panel.classList.remove("minimized");
                const c = getConfig();
                c.panelMinimized = false;
                saveConfig(c);
            }
        });

        // ── Dragging ──
        makeDraggable(panel);

        // ── Render tabs ──
        renderStatusTab();
        renderWhitelistTab();
        renderSettingsTab();
        renderLogTab();
    }

    // ========================================================================
    // UI — DRAG HANDLER
    // ========================================================================

    function makeDraggable(el) {
        const header = el.querySelector("#tta-header");
        let isDragging = false,
            startX,
            startY,
            origX,
            origY;

        header.addEventListener("mousedown", startDrag);
        header.addEventListener("touchstart", startDrag, { passive: false });

        function startDrag(e) {
            if (el.classList.contains("minimized")) return;
            isDragging = true;
            const ev = e.touches ? e.touches[0] : e;
            startX = ev.clientX;
            startY = ev.clientY;
            origX = el.offsetLeft;
            origY = el.offsetTop;
            e.preventDefault();

            document.addEventListener("mousemove", drag);
            document.addEventListener("touchmove", drag, { passive: false });
            document.addEventListener("mouseup", stopDrag);
            document.addEventListener("touchend", stopDrag);
        }

        function drag(e) {
            if (!isDragging) return;
            const ev = e.touches ? e.touches[0] : e;
            const dx = ev.clientX - startX;
            const dy = ev.clientY - startY;
            el.style.left = Math.max(0, origX + dx) + "px";
            el.style.top = Math.max(0, origY + dy) + "px";
            e.preventDefault();
        }

        function stopDrag() {
            isDragging = false;
            document.removeEventListener("mousemove", drag);
            document.removeEventListener("touchmove", drag);
            document.removeEventListener("mouseup", stopDrag);
            document.removeEventListener("touchend", stopDrag);

            const c = getConfig();
            c.panelX = el.offsetLeft;
            c.panelY = el.offsetTop;
            saveConfig(c);
        }
    }

    // ========================================================================
    // UI — STATUS TAB
    // ========================================================================

    function renderStatusTab() {
        const cfg = getConfig();
        const el = document.getElementById("tta-status-content");
        if (!el) return;

        const licensed = cfg.licenseValid;
        const apiOk = !!cfg.tornApiKey;
        const sheetOk = !!cfg.sheetWebAppUrl;
        const processedCount = Object.keys(cfg.processedLogs).length;

        el.innerHTML = `
      <div class="tta-section">
        <div class="tta-status ${licensed ? "tta-status-ok" : "tta-status-err"}">
          License: ${licensed ? "✅ Active" : "❌ Not verified"}
        </div>
        <div class="tta-status ${apiOk ? "tta-status-ok" : "tta-status-warn"}">
          API Key: ${apiOk ? "✅ Set" : "⚠️ Not set"}
        </div>
        <div class="tta-status ${sheetOk ? "tta-status-ok" : "tta-status-warn"}">
          Sheet: ${sheetOk ? "✅ Connected" : "⚠️ Not set"}
        </div>
        <div class="tta-status ${cfg.enabled ? "tta-status-ok" : "tta-status-warn"}">
          Tracking: ${cfg.enabled ? "✅ Active" : "⏸ Paused"}
        </div>
      </div>
      <div class="tta-section">
        <div class="tta-section-title">Stats</div>
        <div>📋 ${cfg.whitelist.length} items whitelisted</div>
        <div>📝 ${processedCount} log entries processed</div>
        <div>📊 ${JSON.parse(getSetting("recentLog", "[]")).length} recent transactions</div>
      </div>
      <div class="tta-section">
        <button class="tta-btn tta-btn-primary" id="tta-btn-poll">🔄 Check Now</button>
        <button class="tta-btn ${cfg.enabled ? "tta-btn-danger" : "tta-btn-success"}" id="tta-btn-toggle">
          ${cfg.enabled ? "⏸ Pause" : "▶ Resume"}
        </button>
      </div>
      <div class="tta-section">
        <button class="tta-btn tta-btn-success tta-btn-sm" id="tta-btn-setstock">📦 Set Starting Stock</button>
        <button class="tta-btn tta-btn-sm" style="background:#ff9800;color:#fff;" id="tta-btn-debug">🔍 Debug Log Fetch</button>
        <button class="tta-btn tta-btn-sm" style="background:#9c27b0;color:#fff;" id="tta-btn-clearprocessed">🗑 Clear Processed IDs</button>
      </div>
      <div style="font-size:10px;color:#555;margin-top:8px;">v${VERSION}</div>
    `;

        document
            .getElementById("tta-btn-poll")
            .addEventListener("click", async () => {
                const btn = document.getElementById("tta-btn-poll");
                btn.textContent = "⏳ Checking...";
                btn.disabled = true;
                await pollLogs();
                btn.textContent = "🔄 Check Now";
                btn.disabled = false;
            });

        document.getElementById("tta-btn-toggle").addEventListener("click", () => {
            const c = getConfig();
            c.enabled = !c.enabled;
            saveConfig(c);
            if (c.enabled) startPollLoop();
            else stopPollLoop();
            renderStatusTab();
        });

        document
            .getElementById("tta-btn-setstock")
            .addEventListener("click", () => {
                showSetStockDialog();
            });

        document
            .getElementById("tta-btn-debug")
            .addEventListener("click", async () => {
                const btn = document.getElementById("tta-btn-debug");
                btn.textContent = "⏳ Fetching...";
                btn.disabled = true;
                await debugLogFetch();
                btn.textContent = "🔍 Debug Log Fetch";
                btn.disabled = false;
            });

        document
            .getElementById("tta-btn-clearprocessed")
            .addEventListener("click", () => {
                if (
                    confirm(
                        "Clear all processed log IDs? This will re-process logs on next check (may create duplicates if sheet already has them).",
                    )
                ) {
                    const c = getConfig();
                    c.processedLogs = {};
                    saveConfig(c);
                    renderStatusTab();
                    alert("✅ Cleared! Next poll will re-check all visible logs.");
                }
            });
    }

    // ========================================================================
    // UI — WHITELIST TAB
    // ========================================================================

    function renderWhitelistTab() {
        const cfg = getConfig();
        const el = document.getElementById("tta-whitelist-content");
        if (!el) return;

        let whitelistHtml = cfg.whitelist
            .map((id) => {
                const name = getItemName(id);
                return `
        <div class="tta-whitelist-item">
          <span>${name} (${id})</span>
          <button class="tta-btn tta-btn-danger tta-btn-sm tta-wl-remove" data-id="${id}">✕</button>
        </div>
      `;
            })
            .join("");

        if (!whitelistHtml) {
            whitelistHtml =
                '<div style="color:#888;padding:8px 0;">No items whitelisted yet. Search below to add.</div>';
        }

        el.innerHTML = `
      <div class="tta-section">
        <div class="tta-section-title">Search & Add Items</div>
        <input type="text" class="tta-input" id="tta-wl-search"
               placeholder="Search by name or ID (case insensitive)..." />
        <div id="tta-wl-results" style="max-height:150px;overflow-y:auto;margin:4px 0;"></div>
      </div>
      <div class="tta-section">
        <div class="tta-section-title">Tracked Items (${cfg.whitelist.length})</div>
        <div id="tta-wl-list">${whitelistHtml}</div>
      </div>
    `;

        // Search handler
        document.getElementById("tta-wl-search").addEventListener("input", (e) => {
            const query = e.target.value.toLowerCase().trim();
            const results = document.getElementById("tta-wl-results");
            if (query.length < 2) {
                results.innerHTML = "";
                return;
            }

            const matches = [];
            for (const [id, item] of Object.entries(ITEM_MAP)) {
                if (!item.tradeable) continue;
                const nameMatch = item.name.toLowerCase().includes(query);
                const idMatch = id.includes(query);
                if (nameMatch || idMatch) {
                    const alreadyAdded = cfg.whitelist.includes(Number(id));
                    matches.push({
                        id: Number(id),
                        name: item.name,
                        type: item.type,
                        added: alreadyAdded,
                    });
                }
                if (matches.length >= 20) break;
            }

            results.innerHTML = matches
                .map(
                    (m) => `
        <div class="tta-whitelist-item">
          <span>${m.name} (${m.id}) <small style="color:#666">${m.type}</small></span>
          ${
                        m.added
                            ? '<span style="color:#4caf50;font-size:10px;">✓ Added</span>'
                            : `<button class="tta-btn tta-btn-success tta-btn-sm tta-wl-add" data-id="${m.id}">+ Add</button>`
                    }
        </div>
      `,
                )
                .join("");

            // Add button handlers
            results.querySelectorAll(".tta-wl-add").forEach((btn) => {
                btn.addEventListener("click", () => {
                    const c = getConfig();
                    const itemId = Number(btn.dataset.id);
                    if (!c.whitelist.includes(itemId)) {
                        c.whitelist.push(itemId);
                        saveConfig(c);
                        renderWhitelistTab();
                    }
                });
            });
        });

        // Remove button handlers
        el.querySelectorAll(".tta-wl-remove").forEach((btn) => {
            btn.addEventListener("click", () => {
                const c = getConfig();
                const itemId = Number(btn.dataset.id);
                c.whitelist = c.whitelist.filter((id) => id !== itemId);
                saveConfig(c);
                renderWhitelistTab();
            });
        });
    }

    // ========================================================================
    // UI — SETTINGS TAB
    // ========================================================================

    function renderSettingsTab() {
        const cfg = getConfig();
        const el = document.getElementById("tta-settings-content");
        if (!el) return;

        el.innerHTML = `
      <div class="tta-section">
        <div class="tta-section-title">License Key</div>
        <input type="text" class="tta-input" id="tta-set-license"
               placeholder="XXXX-XXXX-XXXX-XXXX" value="${cfg.licenseKey}" />
        <button class="tta-btn tta-btn-primary" id="tta-btn-verify">Verify License</button>
        <div id="tta-license-status"></div>
      </div>

      <div class="tta-section">
        <div class="tta-section-title">Torn API Key</div>
        <input type="password" class="tta-input" id="tta-set-apikey"
               placeholder="Your 16-char API key" value="${cfg.tornApiKey}" />
        <div style="font-size:10px;color:#888;">Requires Limited Access (log, money, travel)</div>
      </div>

      <div class="tta-section">
        <div class="tta-section-title">Google Sheet Web App URL</div>
        <input type="text" class="tta-input" id="tta-set-sheeturl"
               placeholder="https://script.google.com/macros/s/..." value="${cfg.sheetWebAppUrl}" />
      </div>

      <div class="tta-section">
        <div class="tta-section-title">Sheet Secret Passphrase</div>
        <input type="password" class="tta-input" id="tta-set-sheetsecret"
               placeholder="Same as set in Apps Script" value="${cfg.sheetSecret}" />
      </div>

      <div class="tta-section">
        <div class="tta-section-title">Poll Interval (seconds)</div>
        <input type="number" class="tta-input" id="tta-set-interval"
               min="15" max="300" value="${cfg.pollInterval}" />
        <div style="font-size:10px;color:#888;">How often to check for new transactions (min 15s)</div>
      </div>

      <div class="tta-section">
        <button class="tta-btn tta-btn-success" id="tta-btn-save">💾 Save Settings</button>
        <button class="tta-btn tta-btn-primary" id="tta-btn-test">🔗 Test Sheet Connection</button>
      </div>

      <div class="tta-section">
        <div class="tta-section-title">Danger Zone</div>
        <button class="tta-btn tta-btn-danger tta-btn-sm" id="tta-btn-reset">🗑 Reset All Data</button>
      </div>
    `;

        // Verify license
        document
            .getElementById("tta-btn-verify")
            .addEventListener("click", async () => {
                const key = document.getElementById("tta-set-license").value.trim();
                const statusEl = document.getElementById("tta-license-status");
                statusEl.innerHTML =
                    '<div class="tta-status tta-status-warn">⏳ Verifying...</div>';

                const valid = await verifyLicense(key);
                const c = getConfig();
                c.licenseKey = key;
                c.licenseValid = valid;
                saveConfig(c);

                statusEl.innerHTML = valid
                    ? '<div class="tta-status tta-status-ok">✅ License verified!</div>'
                    : '<div class="tta-status tta-status-err">❌ Invalid license key</div>';
                renderStatusTab();
            });

        // Save settings
        document.getElementById("tta-btn-save").addEventListener("click", () => {
            const c = getConfig();
            c.tornApiKey = document.getElementById("tta-set-apikey").value.trim();
            c.sheetWebAppUrl = document
                .getElementById("tta-set-sheeturl")
                .value.trim();
            c.sheetSecret = document
                .getElementById("tta-set-sheetsecret")
                .value.trim();
            c.pollInterval = Math.max(
                15,
                parseInt(document.getElementById("tta-set-interval").value) || 30,
            );
            saveConfig(c);

            // Rebuild item map with new API key
            buildItemMap();

            alert("✅ Settings saved!");
            renderStatusTab();
        });

        // Test sheet
        document
            .getElementById("tta-btn-test")
            .addEventListener("click", async () => {
                const c = getConfig();
                if (!c.sheetWebAppUrl) {
                    alert("Set Sheet URL first!");
                    return;
                }
                try {
                    const resp = await fetchGM(c.sheetWebAppUrl, {
                        method: "POST",
                        body: JSON.stringify({ secret: c.sheetSecret, action: "ping" }),
                    });
                    if (resp.data && resp.data.success) {
                        alert("✅ Sheet connected! Response: " + resp.data.message);
                    } else {
                        alert(
                            "❌ Sheet responded but failed: " + JSON.stringify(resp.data),
                        );
                    }
                } catch (e) {
                    alert("❌ Connection failed: " + e.message);
                }
            });

        // Reset
        document.getElementById("tta-btn-reset").addEventListener("click", () => {
            if (
                confirm(
                    "⚠️ This will erase ALL settings, whitelist, and log history.\n\nAre you sure?",
                )
            ) {
                [
                    "licenseKey",
                    "licenseValid",
                    "tornApiKey",
                    "sheetWebAppUrl",
                    "sheetSecret",
                    "whitelist",
                    "lastLogCheck",
                    "processedLogs",
                    "panelX",
                    "panelY",
                    "panelMinimized",
                    "enabled",
                    "pollInterval",
                    "itemMap",
                    "itemMapTime",
                    "recentLog",
                ].forEach((k) => GM_deleteValue("tta_" + k));
                location.reload();
            }
        });
    }

    // ========================================================================
    // UI — LOG TAB
    // ========================================================================

    function renderLogTab() {
        const el = document.getElementById("tta-log-content");
        if (!el) return;

        const logs = JSON.parse(getSetting("recentLog", "[]"));

        if (logs.length === 0) {
            el.innerHTML =
                '<div style="color:#888;padding:8px 0;">No transactions logged yet.</div>';
            return;
        }

        el.innerHTML = logs
            .slice(-50)
            .reverse()
            .map((log) => {
                const cls =
                    log.type.includes("sale") || log.type === "trade_out"
                        ? "tta-log-sell"
                        : log.type === "museum_exchange"
                            ? "tta-log-museum"
                            : log.type.includes("trade")
                                ? "tta-log-trade"
                                : "tta-log-buy";
                const time = new Date(log.timestamp * 1000).toLocaleString();
                return `
        <div class="tta-log-entry ${cls}">
          <strong>${log.type.toUpperCase()}</strong>
          ${log.itemName ? log.itemName + " x" + (log.quantity || 1) : log.setType || log.action || ""}
          <br><small>${time} | ${log.source || ""}</small>
        </div>
      `;
            })
            .join("");
    }

    function addToRecentLog(transactions) {
        const logs = JSON.parse(getSetting("recentLog", "[]"));
        for (const tx of transactions) {
            logs.push(tx);
        }
        // Keep last 200
        const trimmed = logs.slice(-200);
        setSetting("recentLog", JSON.stringify(trimmed));
    }

    // ========================================================================
    // UI — SET STARTING STOCK DIALOG
    // ========================================================================

    function showSetStockDialog() {
        const existing = document.getElementById("tta-stock-dialog");
        if (existing) existing.remove();

        // Define items locally (these match the Apps Script PLUSHIES/FLOWERS arrays)
        const KNOWN_PLUSHIES = [
            { id: 186, name: "Sheep Plushie" },
            { id: 187, name: "Teddy Bear Plushie" },
            { id: 215, name: "Kitten Plushie" },
            { id: 258, name: "Jaguar Plushie" },
            { id: 618, name: "Stingray Plushie" },
            { id: 261, name: "Wolverine Plushie" },
            { id: 266, name: "Nessie Plushie" },
            { id: 268, name: "Red Fox Plushie" },
            { id: 269, name: "Monkey Plushie" },
            { id: 273, name: "Chamois Plushie" },
            { id: 274, name: "Panda Plushie" },
            { id: 384, name: "Camel Plushie" },
            { id: 281, name: "Lion Plushie" },
        ];
        const KNOWN_FLOWERS = [
            { id: 260, name: "Dahlia" },
            { id: 617, name: "Banana Orchid" },
            { id: 263, name: "Crocus" },
            { id: 264, name: "Orchid" },
            { id: 267, name: "Heather" },
            { id: 271, name: "Ceibo Flower" },
            { id: 272, name: "Edelweiss" },
            { id: 277, name: "Cherry Blossom" },
            { id: 276, name: "Peony" },
            { id: 385, name: "Tribulus Omanense" },
            { id: 282, name: "African Violet" },
        ];

        const dialog = document.createElement("div");
        dialog.id = "tta-stock-dialog";
        dialog.style.cssText = `
      position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
      z-index:9999999;background:#1a1a2e;border:2px solid #4285F4;
      border-radius:12px;padding:20px;color:#e0e0e0;
      max-height:80vh;overflow-y:auto;width:320px;
      box-shadow:0 8px 32px rgba(0,0,0,0.6);font-family:Arial;font-size:12px;
    `;

        dialog.innerHTML = `
      <h3 style="margin:0 0 12px;color:#4285F4;">📦 Set Starting Stock</h3>
      <div style="font-size:10px;color:#888;margin-bottom:10px;">
        Enter how many of each item you currently own.
        This sets the baseline for tracking.
      </div>
      <div style="font-weight:bold;color:#DB4437;margin:8px 0 4px;">🧸 Plushies</div>
      ${KNOWN_PLUSHIES.map(
            (i) => `
        <div style="display:flex;align-items:center;margin:3px 0;">
          <span style="flex:1;">${i.name}</span>
          <input type="number" min="0" value="0" style="width:60px;padding:4px;
            background:#0f0f1a;border:1px solid #333;color:#e0e0e0;border-radius:4px;"
            data-itemid="${i.id}" class="tta-stock-input" />
        </div>
      `,
        ).join("")}
      <div style="font-weight:bold;color:#0F9D58;margin:12px 0 4px;">🌸 Flowers</div>
      ${KNOWN_FLOWERS.map(
            (i) => `
        <div style="display:flex;align-items:center;margin:3px 0;">
          <span style="flex:1;">${i.name}</span>
          <input type="number" min="0" value="0" style="width:60px;padding:4px;
            background:#0f0f1a;border:1px solid #333;color:#e0e0e0;border-radius:4px;"
            data-itemid="${i.id}" class="tta-stock-input" />
        </div>
      `,
        ).join("")}
      <div style="margin-top:12px;display:flex;gap:8px;">
        <button class="tta-btn tta-btn-success" id="tta-stock-save">💾 Save to Sheet</button>
        <button class="tta-btn tta-btn-danger" id="tta-stock-cancel">Cancel</button>
      </div>
      <div id="tta-stock-status" style="margin-top:8px;"></div>
    `;

        document.body.appendChild(dialog);

        document
            .getElementById("tta-stock-cancel")
            .addEventListener("click", () => {
                dialog.remove();
            });

        document
            .getElementById("tta-stock-save")
            .addEventListener("click", async () => {
                const cfg = getConfig();
                if (!cfg.sheetWebAppUrl) {
                    alert("Set your Sheet Web App URL in settings first!");
                    return;
                }

                const statusEl = document.getElementById("tta-stock-status");
                statusEl.innerHTML =
                    '<div class="tta-status tta-status-warn">⏳ Saving...</div>';

                const stockItems = [];
                dialog.querySelectorAll(".tta-stock-input").forEach((input) => {
                    const qty = parseInt(input.value) || 0;
                    const itemId = parseInt(input.dataset.itemid);
                    if (itemId) {
                        stockItems.push({ itemId, quantity: qty });
                    }
                });

                try {
                    const resp = await fetchGM(cfg.sheetWebAppUrl, {
                        method: "POST",
                        body: JSON.stringify({
                            secret: cfg.sheetSecret,
                            action: "setInitialStock",
                            items: stockItems,
                        }),
                    });

                    if (resp.data && resp.data.success) {
                        statusEl.innerHTML = `<div class="tta-status tta-status-ok">✅ Updated ${resp.data.updated} items!</div>`;
                        setTimeout(() => dialog.remove(), 2000);
                    } else {
                        statusEl.innerHTML = `<div class="tta-status tta-status-err">❌ ${resp.data ? resp.data.error : "Unknown error"}</div>`;
                    }
                } catch (e) {
                    statusEl.innerHTML = `<div class="tta-status tta-status-err">❌ ${e.message}</div>`;
                }
            });
    }

    // ========================================================================
    // UI — STATUS BADGE
    // ========================================================================

    function updateStatusBadge(count) {
        const badge = document.getElementById("tta-badge");
        if (!badge) return;
        if (count > 0) {
            badge.style.display = "flex";
            badge.textContent = count;
            setTimeout(() => {
                badge.style.display = "none";
            }, 5000);
        }
    }

    // ========================================================================
    // PAGE MONITOR — Detect transactions on currently viewed pages
    // ========================================================================

    /**
     * COMPLIANCE NOTE:
     * This only reads data from the page the user is CURRENTLY VIEWING.
     * It does NOT make any additional requests to Torn.
     * It does NOT scrape pages not being viewed.
     * It only observes DOM changes on the active page.
     */

    function setupPageMonitor() {
        const path = window.location.pathname;

        // Monitor item market page
        if (path.includes("/imarket") || path.includes("/itemmarket")) {
            observePageForTransactions("itemmarket");
        }

        // Monitor bazaar page
        if (path.includes("/bazaar")) {
            observePageForTransactions("bazaar");
        }

        // Monitor trade page
        if (path.includes("/trade")) {
            observePageForTransactions("trade");
        }

        // Monitor museum page
        if (path.includes("/museum")) {
            observePageForTransactions("museum");
        }

        // Monitor abroad page
        if (path.includes("/travelagency") || path.includes("/abroad")) {
            observePageForTransactions("abroad");
        }
    }

    function observePageForTransactions(pageType) {
        // Use MutationObserver to detect when new content loads
        // This is compliant — we're only reading the page the user is viewing
        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType !== 1) continue;

                    // Look for success messages / confirmation dialogs
                    const text = node.textContent || "";

                    // Item market / bazaar buy confirmations
                    if (pageType === "itemmarket" || pageType === "bazaar") {
                        if (
                            text.includes("You bought") ||
                            text.includes("purchase") ||
                            text.includes("sold") ||
                            text.includes("listed")
                        ) {
                            // Trigger a log poll to catch the transaction
                            setTimeout(() => pollLogs(), 3000);
                        }
                    }

                    // Trade completions
                    if (pageType === "trade") {
                        if (text.includes("Trade completed") || text.includes("accepted")) {
                            setTimeout(() => pollLogs(), 3000);
                        }
                    }

                    // Museum exchanges
                    if (pageType === "museum") {
                        if (
                            text.includes("exchanged") ||
                            text.includes("museum") ||
                            text.includes("points")
                        ) {
                            setTimeout(() => pollLogs(), 3000);
                        }
                    }

                    // Abroad purchases
                    if (pageType === "abroad") {
                        if (text.includes("bought") || text.includes("purchase")) {
                            setTimeout(() => pollLogs(), 3000);
                        }
                    }
                }
            }
        });

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

    // ========================================================================
    // ENHANCED LOG POLLER WITH SHEET SYNC
    // ========================================================================

    // Override the original pollLogs with one that also updates the log tab
    const _originalPollLogs = pollLogs;

    pollLogs = async function () {
        const cfg = getConfig();
        if (!cfg.enabled || !cfg.licenseValid || !cfg.tornApiKey) return;

        try {
            const logData = await fetchUserLog();
            if (!logData || !logData.log) return;

            const newTransactions = [];

            for (const [logId, entry] of Object.entries(logData.log)) {
                if (isLogProcessed(logId)) continue;

                const tx = processLogEntry(logId, entry, cfg.whitelist);
                if (tx) {
                    newTransactions.push(tx);
                    markLogProcessed(logId);
                } else {
                    markLogProcessed(logId);
                }
            }

            if (newTransactions.length > 0) {
                console.log(`[TTA] Found ${newTransactions.length} new transactions`);

                // Add to recent log for display
                addToRecentLog(newTransactions);

                // Sync to sheet
                await syncToSheet(newTransactions);

                // Update badge & notification
                updateStatusBadge(newTransactions.length);
                showNotification(newTransactions);
            }

            // ALWAYS refresh UI — even when 0 new transactions
            // (so processed count updates after Check Now)
            renderStatusTab();
            renderLogTab();
        } catch (e) {
            console.error("[TTA] Poll error:", e);
        }
    };

    // ========================================================================
    // NOTIFICATIONS
    // ========================================================================

    function showNotification(transactions) {
        const existing = document.getElementById("tta-notification");
        if (existing) existing.remove();

        const purchases = transactions.filter((t) =>
            ["purchase", "abroad_buy", "trade_in"].includes(t.type),
        );
        const sales = transactions.filter((t) =>
            ["sale", "trade_out"].includes(t.type),
        );
        const other = transactions.filter(
            (t) =>
                !["purchase", "abroad_buy", "trade_in", "sale", "trade_out"].includes(
                    t.type,
                ),
        );

        let msg = "";
        if (purchases.length) msg += `📥 ${purchases.length} purchase(s) logged\n`;
        if (sales.length) msg += `📤 ${sales.length} sale(s) logged\n`;
        if (other.length) msg += `📋 ${other.length} other event(s) logged\n`;

        const notif = document.createElement("div");
        notif.id = "tta-notification";
        notif.style.cssText = `
      position:fixed;bottom:20px;right:20px;z-index:9999999;
      background:#1a1a2e;border:1px solid #4285F4;border-radius:8px;
      padding:12px 16px;color:#e0e0e0;font-family:Arial;font-size:12px;
      box-shadow:0 4px 16px rgba(0,0,0,0.4);max-width:280px;
      animation:ttaSlideIn 0.3s ease;
    `;
        notif.innerHTML = `
      <div style="font-weight:bold;margin-bottom:4px;">📊 Trading Assistant</div>
      <div style="white-space:pre-line;">${msg.trim()}</div>
    `;

        // Add animation
        GM_addStyle(`
      @keyframes ttaSlideIn {
        from { transform: translateX(100%); opacity: 0; }
        to { transform: translateX(0); opacity: 1; }
      }
    `);

        document.body.appendChild(notif);
        setTimeout(() => {
            if (notif.parentNode) {
                notif.style.transition = "opacity 0.3s";
                notif.style.opacity = "0";
                setTimeout(() => notif.remove(), 300);
            }
        }, 4000);
    }

    // ========================================================================
    // MAIN POLL LOOP
    // ========================================================================

    let pollTimer = null;

    function startPollLoop() {
        const cfg = getConfig();
        if (pollTimer) clearInterval(pollTimer);

        const interval = Math.max(15, cfg.pollInterval) * 1000;

        // Initial poll after 5 seconds (let page load)
        setTimeout(() => pollLogs(), 5000);

        // Then poll on interval
        pollTimer = setInterval(() => {
            pollLogs();
        }, interval);
    }

    function stopPollLoop() {
        if (pollTimer) {
            clearInterval(pollTimer);
            pollTimer = null;
        }
    }

    // ========================================================================
    // INITIALIZATION
    // ========================================================================

    async function init() {
        console.log("[TTA] Torn Trading Assistant v" + VERSION + " starting...");

        const cfg = getConfig();

        // Build item map (from API, cached 24h)
        if (cfg.tornApiKey) {
            await buildItemMap();
        }

        // Create the UI panel
        createPanel();

        // Setup page monitoring (compliant — current page only)
        setupPageMonitor();

        // Register GM menu command for quick access
        GM_registerMenuCommand("📊 Trading Assistant Settings", () => {
            const panel = document.getElementById("tta-panel");
            if (panel) {
                panel.classList.remove("minimized");
                const c = getConfig();
                c.panelMinimized = false;
                saveConfig(c);
                // Switch to settings tab
                panel
                    .querySelectorAll(".tta-tab")
                    .forEach((t) => t.classList.remove("active"));
                panel
                    .querySelectorAll(".tta-tab-content")
                    .forEach((c) => (c.style.display = "none"));
                panel.querySelector('[data-tab="settings"]').classList.add("active");
                document.getElementById("tta-tab-settings").style.display = "block";
            }
        });

        // Auto-verify license on load (if key exists)
        if (cfg.licenseKey && !cfg.licenseValid) {
            const valid = await verifyLicense(cfg.licenseKey);
            if (valid) {
                const c = getConfig();
                c.licenseValid = true;
                saveConfig(c);
                renderStatusTab();
                console.log("[TTA] License auto-verified ✓");
            }
        }

        // Start polling if licensed and configured
        if (cfg.licenseValid && cfg.tornApiKey) {
            startPollLoop();
            console.log("[TTA] Poll loop started");
        } else {
            console.log("[TTA] Not starting poll loop — license or API key missing");
        }
    }

    // Wait for page to be ready
    if (
        document.readyState === "complete" ||
        document.readyState === "interactive"
    ) {
        setTimeout(init, 1000);
    } else {
        window.addEventListener("load", () => setTimeout(init, 1000));
    }
})();