Torn Spy Integration

Adds Torn Spy integration to Torn.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         Torn Spy Integration
// @namespace    tornspy-integration
// @version      0.9.3
// @description  Adds Torn Spy integration to Torn.
// @author       Neodork
// @match        https://www.torn.com/*
// @connect      www.tornspy.com
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_addStyle
// @license      GNU GPLv3
// ==/UserScript==

(function() {
    'use strict';

    const DOMAIN = 'https://www.tornspy.com';
    const TORN_SPY_KEY = 'tspy-key';
    const MINI_PROFILE_BREAKDOWN = 'tspy-mini-bd';
    const PAYMENT_TRACKER = 'tspy-ptrack';
    const SPY_TRACKER = 'tspy-strack';

    /**
     * Global functions
     */
    const setPermissions = (permissions) => {
        GM_setValue('permissions', permissions)
    }

    const hasPermission = (permission) => {
        return (GM_getValue('permissions')?.[permission]) ?? false;
    }

    const loaded_state = {}

    const has_loaded = (subject) => {
        return loaded_state[subject] ?? false;
    }

    const set_loaded = (subject) => {
        loaded_state[subject] = true
    }

    const isMobile = () => {
        return window.innerWidth < 785;
    }

    const search = (query, callback) => {
        getAction({
            type: "post",
            action: "/autocompleteHeaderAjaxAction.php",
            data: {
                q: query,
                option: 'player'
            },
            success: callback
        })
    };

    const purchase = (step, id, money, tag, theanon, callback) => {
        getAction({
            type: "post",
            action: "/sendcash.php",
            data: {
                step: step,
                ID: id,
                money: money,
                tag: tag,
                theanon: theanon
            },
            success: callback
        })
    };

    const spy = (id, specialId, amount, usersId, callback) => {
        ajaxWrapper({
            url: 'companies.php?step=specialgo',
            type: 'POST',
            data: [
                {name: 'ID', value: id},
                {name: 'specialid', value: specialId},
                {name: 'amount', value: amount},
                {name: 'usersID', value: usersId},
            ],
            oncomplete: callback
        });
    };

    const waitForElement = (selector) => {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }

            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });

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

    const initializeTornTooltip = (tooltipElements) => {
        initializeTooltip(tooltipElements, 'white-tooltip');
    }

    const initializeTornItemInfo = (withListener) => {
        document.querySelectorAll('.item-list>.item-weapon, .item-list>.item-armour').forEach((itemPlate) =>{
            let lastOpen = null;
            if(itemPlate.dataset.trigger === false){
                return
            }

            if(withListener){
                itemPlate.addEventListener("click", () => {
                    document.querySelectorAll('[data-itemId]').forEach((itemDescription) =>{
                        if(itemDescription.dataset.itemid != itemPlate.dataset.armoury && itemDescription.classList.contains('d-none') == false){
                            itemDescription.classList.add('d-none')
                        }
                    })
                    if(document.querySelector('[data-itemId="'+itemPlate.dataset.armoury+'"]').classList.contains('d-none')){
                        document.querySelector('[data-itemId="'+itemPlate.dataset.armoury+'"]').classList.remove('d-none')
                    }else{
                        document.querySelector('[data-itemId="'+itemPlate.dataset.armoury+'"]').classList.add('d-none')
                    }
                });
            }
        });
    }

    const detective_svg = () => {
        return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 512 512" xml:space="preserve" fill="#777">

<path class="st0" d="M479.031,184.313l5.531-11.25h-94.453c-0.141-0.813-0.344-1.875-0.594-3.234 c-1.828-9.766-6.172-32.969-10.797-57.594v-0.016c-3.047-16.188-6.219-32.984-8.875-47c-1.328-7-2.531-13.313-3.531-18.5 s-1.797-9.25-2.313-11.781c-0.563-2.688-1.391-5.25-2.453-7.688c-1.078-2.438-2.391-4.75-3.922-6.875 c-4.563-6.469-10.906-11.516-18.156-15C332.203,1.906,324.016,0,315.531,0c-5.188,0-10.469,0.719-15.688,2.25 c-5.203,1.516-10.328,3.875-15.172,7.094c-6.141,4.109-11.813,6.219-16.688,7.375c-2.453,0.563-4.703,0.891-6.703,1.063 c-2,0.188-3.75,0.219-5.281,0.219c-1.469,0-2.719,0.063-3.922,0.063c-1.297,0-2.516-0.063-3.891-0.234 c-2.063-0.266-4.563-0.797-7.969-2.109c-3.438-1.281-7.75-3.344-13.219-6.594c-9.891-5.875-20.625-8.531-30.875-8.531 c-5.625,0-11.109,0.813-16.281,2.344c-7.75,2.281-14.813,6.188-20.438,11.609c-2.813,2.703-5.25,5.766-7.188,9.203 c-1.938,3.422-3.391,7.172-4.203,11.188h0.016c-0.531,2.531-1.313,6.594-2.313,11.781c-3,15.563-7.859,41.219-12.438,65.516 c-4.625,24.625-8.953,47.828-10.781,57.594c-0.25,1.359-0.469,2.422-0.609,3.234H27.438l5.531,11.25 c0.281,0.563,3,5.953,9.844,13.719c10.219,11.656,29.688,28.625,63.5,42.578c9.672,3.969,20.5,7.703,32.594,11.016v4.156 L176,298.656c0,0,42.906,8.125,46.391,6.969c3.469-1.156,33.609-31.313,33.609-31.313s30.156,30.156,33.625,31.313 S336,298.656,336,298.656l37.094-42.875v-4.156c40.391-11.031,66.484-26.875,82.625-40.469 C472.688,196.875,478.641,185.125,479.031,184.313z M148.656,115.125c3.031-16.188,6.219-33,8.859-46.984 c1.328-7,2.547-13.297,3.531-18.453c1-5.172,1.797-9.219,2.297-11.656l0,0c0.625-3.125,1.891-5.953,3.719-8.563 c2.75-3.875,6.844-7.219,11.875-9.563s10.969-3.688,17.188-3.672c7.563,0,15.5,1.953,22.906,6.328 c8.094,4.813,14.469,7.625,19.875,9.219c2.688,0.781,5.141,1.281,7.344,1.563c2.219,0.281,4.156,0.344,5.828,0.344 c1.563,0,2.859-0.063,3.922-0.063c1.781,0,4.063-0.047,6.688-0.281c3.938-0.344,8.688-1.125,13.906-2.813s10.906-4.281,16.75-8.188 c3.516-2.344,7.188-4.031,10.906-5.125s7.531-1.594,11.281-1.594c4.063,0,8.094,0.625,11.813,1.766 c5.609,1.703,10.578,4.609,14.281,8.203c1.828,1.781,3.375,3.75,4.563,5.844c1.188,2.078,2.031,4.281,2.5,6.594l0,0 c0.5,2.438,1.281,6.484,2.281,11.656c2.969,15.484,7.844,41.141,12.391,65.422c0.609,3.219,1.203,6.391,1.797,9.547H146.859 C147.453,121.5,148.047,118.328,148.656,115.125z M395.375,227.906c-31.469,12.25-76.313,21.969-139.375,21.969 c-44.016,0-79.156-4.75-107.063-11.781c-41.859-10.531-67.469-26.188-82.578-38.891c-4.563-3.828-8.156-7.422-10.953-10.516 h401.203c-0.547,0.594-1.109,1.219-1.703,1.844C445.031,200.906,426.844,215.656,395.375,227.906z"/> <path class="st0" d="M94.25,345.125c5.828-6.141,10.969-10.828,14.625-13.969c1.813-1.547,3.266-2.719,4.234-3.5 c0.484-0.375,0.859-0.656,1.109-0.844c0.109-0.078,0.203-0.141,0.25-0.188l0.047-0.031h0.016l-0.094-0.109l-0.031-0.047 l-9.094-12.484c-0.281,0.219-9.891,7.234-22.406,20.422c-12.5,13.203-27.938,32.594-39.719,57.625l-3.156,6.688l49,26.438 c-7.438,5.516-18.063,13.797-29.313,23.766c-10.375,9.203-21.234,19.859-30.594,31.234c-4.656,5.672-8.938,11.531-12.594,17.531 c-2.844,4.703-5.313,9.484-7.219,14.344h17.125c1-2.063,2.172-4.141,3.453-6.219c5.531-9.125,13.203-18.516,21.609-27.344 c12.625-13.266,26.844-25.281,37.859-33.922c5.516-4.328,10.234-7.813,13.563-10.203c1.672-1.188,2.984-2.109,3.875-2.75 c0.453-0.313,0.781-0.531,1-0.688c0.109-0.078,0.203-0.125,0.25-0.172l0.063-0.047h0.016l10.688-7.219l-58.203-31.406 C70.969,372.094,83.719,356.234,94.25,345.125z"/> <path class="st0" d="M495.469,497.656c-6.375-10.5-14.719-20.594-23.656-30c-13.391-14.063-28.109-26.469-39.531-35.438 c-3.438-2.703-6.594-5.094-9.313-7.094l49-26.438L468.828,392c-11.797-25.031-27.219-44.422-39.734-57.625 c-12.5-13.188-22.125-20.203-22.422-20.422l-9.031,12.422l-0.156,0.219c0.266,0.203,9.266,6.828,20.828,19.125 c10.406,11.063,22.906,26.719,33.094,46.313l-58.219,31.406l10.703,7.219c0.047,0.031,4.656,3.156,11.734,8.469 c7.063,5.313,16.594,12.828,26.5,21.656c9.938,8.813,20.219,18.938,28.781,29.375c4.266,5.219,8.094,10.5,11.25,15.688 c1.25,2.063,2.406,4.125,3.438,6.156h17.094C500.781,507.141,498.328,502.359,495.469,497.656z"/> <rect x="235.906" y="417.969" class="st0" width="40.188" height="28.406"/> <polygon class="st0" points="240.469,461.313 234.234,512 277.781,512 271.547,461.313 "/>

</svg>`
    }

    const toggle_display = (element) => {
        if(element.classList.contains('d-none')){
            element.classList.remove('d-none')
        }else{
            element.classList.add('d-none')
        }
    }

    const set_display = (element, bool) => {
        if(bool){
            element.classList.remove('d-none')
        }else{
            element.classList.add('d-none')
        }
    }

    const set_loading = (element, disabled, text) => {
        if(!element){
            return
        }

        element.disabled = disabled;

        const titleElement = element.querySelector('[class^="title"], [class^="linkTitle"]')
        if(titleElement){
            titleElement.innerHTML = text
        }else{
            element.innerHTML = text
        }
    }

    const attach_purchase_flow = () => {
        document.querySelectorAll('.tspy-sale').forEach((saleWrapper) => {
            if(saleWrapper.querySelector(".tspy-buy")){
                saleWrapper.querySelector(".tspy-buy").addEventListener("click", () => {
                    set_display(saleWrapper.querySelector(".tspy-product"), false)
                    set_display(saleWrapper.querySelector(".tspy-purchase"), true)
                });
            }
            if(saleWrapper.querySelector(".tspy-cancel-btn")){
                saleWrapper.querySelector(".tspy-cancel-btn").addEventListener("click", () => {
                    set_display(saleWrapper.querySelector(".tspy-purchase"), false)
                    set_display(saleWrapper.querySelector(".tspy-product"), true)
                });
            }
            if(saleWrapper.querySelector(".tspy-purchase-btn")) {
                if(hasPaymentTracked(saleWrapper.querySelector('.tspy-purchase-btn').dataset.tag)) {
                    set_display(saleWrapper.querySelector(".tspy-product"), false)
                    set_display(saleWrapper.querySelector(".tspy-complete"), true)
                }
                saleWrapper.querySelector(".tspy-purchase-btn").addEventListener("click", (event) => {
                    set_loading(saleWrapper.querySelector(".tspy-purchase-btn"), true, 'Sending payment...')
                    set_display(saleWrapper.querySelector(".tspy-cancel-btn"), false)
                    purchase('cash1', event.target.dataset.target, event.target.dataset.price, event.target.dataset.tag, false, (response) => {
                        response = JSON.parse(response);
                        if(response.error){
                            saleWrapper.querySelector(".tspy-error").innerHTML = response.error.replace("area", '').replace('This', 'Buying reports');
                            set_display(saleWrapper.querySelector(".tspy-purchase"), false)
                            set_display(saleWrapper.querySelector(".tspy-failure"), true)
                        }else if(response.success === false){
                            saleWrapper.querySelector(".tspy-error").innerHTML = response.text.replace('<b>', '<b class="t-red">').replace('<b>', '<b class="t-green">');
                            set_display(saleWrapper.querySelector(".tspy-purchase"), false)
                            set_display(saleWrapper.querySelector(".tspy-failure"), true)
                        }else{
                            set_display(saleWrapper.querySelector(".tspy-purchase"), false)
                            set_display(saleWrapper.querySelector(".tspy-complete"), true)
                            if(getPaymentTracker()){
                                GM_setValue(`${PAYMENT_TRACKER}_${event.srcElement.dataset.tag}`, Date.now())
                            }
                        }
                    });
                });
            }
        });
    }

    const attach_copy = () => {
        document.querySelectorAll('[data-copy]').forEach(element => {
            element.addEventListener("click", (event) => {
                set_loading(element, true, 'Copying...')
                GM_xmlhttpRequest(
                    {
                        method: 'POST',
                        url: `${DOMAIN}${element.dataset.copy}`,
                        responseType: 'text',
                        headers: { 'TORNSPY-KEY': getApiKey() },
                        onload: (response) => {
                            navigator.clipboard.writeText(response.response ?? response.responseText);
                            set_loading(element, false, 'Copy')
                        }
                    }
                );
            })
        })
    }

    const cachedSpyRecord = (userId, element) => {
        var record = GM_getValue(`${SPY_TRACKER}_${userId}`)
        if(!record){
            return;
        }

        record = JSON.parse(record);

        const difference = record.timestamp / 1000 - element.querySelector('.tspy-spy-details').dataset.timestamp;
        if(difference < 1){
            GM_deleteValue(`${SPY_TRACKER}_${userId}`);
            return;
        }

        updateSpyRecordView(record, element);
    };

    const updateSpyRecordError = (error, element) => {
        element.querySelector('.tspy-spy-btn').innerHTML = 'Oops!';
        element.querySelector('.tspy-spy-btn').classList.add('t-red');
        element.querySelector('.tspy-spy-btn').classList.remove('t-yellow');
        element.querySelector('.tspy-spy-btn').classList.remove('t-green');
        element.querySelector('.tspy-record-head').innerHTML = `<span class="t-red">Oops!</span> Something went wrong...`;
        element.querySelector('.tspy-record-body').innerHTML = `<span>${error}</span`;
        element.querySelector('.tspy-record-head-mobile').innerHTML = `<span class="t-red">Oops!</span>`;
        element.querySelector('.tspy-record-body-mobile').innerHTML = `<span class="t-red">${error}</span>`;

        element.querySelector('.tspy-record').style.display = 'flex';
        element.querySelector('.tspy-spy-details').style.display = 'none';
    };

    const updateSpyRecordView = (record, element) => {
        if(Object.keys(record.exposed).length > 3){
            element.querySelector('.tspy-spy-btn').classList.add("t-green")
            element.querySelector('.tspy-spy-btn').classList.remove("t-yellow")
            element.querySelector('.tspy-spy-btn').disabled = true;
            element.querySelector('.tspy-spy-btn').innerHTML = 'Full';
            element.querySelector('.tspy-record-head').innerHTML = `<span class="t-green">Full</span>`;
            element.querySelector('.tspy-record-body').innerHTML = `<span>${Object.keys(record.exposed).join(', ')}</span>`;
            element.querySelector('.tspy-record-head-mobile').innerHTML = `<span class="t-green">Full </span><div><strong>JP: </strong> ${record.pointsUsed}</div>`;
        }else{
            element.querySelector('.tspy-spy-btn').classList.add("t-yellow")
            element.querySelector('.tspy-spy-btn').classList.remove("t-green")
            element.querySelector('.tspy-record-head').innerHTML = `<span class="t-yellow">Partial</span>`;
            element.querySelector('.tspy-record-body').innerHTML = `<span>${Object.keys(record.exposed).join(', ')}</span>`;
            element.querySelector('.tspy-record-head-mobile').innerHTML = `<span class="t-yellow">Partial</span><div><strong>JP: </strong> ${record.pointsUsed}</div>`;
        }

        element.querySelector('.tspy-record').style.display = 'flex';
        //element.querySelector('.tspy-spy-details').style.display = 'none';

        element.querySelector('.tspy-record-head').innerHTML = element.querySelector('.tspy-record-head').innerHTML + `<span> (${new Date(record.timestamp).toLocaleString('en-GB', { timeZone: 'UTC' })})<span> <strong>JP used: </strong> ${record.pointsUsed}`;
        element.querySelector('.tspy-record-body-mobile').innerHTML = `<span>${new Date(record.timestamp).toLocaleString('en-GB', { timeZone: 'UTC' })}<span>`;
    };

    const updateSpyRecord = (spyDetails, user, amount) => {
        var record = GM_getValue(`${SPY_TRACKER}_${user.userID}`);

        if(!record){
            record = {
                timestamp: Date.now(),
                exposed: {},
                pointsUsed: 0
            }
        }else{
            record = JSON.parse(record);
        }

        record.timestamp = Date.now();

        for (const [key, value] of Object.entries(spyDetails)) {
            if(['money', 'moneyshow'].includes(key)){
                continue;
            }

            if(value !== 'N/A'){
                record.exposed[key] = value;

            }
        }

        record.pointsUsed += parseInt(amount);

        GM_setValue(`${SPY_TRACKER}_${user.userID}`, JSON.stringify(record));

        return record;
    };

    const capitalize = s => s && s[0].toUpperCase() + s.slice(1);

    /**
     * Settings controls
     */
    const hasApiKey = () => {
        return GM_getValue(TORN_SPY_KEY)?.length > 16;
    }

    const getApiKey = () => {
        return GM_getValue(TORN_SPY_KEY);
    }

    const getMiniBreakdown = () => {
        return GM_getValue(MINI_PROFILE_BREAKDOWN) ?? true;
    }

    const getPaymentTracker = () => {
        return GM_getValue(PAYMENT_TRACKER) ?? true;
    }

    const hasPaymentTracked = (tag) => {
        return GM_getValue(`${PAYMENT_TRACKER}_${tag}`) ?? false;
    }

    /**
     * Profile page support: https://www.torn.com/profiles.php?XID=*
     */
    if(window.location.pathname === '/profiles.php' && hasApiKey()) {
        waitForElement('.medals-wrapper').then(()=> {
            profile(profile_id());
        });
    }

    const profile = (id) => {
        GM_xmlhttpRequest(
            {
                method: 'POST',
                url: `${DOMAIN}/userscripts/profile`,
                data: JSON.stringify({'xid': id}),
                responseType: 'json',
                headers: { 'TORNSPY-KEY': getApiKey() },
                onload: (response) => {
                    response = response.response ?? JSON.parse(response.responseText);

                    if(response.style){
                        GM_addStyle(response.style);
                    }

                    setPermissions(response.permissions)

                    profile_page(response)

                    initializeTornTooltip($('.bonus-attachment-icons, .view-info'))
                    initializeTornItemInfo()
                }
            }
        );
    };

    const profile_page = (response) => {
        if(document.querySelector('.buttons-list')) {
            response.buttons.forEach((button) =>{document.querySelector('.buttons-list').insertAdjacentHTML('beforeend', button)});
        }
        document.getElementById('profileroot').querySelector('.medals-wrapper').insertAdjacentHTML('beforeBegin', response.loadout);
        document.getElementById('profileroot').querySelector('.medals-wrapper').insertAdjacentHTML('beforeBegin', response.statBar);
        attach_purchase_flow()
    }

    const profile_id = () => {
        return (new URLSearchParams(window.location.search)).get('XID');
    }

    /**
     * Mini profile support.
     */
    if(hasApiKey() && getMiniBreakdown()){
        waitForElement('.profile-mini-root').then(()=> {
            waitForElement('div[class^="profile-mini-_userImage"]').then(() => {
                mini_profile(mini_profile_id());
                const observer = new MutationObserver((mutationList, observer) => {
                    if(document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-"]')){
                        waitForElement('div[class^="profile-mini-_userImage"]').then(() => {
                            mini_profile(mini_profile_id());
                        });
                    }
                });
                observer.observe(document.getElementById('profile-mini-root'), { childList: true });
            });
        });
    }

    const mini_profile = (id) => {
        GM_xmlhttpRequest({
            method: 'POST',
            url: `${DOMAIN}/userscripts/mini-profile`,
            data: JSON.stringify({'xid': id}),
            responseType: 'json',
            headers: { 'TORNSPY-KEY': getApiKey() },
            onload: (response) => {
                response = response.response ?? JSON.parse(response.responseText);
                setPermissions(response.permissions)
                const height = getComputedStyle(document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-_userProfileWrapper"]')).height;
                if(document.getElementById("profile-mini-root").querySelector('.icons') && response.statBar) {
                    document.getElementById("profile-mini-root").querySelector('.icons').insertAdjacentHTML('beforebegin', response.statBar);
                }
                if(document.getElementById("profile-mini-root").querySelector('.buttons-list')){
                    response.buttons.forEach((button) =>{document.getElementById("profile-mini-root").querySelector('.buttons-list').insertAdjacentHTML('beforeend', button)});
                }
                if(document.getElementById("profile-mini-root").querySelector('div[class^="profile-mini-_wrapper___"]')) {
                    // Allow the mini profile to scale larger than the set height.
                    document.getElementById("profile-mini-root").querySelector('div[class^="profile-mini-_wrapper___"]').style.maxHeight = 'fit-content';
                }
                // Adjust height of the mini profile.
                if(getComputedStyle(document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-_arrow"]')).bottom === '-8px' && response.statBar){
                    const adjustedHeight = getComputedStyle(document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-_userProfileWrapper"]')).height;
                    const heightDiff = adjustedHeight.replace('px', '') - height.replace('px', '');
                    const top = document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-_wrapper"]').style.top.replace('px', '')
                    document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-_wrapper"]').style.top = `${(top - heightDiff)}px`;
                }
            }
        });
    };

    const mini_profile_id = () => {
        return document.querySelector('a[class^="profile-mini-_linkWrap___"][class*=" profile-mini-_flexCenter___"][href^="/profiles.php?XID="]').href.replace('https://www.torn.com/profiles.php?XID=', '');
    };

    /**
     * Faction page support: https://www.torn.com/factions.php
     */
    if(hasApiKey() && window.location.pathname === '/factions.php' && !document.getElementById('tspy-view-bazaar')) {
        waitForElement('.content-title').then(()=> {
            waitForElement('.faction-profile').then(() => {
                waitForElement('.f-war-list.members-list').then(()=> {
                    faction_profile();
                });
            });
        });
    }

    var purchase_overview_loaded = false;
    var spy_overview_loaded = false;
    var activeTable = 'default';
    const faction_profile = (ids) => {
        GM_xmlhttpRequest({
            method: 'POST',
            url: `${DOMAIN}/userscripts/faction/profile`,
            data: JSON.stringify({'fid': faction_id()}),
            responseType: 'json',
            headers: { 'TORNSPY-KEY': getApiKey() },
            onload: (response) => {
                response = response.response ?? JSON.parse(response.responseText);
                setPermissions(response.permissions)

                if(response.style){
                    GM_addStyle(response.style);
                }

                document.querySelector('.faction-info-wrap.restyle.another-faction').insertAdjacentHTML('beforeBegin', response.actions);

                document.getElementById('tspy-spy-purchase-icon').addEventListener("click", (event) => {
                    if(purchase_overview_loaded){
                        toggle_active_table('purchase');
                        toggle_purchase_overview();
                        activeTable = activeTable!=='purchase'?'purchase':'default';
                    }
                    if(!purchase_overview_loaded){
                        purchase_overview_loaded = true;
                        document.getElementById('tspy-spy-purchase').innerHTML = 'Loading...';
                        purchase_overview_load();
                    }
                });

                document.getElementById('tspy-spy-spy-icon').addEventListener("click", (event) => {
                    if(spy_overview_loaded){
                        toggle_active_table('spy');
                        toggle_spy_overview();
                        activeTable = activeTable!=='spy'?'spy':'default';
                    }
                    if(!spy_overview_loaded){
                        spy_overview_loaded = true;
                        document.getElementById('tspy-spy-spy').innerHTML = 'Loading...';
                        spy_overview_load();
                    }
                });
            }
        });
    };

    const toggle_purchase_overview = () => {
        const tspyPurchaseHead = document.getElementById('tspy-purchase-thead');
        if(tspyPurchaseHead){
            tspyPurchaseHead.style.display = (tspyPurchaseHead.style.display ==='none')?'flex':'none';
        }

        document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row').forEach((element) => {
            const spyPurchaseRow = element.querySelector('.tspy-purchase-row');

            element.querySelectorAll('.tspy-purchase-row').forEach((element)=>{
                element.style.display = (element.style.display)==='none'?'flex':'none';
            });
        });
    };

    const purchase_overview_load = () => {
        GM_xmlhttpRequest({
            method: 'POST',
            url: `${DOMAIN}/userscripts/faction/purchase`,
            data: JSON.stringify({'mids': member_ids()}),
            responseType: 'json',
            headers: { 'TORNSPY-KEY': getApiKey() },
            onload: (response) => {
                response = response.response ?? JSON.parse(response.responseText);
                setPermissions(response.permissions)
                purchase_overview(response);
                purchase_overview_loaded = true;
            }
        });
    };

    const purchase_overview = (response) => {
        toggle_active_table('purchase');
        activeTable = activeTable!=='purchase'?'purchase':'default';
        document.getElementById('tspy-spy-purchase').innerHTML = 'Purchase reports';
        document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').insertAdjacentHTML('beforeEnd', response.heading);
        document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row').forEach((row) => {
            const responseRow = response.spyRows[row.querySelector('a[href^="/profiles.php?XID="]').href.replace('https://www.torn.com/profiles.php?XID=', '')]
            row.insertAdjacentHTML('beforeEnd', responseRow.purchase);
            const reportHash = row.querySelector('.tspy-purchase-btn')?.dataset.tag;
            if(reportHash && hasPaymentTracked(reportHash)){
                row.querySelector(`.tspy-cta`).style.display = 'none';
                row.querySelector(`.tspy-purchase-details`).style.display = 'none';
                row.querySelector(`.tspy-complete`).style.display = 'block';
            }
            if(reportHash){
                row.querySelector(`.tspy-sale-btn`).addEventListener("click", (event) => {
                    row.querySelector(`.tspy-cta`).style.display = 'none';
                    row.querySelector(`.tspy-purchase-details`).style.display = 'none';
                    row.querySelector(`.tspy-purchase`).style.display = 'block';
                });
                row.querySelectorAll(`.tspy-cancel-btn`).forEach((element) => {
                    element.addEventListener('click', (event) => {
                        row.querySelector(`.tspy-purchase`).style.display = 'none';
                        row.querySelector(`.tspy-cta`).style.display = 'block';
                        row.querySelector(`.tspy-purchase-details`).style.display = 'block';
                    })
                });
                row.querySelectorAll(`.tspy-purchase-btn`).forEach((element) => {
                    element.addEventListener('click', (event) => {
                        event.srcElement.disabled = true;
                        event.srcElement.innerHTML = 'Sending payment...'
                        row.querySelectorAll(`.tspy-cancel-btn`).forEach((element) => {
                            element.style.display = 'none';
                        });
                        purchase('cash1', event.srcElement.dataset.target, event.srcElement.dataset.price, event.srcElement.dataset.tag, false, (response) => {
                            response = JSON.parse(response);
                            if(response.error){
                                row.querySelector(`.tspy-purchase`).style.display = 'none';
                                row.querySelector(`.tspy-error`).innerHTML = response.error.replace("area", '').replace('This', 'Buying reports');
                                row.querySelector(`.tspy-failure`).style.display = 'block';
                            }else if(response.success === false){
                                row.querySelector(`.tspy-error`).innerHTML = response.text.replace('<b>', '<b class="t-red">').replace('<b>', '<b class="t-green">');
                                row.querySelector(`.tspy-purchase`).style.display = 'none';
                                row.querySelector(`.tspy-failure`).style.display = 'block';
                            }else{
                                row.querySelector(`.tspy-purchase`).style.display = 'none';
                                row.querySelector(`.tspy-complete`).style.display = 'block';
                                if(getPaymentTracker()){
                                    GM_setValue(`${PAYMENT_TRACKER}_${event.srcElement.dataset.tag}`, Date.now())
                                }
                            }
                        });
                    })
                });
            }
        });
    };

    const spy_overview_load = () => {
        GM_xmlhttpRequest({
            method: 'POST',
            url: `${DOMAIN}/userscripts/faction/spy`,
            data: JSON.stringify({'mids': member_ids()}),
            responseType: 'json',
            headers: { 'TORNSPY-KEY': getApiKey() },
            onload: (response) => {
                response = response.response ?? JSON.parse(response.responseText);
                setPermissions(response.permissions)
                spy_overview(response);
                spy_overview_loaded = true;
            }
        });
    };

    const spy_overview = (response) => {
        toggle_active_table('spy');
        activeTable = activeTable!=='spy'?'spy':'default';
        document.getElementById('tspy-spy-spy').innerHTML = 'Spy';
        document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').insertAdjacentHTML('beforeEnd', response.heading);
        document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row').forEach((row) => {
            const userId = row.querySelector('a[href^="/profiles.php?XID="]').href.replace('https://www.torn.com/profiles.php?XID=', '');
            const responseRow = response.spyRows[userId]
            row.insertAdjacentHTML('beforeEnd', responseRow.spy);

            cachedSpyRecord(userId, row);

            row.querySelector(`.tspy-spy-btn`).addEventListener("click", (event) => {
                row.querySelector('.tspy-spy-btn').disabled = true;
                row.querySelector('.tspy-spy-btn').innerHTML = '...';
                spy(event.srcElement.dataset.id, event.srcElement.dataset.special, event.srcElement.dataset.amount, event.srcElement.dataset.spy, (response) => {
                    response = JSON.parse(response.responseText);

                    if(response.success === false){
                        updateSpyRecordError(response.text.replace("area", '').replace('This', 'Using your job special'), row);
                        return;
                    }

                    if(response.result.error){
                        updateSpyRecordError(response.result.error, row);
                        return;
                    }

                    row.querySelector('.tspy-spy-btn').disabled = false;
                    row.querySelector('.tspy-spy-btn').innerHTML = 'Spy';
                    updateSpyRecordView(
                        updateSpyRecord(response.result.msg, response.result.user, response.amount),
                        row
                    );
                    document.getElementById('tspy-spy-thead-amount').innerHTML = response.pointsLeft;
                    document.getElementById('tspy-spy-thead-amount').parentNode.style.display = 'block';
                });
            });
        });
    };

    const toggle_spy_overview = () => {
        const tspySpyHead = document.getElementById('tspy-spy-thead');
        if(tspySpyHead){
            tspySpyHead.style.display = (tspySpyHead.style.display ==='none')?'flex':'none';
        }
        document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row').forEach((element) => {
            element.querySelectorAll('.tspy-spy-row').forEach((element)=>{
                element.style.display = (element.style.display)==='none'?'flex':'none';
            });
        });
    };

    const toggle_active_table = (targetTable) => {
        if(activeTable === targetTable){
            toggle_table();
            return;
        }

        if(activeTable === 'purchase'){
            toggle_purchase_overview();
        }else if(activeTable === 'spy'){
            toggle_spy_overview();
        }else{
            toggle_table();
        }
    };

    const toggle_table = () => {
        const memberIcons = document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').querySelector('.member-icons');
        const position = document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').querySelector('.position');
        const days = document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').querySelector('.days');
        const status = document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').querySelector('.status');
        const lvl = document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').querySelector('.lvl');

        position.style.display = (position.style.display ==='none')?'flex':'none';
        days.style.display = (days.style.display ==='none')?'flex':'none';
        status.style.display = (status.style.display ==='none')?'flex':'none';

        if(!memberIcons.classList.contains('hide-mobile')){
            memberIcons.classList.add('hide-mobile');
            memberIcons.classList.add('hide-desktop');
        }else{
            memberIcons.classList.remove('hide-mobile');
            memberIcons.classList.remove('hide-desktop');
        }

        if(!lvl.classList.contains('hide-mobile')){
            lvl.classList.add('hide-mobile')
        }else{
            lvl.classList.remove('hide-mobile');
        }

        const memberRows = document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row');
        memberRows.forEach((element) => {
            const memberIconsInline = element.querySelector('.member-icons');
            const positionInline = element.querySelector('.position');
            const daysInline = element.querySelector('.days');
            const statusInline = element.querySelector('.status');
            const lvl = element.querySelector('.lvl');

            positionInline.style.display = (positionInline.style.display ==='none')?'flex':'none';
            daysInline.style.display = (daysInline.style.display ==='none')?'flex':'none';
            statusInline.style.display = (statusInline.style.display ==='none')?'flex':'none';

            if(!memberIconsInline.classList.contains('hide-mobile')){
                memberIconsInline.classList.add('hide-mobile');
                memberIconsInline.classList.add('hide-desktop');
            }else{
                memberIconsInline.classList.remove('hide-mobile');
                memberIconsInline.classList.remove('hide-desktop');
            }

            if(!lvl.classList.contains('hide-mobile')){
                lvl.classList.add('hide-mobile')
            }else{
                lvl.classList.remove('hide-mobile');
            }
        });
    };

    const faction_id = () => {
        const id = (new URLSearchParams(window.location.search)).get('ID');
        if(id){
            return id;
        }

        if(document.getElementById('top-page-links-list').querySelector('a[href^="/page.php?sid=factionWarfare#/ranked/"]')){
            return document.getElementById('top-page-links-list').querySelector('a[href^="/page.php?sid=factionWarfare#/ranked/"]').href.replace('https://www.torn.com/page.php?sid=factionWarfare#/ranked/', '')
        }

        return document.querySelector('.forum-thread').href.replace('https://www.torn.com/forums.php#!p=forums&f=999&b=1&a=', '');
    }

    const member_ids = () => {
        const mids = [];
        document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row').forEach((element)=>{
            mids.push(element.querySelector('a[href^="/profiles.php?XID="]').href.replace('https://www.torn.com/profiles.php?XID=', ''));
        })
        return mids;
    };

    /**
     * Settings page support: https://www.torn.com/preferences.php?tab=tornspy
     */
    if(window.location.pathname === '/preferences.php' && (new URLSearchParams(window.location.search)).get('tab') === 'tornspy'){
        waitForElement('.ui-tabs-nav').then(()=> {
            settings();
        });
    }

    const settings = (id) => {
        GM_xmlhttpRequest({
            method: 'POST',
            url: `${DOMAIN}/userscripts/settings`,
            responseType: 'json',
            onload: (response) => {
                response = response.response ?? JSON.parse(response.responseText);
                setPermissions(response.permissions)
                if(document.querySelector('.ui-tabs-nav')){
                    document.querySelector('#prefs-tab-menu').innerHTML = response.page;
                    document.querySelector('.prefs-tab-title').innerHTML = 'Torn Spy settings'
                    document.getElementById('tspy-apikey').value = getApiKey() ?? '';
                    document.getElementById('tspy-mini-bd-on').checked = getMiniBreakdown() ?? true;
                    document.getElementById('tspy-mini-bd-off').checked = !getMiniBreakdown() ?? false;
                    document.getElementById('tspy-payment-track-on').checked = getPaymentTracker() ?? true;
                    document.getElementById('tspy-payment-track-off').checked = !getPaymentTracker() ?? false;
                }
                if(document.getElementById('tspy-settings')){
                    document.getElementById('tspy-save-btn').addEventListener("click", () => {
                        if(document.getElementById('tspy-apikey').value.length <= 16){
                            document.getElementById('tspy-key-error').innerHTML = 'Oops! your key needs to be longer than 16 characters!';
                        }else{
                            document.getElementById('tspy-key-error').style.display = 'none';
                            GM_setValue(TORN_SPY_KEY, document.getElementById('tspy-apikey').value);
                        }
                        GM_setValue(MINI_PROFILE_BREAKDOWN, document.getElementById('tspy-mini-bd-on').checked);
                        document.getElementById('tspy-success').innerHTML = 'Settings updated!';
                    });
                    document.getElementById('tspy-reset-btn').addEventListener("click", () => {
                        GM_deleteValue(TORN_SPY_KEY);
                        GM_deleteValue(MINI_PROFILE_BREAKDOWN);
                        document.getElementById('tspy-apikey').value = '';
                        document.getElementById('tspy-success').innerHTML = 'Torn Spy integration settings reset!';
                    });
                }
                if(document.getElementById('tspy-ptrack-clear-btn')){
                    document.getElementById('tspy-ptrack-clear-btn').addEventListener("click", () => {
                        GM_listValues().forEach((value) => {
                            if(value.startsWith(`${PAYMENT_TRACKER}_`)){
                                GM_deleteValue(value);
                            }
                        })
                        document.getElementById('tspy-ptrack-message').innerHTML = 'Tracked payments cleared!'
                    });
                }
            }
        });
    };

    /**
     * Icon support
     */
    waitForElement('ul[class^="status-icons"').then(()=> {
        const icon = `<li id="tspy-icon" style="background-image:url('https://www.tornspy.com/assets/img/favicon.ico');"><a href="https://www.torn.com/preferences.php?tab=tornspy" aria-label="Torn Spy" tabindex="0" data-is-tooltip-opened="false"></a></li>`;
        const existingIcon = document.querySelector('ul[class^="status-icons"]')?.querySelector('#tspy-icon');
        if (existingIcon) {
            existingIcon.remove();
        }
        document.querySelector('ul[class^="status-icons"]').insertAdjacentHTML('beforeEnd', icon)

        const observer = new MutationObserver((mutationList, observer) => {
            if(document.querySelector('ul[class^="status-icons"]')?.querySelector('#tspy-icon')){
                return;
            }

            document.querySelector('ul[class^="status-icons"]').insertAdjacentHTML('beforeEnd', icon)
        });
        observer.observe(document.getElementById('sidebarroot'), { childList: true, subtree: true });
    });

    /**
     * No API key is set prompt.
     */
    if(!hasApiKey()){
        document.querySelector('.content-title').insertAdjacentHTML('afterEnd', `<div class="info-msg-cont green border-round m-top10">
		<div class="info-msg border-round">
			<i class="info-icon"></i>
			<div class="delimiter">
				<div class="msg right-round" role="alert" aria-live="polite">
					<ul><li>Torn Spy API key is not set, click <a class="t-blue h" href="preferences.php?tab=tornspy">here</a> to set your API Key!</li></ul>
				</div>
			</div>
		</div>
	</div>`);
    }

    /**
     * Attack / Loadout support
     */
    if(hasApiKey() && hasPermission('loadouts') && window.location.pathname === '/page.php' && (new URLSearchParams(window.location.search)).get('sid') === 'attack') {
        waitForElement('#react-root').then(()=> {
            waitForElement('div[class^="playerWindow"').then(()=> {
                if(hasPermission('gun_shop')){
                    window.addEventListener("fetch-listener", sendLoadoutToTornspy);
                }else{
                    const fightButton = document.getElementById('react-root').querySelector('button[class^="torn-btn"]')
                    if(fightButton && fightButton.innerText === 'START FIGHT') {
                        fightButton.addEventListener("click", (event) => {
                            window.addEventListener("fetch-listener", sendLoadoutToTornspy);
                        });
                    }
                }
            });
        });

        function addFetchListener() {
            const originalFetch = unsafeWindow.fetch;
            unsafeWindow.fetch = async (url, init = {}) => {
                const response = await originalFetch(url, init);
                const clonedResponse = response.clone();

                clonedResponse.text().then((text) => {
                    window.dispatchEvent(new CustomEvent(`fetch-listener`, {
                        detail: {
                            url,
                            body: init.body || null,
                            response: text
                        }
                    }));
                }).catch(console.error);

                return response;
            };
        }
        addFetchListener()
    }

    function sendLoadoutToTornspy(event) {
        const payload = JSON.parse(event.detail.response)

        if(["started", "end"].includes(payload.DB?.attackStatus) || payload.DB?.showEnemyItems){
            GM_xmlhttpRequest({
                method: 'POST',
                url: `${DOMAIN}/userscripts/loadout/add`,
                data: JSON.stringify(mapAttackData(payload)),
                responseType: 'json',
                headers: { 'TORNSPY-KEY': getApiKey() },
                onload: (response) => {
                    response = response.response ?? JSON.parse(response.responseText);
                    setPermissions(response.permissions)
                    const title = document.querySelector('h4[class^="title"]')
                    title.insertAdjacentHTML('afterend', response.status);
                }
            });
            window.removeEventListener("fetch-listener", sendLoadoutToTornspy);
        }
    }

    const mapAttackData = (attackDataPayload) => {
        return {
            fightId: attackDataPayload.DB.fightID,
            target: attackDataPayload.DB.defenderUser.userID,
            maxLife: attackDataPayload.DB.defenderUser.maxlife,
            modifiers: {
                strength: attackDataPayload.DB.defenderUser.statsModifiers.strength.value,
                speed: attackDataPayload.DB.defenderUser.statsModifiers.speed.value,
                dexterity: attackDataPayload.DB.defenderUser.statsModifiers.dexterity.value,
                defense: attackDataPayload.DB.defenderUser.statsModifiers.defense.value,
                damage: attackDataPayload.DB.defenderUser.statsModifiers.damage.value,
            },
            loadout: Object.entries(attackDataPayload.DB.defenderItems).flatMap(
                ([slot, data]) => data.item.map(item => ({
                    itemId: item.ID,
                    armoryId: item.armoryID,
                    ammoType: item.ammotype,
                    ammoPreference: item.equipSlot == '1' ? attackDataPayload.DB.defenderAmmoPreferences.primary :
                        item.equipSlot == '2' ? attackDataPayload.DB.defenderAmmoPreferences.secondary : 0,
                    upgrades: item.currentUpgrades?.map(upgrade => ({
                        upgradeId: upgrade.upgradeID,
                        icon: upgrade.icon
                    }))
                }))
            )
        }
    }

    /**
     * Marketplace support
     */
    if(hasApiKey() && window.location.pathname === '/page.php' &&
        (new URLSearchParams(window.location.search)).get('sid') === 'ItemMarket' &&
        !window.__tspyMarketHooked
    ) {
        window.__tspyMarketHooked = true;
        waitForElement('#item-market-root').then(()=> {
            waitForElement('[class^="categoriesWrapper"]').then(()=> {
                waitForElement('[class^="most-popular"]').then(()=> {
                    marketButton()
                    if((new URLSearchParams(window.location.search)).get('tspy') === 'true'){
                        marketplaceRequest()
                    }
                })
            })
        });
    }

    const marketButton = () => {
        const existing = document.getElementById('tspy-marketplace-wrapper');
        if (existing) {
            existing.remove();
        }

        const mostPopularButton = document.querySelector('[class^="mostPopular"]');
        const tornSpyMarketWrapper = mostPopularButton.cloneNode(true)
        tornSpyMarketWrapper.id = "tspy-marketplace-wrapper";
        [...tornSpyMarketWrapper.attributes].forEach(attr => {
            if (['id', 'class'].includes(attr.name) === false) {
                tornSpyMarketWrapper.removeAttribute(attr.name);
            }
        });
        tornSpyMarketWrapper.style.borderRadius = "var(--item-market-border-radius)";
        tornSpyMarketWrapper.querySelector('[class^="most-popular"]').outerHTML = detective_svg()

        const tornSpyMarketButton = tornSpyMarketWrapper.querySelector('button');
        tornSpyMarketButton.id = "tspy-marketplace-button";
        [...tornSpyMarketButton.attributes].forEach(attr => {
            if (['id', 'class'].includes(attr.name) === false) {
                tornSpyMarketButton.removeAttribute(attr.name);
            }
        });

        tornSpyMarketButton.classList.forEach((classname) => {
            if(classname.includes('selected')){
                tornSpyMarketButton.classList.remove(classname);
            }
        })
        tornSpyMarketButton.style.background = "var(--shared-category-icon-background);";
        tornSpyMarketButton.style.color = "var(--item-market-color);";
        if(tornSpyMarketButton.querySelector('[class^="title"]')){
            tornSpyMarketButton.querySelector('[class^="title"]').innerHTML = 'Torn Spy Marketplace'
        }

        mostPopularButton.insertAdjacentHTML('afterEnd', tornSpyMarketWrapper.outerHTML);

        document.getElementById('tspy-marketplace-wrapper').addEventListener("click", (event) => {
            if(has_loaded('marketplace')){
                set_display(document.querySelector('[class^="marketWrapper"]'), false)
                set_display(document.querySelector('[class^="tspy-marketplace-wrapper"]'), true)
                set_display(document.querySelector('[class^="appHeaderWrapper"]'), false)
                set_display(document.querySelector('[class^="app-header-wrapper"]'), true)
            }else{
                marketplaceRequest()
            }
            const currentPanel = get_current_panel()
            const url = new URL(window.location.href);
            url.searchParams.set('tspy', true);
            url.searchParams.set('tspy-panel', currentPanel !== null?currentPanel:'welcome-panel');
            window.history.replaceState({}, "", url);
        })
    };

    const get_current_panel = () => {
        const panels = document.getElementById('tspy-marketplace')?.querySelectorAll('.panel');

        if(!panels){
            return null
        }

        for (const element of panels) {
            if (!element.classList.contains('d-none')) {
                return element.id;
            }
        }

        return null;
    };

    const marketplace = (response) => {
        set_display(document.querySelector('[class^="marketWrapper"]'), false)
        set_display(document.querySelector('[class^="appHeaderWrapper"]'), false)
        document.querySelector('[class^="appHeaderWrapper"]').insertAdjacentHTML('beforeBegin', response.header);
        document.querySelector('[class^="marketWrapper"]').insertAdjacentHTML('beforeBegin', response.marketplace);
        if(isMobile()){
            const categories = document.querySelector('.tspy-marketplace-wrapper').querySelector('.tspy-category-wrapper')
            document.querySelector('.tspy-marketplace-wrapper').insertAdjacentHTML('afterBegin', categories.outerHTML);
            categories.outerHTML = '';
        }
        load_state_from_url()
        global_search_input()
        document.querySelectorAll('.tspy-category-wrapper [id^="tspy-marketplace-"]').forEach(el => {
            const match = el.id.startsWith('tspy-marketplace-');
            if (!match) {
                return;
            }

            const key = el.id.replace('tspy-marketplace-', '') + '-panel';

            el.addEventListener("click", () => {
                loaders[key]?.();
            });
        });
        document.getElementById('tspy-marketplace-back-button').addEventListener("click", (event) => {
            set_display(document.querySelector('[class^="marketWrapper"]'), true)
            set_display(document.querySelector('[class^="tspy-marketplace-wrapper"]'), false)
            set_display(document.querySelector('[class^="appHeaderWrapper"]'), true)
            set_display(document.querySelector('[class^="app-header-wrapper"]'), false)
            const url = new URL(window.location.href);
            url.searchParams.delete('tspy');
            url.searchParams.delete('tspy-query');
            url.searchParams.delete('tspy-panel');
            window.history.replaceState({}, "", url);
        })
        attach_create_request_flow()
    };

    const marketplaceRequest = () => {
        set_loading(document.getElementById('tspy-marketplace-button'), true, isMobile()?'...':'Loading...')
        GM_xmlhttpRequest(
            {
                method: 'POST',
                url: `${DOMAIN}/userscripts/marketplace`,
                responseType: 'json',
                headers: { 'TORNSPY-KEY': getApiKey() },
                onload: (response) => {
                    response = response.response ?? JSON.parse(response.responseText);

                    if(response.style){
                        GM_addStyle(response.style);
                    }

                    setPermissions(response.permissions)

                    set_loaded('marketplace')
                    set_loaded('requests-create-overview-panel')
                    set_loading(document.getElementById('tspy-marketplace-button'), false, isMobile()?detective_svg():'Torn Spy Marketplace')
                    marketplace(response)
                },
            }
        );
    };

    const createOverviewLoader = ({panelId, loadFn, loadArgs = [] }) => {
        return () => {
            const errorPanel = document.getElementById('make-request-error-panel');
            if(errorPanel){
                errorPanel.outerHTML = ''
            }

            if (has_loaded(panelId)) {
                show_marketplace_panel(panelId)
            } else {
                loadFn(...(loadArgs.map(arg => typeof arg === 'function' ? arg() : arg)))
            }
        }
    }

    const marketplace_request_overview = (data, requestOverviewType) => {
        set_loading(document.getElementById(`tspy-marketplace-requests-${requestOverviewType}-overview`), true, 'Loading...')
        GM_xmlhttpRequest(
            {
                method: 'POST',
                url: `${DOMAIN}/userscripts/marketplace/request/${requestOverviewType}/overview`,
                data: JSON.stringify(data),
                responseType: 'json',
                headers: { 'TORNSPY-KEY': getApiKey() },
                onload: (response) => {
                    response = response.response ?? JSON.parse(response.responseText);
                    document.getElementById('tspy-marketplace').insertAdjacentHTML('afterBegin', response.panel)
                    show_marketplace_panel(`requests-${requestOverviewType}-overview-panel`)
                    set_loaded(`requests-${requestOverviewType}-overview-panel`)
                    set_loading(document.getElementById(`tspy-marketplace-requests-${requestOverviewType}-overview`), false, `${capitalize(requestOverviewType)} requests`)

                    attach_pagination(paginated_requests(requestOverviewType));
                    if(requestOverviewType === 'available'){
                        attach_accept_request_flow()
                    }

                    if(requestOverviewType === 'created'){
                        attach_purchase_flow()
                    }

                    if(requestOverviewType === 'accepted'){
                        attach_spy_request_flow()
                    }
                },
            }
        );
    }

    const reports_overview = (data, reportType) => {
        const reportSelector = reportType.replace('_', '-')
        set_loading(document.getElementById(`tspy-marketplace-${reportSelector}-overview`), true, 'Loading...')
        GM_xmlhttpRequest(
            {
                method: 'POST',
                url: `${DOMAIN}/userscripts/marketplace/reports/${reportType}`,
                data: JSON.stringify(data),
                responseType: 'json',
                headers: { 'TORNSPY-KEY': getApiKey() },
                onload: (response) => {
                    response = response.response ?? JSON.parse(response.responseText);

                    if(document.getElementById(`${reportSelector}-overview-panel`)){
                        document.getElementById(`${reportSelector}-overview-panel`).outerHTML = response.panel
                    }else{
                        document.getElementById('tspy-marketplace').insertAdjacentHTML('beforeEnd', response.panel)
                    }

                    show_marketplace_panel(`${reportSelector}-overview-panel`)
                    set_loaded(`${reportType.replace('_', '-')}-overview-panel`)
                    set_loading(document.getElementById(`tspy-marketplace-${reportSelector}-overview`), false, `${capitalize(reportType.replace('_', ' '))}s`)

                    attach_purchase_flow()
                    attach_copy()
                    attach_pagination(paginated_reports(reportType));
                    initializeTornItemInfo(false)
                },
            }
        );
    }

    const loaders = {
        'requests-create-overview-panel': createOverviewLoader({
            panelId: 'requests-create-overview-panel',
            loadFn: marketplace_request_overview,
            loadArgs: [{}, 'create']
        }),
        'requests-created-overview-panel': createOverviewLoader({
            panelId: 'requests-created-overview-panel',
            loadFn: marketplace_request_overview,
            loadArgs: [{}, 'created']
        }),
        'requests-accepted-overview-panel': createOverviewLoader({
            panelId: 'requests-accepted-overview-panel',
            loadFn: marketplace_request_overview,
            loadArgs: [{}, 'accepted']
        }),
        'requests-available-overview-panel': createOverviewLoader({
            panelId: 'requests-available-overview-panel',
            loadFn: marketplace_request_overview,
            loadArgs: [{}, 'available']
        }),
        'requests-failed-overview-panel': createOverviewLoader({
            panelId: 'requests-failed-overview-panel',
            loadFn: marketplace_request_overview,
            loadArgs: [{}, 'failed']
        }),
        'requests-archived-overview-panel': createOverviewLoader({
            panelId: 'requests-archived-overview-panel',
            loadFn: marketplace_request_overview,
            loadArgs: [{}, 'archived']
        }),
        'stat-report-overview-panel': createOverviewLoader({
            panelId: 'stat-report-overview-panel',
            loadFn: reports_overview,
            loadArgs: [() => ({ query: document.getElementById('tspy-global-search-input').value }), 'stat_report']
        }),
        'loadout-report-overview-panel': createOverviewLoader({
            panelId: 'loadout-report-overview-panel',
            loadFn: reports_overview,
            loadArgs: [() => ({ query: document.getElementById('tspy-global-search-input').value }), 'loadout_report']
        }),
        'references-report-overview-panel': createOverviewLoader({
            panelId: 'references-report-overview-panel',
            loadFn: reports_overview,
            loadArgs: [() => ({ query: document.getElementById('tspy-global-search-input').value }), 'references_report']
        }),
        'friendorfoe-report-overview-panel': createOverviewLoader({
            panelId: 'friendorfoe-report-overview-panel',
            loadFn: reports_overview,
            loadArgs: [() => ({ query: document.getElementById('tspy-global-search-input').value }), 'friendorfoe_report']
        })
    }

    const load_state_from_url = () => {
        const params = new URLSearchParams(window.location.search)

        const query = params.get('tspy-query')
        document.getElementById('tspy-global-search-input').value = query || ''

        const panel = params.get('tspy-panel')

        if (loaders[panel]) {
            loaders[panel]()
        }
    }

    const attach_create_request_flow = () => {
        set_display(document.getElementById('tspy-spy-report-select'), false)
        document.getElementById('tspy-report-type-select').addEventListener("change", (event) => {
            if(document.getElementById('tspy-report-type-select').value === 'stat_report'){
                set_display(document.getElementById('tspy-spy-report-select'), true)
                return
            }

            set_display(document.getElementById('tspy-spy-report-select'), false)
        })
        document.getElementById('finalise-button').addEventListener("click", (event) => {
            const spyReportValue = document.getElementById('tspy-spy-report-type-select').value === 'Select spy report type...'?null:document.getElementById('tspy-spy-report-type-select').value
            const deadline = document.getElementById('tspy-deadline-select').value === 'Select deadline...'?null:document.getElementById('tspy-deadline-select').value
            validateRequest({
                reportType: document.getElementById('tspy-report-type-select').value,
                spyReportType: spyReportValue,
                deadline: deadline,
                pricePerReport: document.getElementById('tspy-price-per-report-input').value,
                targets: [...document.getElementById('tspy-targets').querySelectorAll('[data-id]')].map(el => el.dataset.id)
            });
        })
        search_input()
    }

    const reset_create_request_flow = () => {
        document.getElementById("tspy-report-type-select").selectedIndex = 0;
        document.getElementById("tspy-spy-report-type-select").selectedIndex = 0;
        document.getElementById("tspy-deadline-select").selectedIndex = 0;
        document.getElementById("tspy-price-per-report-input").value = '';
        document.getElementById("tspy-search-input").value = '';
        document.getElementById("tspy-targets").innerHTML = '';
    }

    const paginated_reports = (reportType) => {
        return function (page, query){
            reports_overview({
                page: page,
                query: query
            }, reportType)
        }
    }

    const paginated_requests = (requestOverviewType) => {
        return function (page, query){
            marketplace_request_overview({
                page: page,
                query: query
            }, requestOverviewType)
        }
    }


    const attach_pagination = (callback) => {
        const query = document.getElementById('tspy-global-search-input').value
        document.querySelectorAll('.page-number')?.forEach(element => {
            element.addEventListener("click", (event) => {
                const page = parseInt(event.currentTarget.querySelector('.page-nb').innerHTML)
                const currentPage = parseInt(document.querySelector('.page-number.active').querySelector('.page-nb').innerHTML)
                if(Number.isInteger(page) && page != currentPage){
                    callback(page, query)
                }
                event.preventDefault()
            })
        })
        document.querySelector('.pagination-left')?.addEventListener("click", (event) => {
            const page = parseInt(document.querySelector('.page-number.active').querySelector('.page-nb').innerHTML)
            if(Number.isInteger(page) && page != 1){
                callback(page - 1, query)
            }
            event.preventDefault()
        })
        document.querySelector('.pagination-right')?.addEventListener("click", (event) => {
            const page = parseInt(document.querySelector('.page-number.active').querySelector('.page-nb').innerHTML)
            const lastPage = parseInt(document.querySelector('.page-number.last')?.querySelector('.page-nb').innerHTML)
            if(Number.isInteger(page) && Number.isInteger(lastPage) && page != lastPage){
                callback(page + 1, query)
            }
            event.preventDefault()
        })
    }

    const attach_spy_request_flow = () => {
        document.querySelectorAll('.tspy-spy-row').forEach((row) => {
            const button = row.querySelector('.tspy-spy-details .tspy-spy-btn')
            const cacheId = button.dataset.cacheId;

            cachedSpyRecord(cacheId, row);

            button.addEventListener("click", (event) => {
                set_loading(button, true, '...')
                spy(button.dataset.id, button.dataset.special, button.dataset.amount, button.dataset.spy, (response) => {
                    response = JSON.parse(response.responseText);

                    if(response.success === false) {
                        updateSpyRecordError(response.text.replace("area", '').replace('This', 'Using your job special'), row);
                        return;
                    }

                    if(response.result.error) {
                        updateSpyRecordError(response.result.error, row);
                        return;
                    }

                    set_loading(button, false, 'Spy')
                    updateSpyRecordView(
                        updateSpyRecord(response.result.msg, response.result.user, response.amount),
                        row
                    );
                });
            });
        });

        document.querySelectorAll('[data-toggle]').forEach((toggle) => {
            toggle.addEventListener("click", (event) => {
                toggle_display(document.querySelector('[data-toggle-id="'+event.srcElement.dataset.toggle+'"]'))
            });
        })
    }

    const attach_accept_request_flow = () => {
        document.querySelectorAll('.request').forEach((request) => {
            request.querySelector('.buy-button')?.addEventListener("click", (event) => {
                set_display(request.querySelector('.request-item-overview'), false)
                set_display(request.querySelector('.request-item-accept'), true)
            })
            request.querySelector('.tspy-accept-btn')?.addEventListener("click", (event) => {
                acceptRequest(request, event.target.dataset.requestId)
            })
            request.querySelector('.tspy-cancel-btn')?.addEventListener("click", (event) => {
                set_display(request.querySelector('.request-item-overview'), true)
                set_display(request.querySelector('.request-item-accept'), false)
            })
        });
    }

    const acceptRequest = (requestElement, requestId) => {
        GM_xmlhttpRequest(
            {
                method: 'POST',
                url: `${DOMAIN}/userscripts/marketplace/request/${requestId}/accept`,
                responseType: 'json',
                headers: { 'TORNSPY-KEY': getApiKey() },
                onload: (response) => {
                    response = response.response ?? JSON.parse(response.responseText);
                    requestElement.insertAdjacentHTML('beforeEnd', response.result)
                    set_display(requestElement.querySelector('.request-item-overview'), false)
                    set_display(requestElement.querySelector('.request-item-accept'), false)
                    requestElement.querySelector('.tspy-okay-btn')?.addEventListener("click", (event) => {
                        set_display(requestElement.querySelector('.request-item-overview'), true)
                        set_display(requestElement.querySelector('.request-item-accept'), false)
                        requestElement.querySelector('.request-item-accept-error').outerHTML = '';
                    })
                    requestElement.querySelector('.tspy-success-okay-btn')?.addEventListener("click", (event) => {
                        requestElement.remove()
                    })
                },
            }
        );
    }

    const validateRequest = (data) => {
        GM_xmlhttpRequest(
            {
                method: 'POST',
                url: `${DOMAIN}/userscripts/marketplace/request/validate`,
                data: JSON.stringify(data),
                responseType: 'json',
                headers: { 'TORNSPY-KEY': getApiKey() },
                onload: (response) => {
                    response = response.response ?? JSON.parse(response.responseText);
                    set_display(document.getElementById('requests-create-overview-panel'), false)
                    if(response.error){
                        document.getElementById('tspy-marketplace').insertAdjacentHTML('afterBegin', response.error)
                        error_panel()
                    }
                    if(response.success){
                        document.getElementById('tspy-marketplace').insertAdjacentHTML('afterBegin', response.success)
                        success_panel()
                        reset_create_request_flow()
                    }
                },
            }
        );
    }

    const success_panel = () => {
        document.getElementById('success-button').addEventListener("click", (event) => {
            loaders['requests-available-overview-panel']()
        })
    }

    const error_panel = () => {
        document.getElementById('error-back-button').addEventListener("click", (event) => {
            document.getElementById('make-request-error-panel').remove()
            set_display(document.getElementById('requests-create-overview-panel'), true)
        })
    }

    const global_search_input = () => {
        let searchTimer;
        const doneTypingDelay = 500;
        const input = document.getElementById('tspy-global-search-input')

        input.addEventListener('keyup', () => {
            clearTimeout(searchTimer)
            searchTimer = setTimeout(handle_global_search, doneTypingDelay)
        });

        input.addEventListener('keydown', () => {
            clearTimeout(searchTimer)
        });
    }

    const handle_global_search = () => {
        const current_panel = get_current_panel();
        const reportType = current_panel.replace('-overview-panel','').replace('-','_')

        const query = document.getElementById('tspy-global-search-input').value
        reports_overview({
            page: document.getElementById(current_panel).querySelector('.page-number.active')?.querySelector('.page-nb').innerHTML,
            query: query
        }, reportType)

        if(query.trim() === ''){
            const url = new URL(window.location.href);
            url.searchParams.delete('tspy-query');
            window.history.replaceState({}, "", url);
        }else{
            const url = new URL(window.location.href);
            url.searchParams.set('tspy-query', query);
            window.history.replaceState({}, "", url);
        }
    }

    const search_input = () => {
        let searchTimer;
        const doneTypingDelay = 500;
        const input = document.getElementById('tspy-search-input')

        input.addEventListener('keyup', () => {
            clearTimeout(searchTimer)
            searchTimer = setTimeout(handle_search, doneTypingDelay)
        });

        input.addEventListener('keydown', () => {
            clearTimeout(searchTimer)
        });

        document.addEventListener('click', function(event) {
            if(event.target.id !== 'tspy-search-input' && event.target.classList.contains('search-item') === false){
                set_display(document.getElementById('tspy-search-result'), false)
            }
        });

        ['click', 'focus', 'input'].forEach(event => {
            document.getElementById('tspy-search-input').addEventListener(event, function(event) {
                set_display(document.getElementById('tspy-search-result'), true)
            })
        })
    };

    const handle_search = () => {
        search(document.getElementById('tspy-search-input').value, (response) => {
            response = JSON.parse(response);
            document.getElementById('tspy-search-result').innerHTML = "";;
            response.list.forEach((result) => {
                document.getElementById('tspy-search-result').insertAdjacentHTML('afterBegin', "<div class=\"search-item center\" data-id=\""+result.id+"\">"+result.name+" ["+result.id+"]</div>");
            })
            document.querySelectorAll('.search-item').forEach((item) => {
                item.addEventListener("click", () => {
                    if(document.getElementById('tspy-targets').querySelector(`[data-id="${item.dataset.id}"`)){
                        document.getElementById('tspy-targets').querySelector(`[data-id="${item.dataset.id}"`).outerHTML = '';
                    }else{
                        const selection = item.cloneNode(true);
                        selection.classList.add('character-item')
                        selection.setAttribute('title', 'Click to remove target');
                        document.getElementById('tspy-targets').insertAdjacentHTML('beforeEnd', selection.outerHTML);
                        initializeTornTooltip(document.getElementById('tspy-targets').querySelectorAll('.character-item'))
                        document.getElementById('tspy-targets').querySelector('[data-id="'+item.dataset.id+'"]').addEventListener("click", (event) => {
                            event.target.remove()
                            document.querySelector('.ui-tooltip-content').outerHTML = '';
                        })
                        item.remove()
                    }
                })
            })
        });
    }

    const show_marketplace_panel = (panelId) => {
        document.getElementById('tspy-marketplace').querySelectorAll('.panel').forEach(element => {
            if(element.id !== panelId){
                set_display(element, false)
            }
        });

        if(document.getElementById(panelId).dataset.search){
            set_display(document.getElementById('tspy-global-search'), true)
        }else{
            set_display(document.getElementById('tspy-global-search'), false)
        }
        set_display(document.getElementById(panelId), true)
        const url = new URL(window.location.href);
        url.searchParams.set('tspy', true);
        url.searchParams.set('tspy-panel', panelId);
        window.history.replaceState({}, "", url);
    }

    /**
     * Events page support
     */
    if(hasApiKey() && window.location.pathname === '/page.php' && (new URLSearchParams(window.location.search)).get('sid') === 'events') {
        waitForElement('#events-root').then(()=> {
            waitForElement('[class^="appHeaderWrapper"]').then(()=> {
                event_button()
                if((new URLSearchParams(window.location.search)).get('tspy') === 'true'){
                    eventRequest()
                }
            });
        });
    }

    const event_button = () => {
        const logButton = document.querySelector('[class^="linksContainer"]')
        const tornSpyEventsButton = logButton.cloneNode(true)
        tornSpyEventsButton.id = "tspy-events-button";

        [...tornSpyEventsButton.attributes].forEach(attr => {
            if (['id', 'class'].includes(attr.name) === false) {
                tornSpyEventsButton.removeAttribute(attr.name);
            }
        });

        const aElement = tornSpyEventsButton.querySelector('a');
        [...aElement.attributes].forEach(attr => {
            if (['id', 'class'].includes(attr.name) === false) {
                aElement.removeAttribute(attr.name);
            }
        });

        tornSpyEventsButton.querySelector('[class^="iconWrapper"]').innerHTML = detective_svg()
        tornSpyEventsButton.querySelector('[class^="linkTitle"]').innerHTML = 'Torn spy'

        logButton.insertAdjacentHTML('beforeBegin', tornSpyEventsButton.outerHTML);

        document.getElementById('tspy-events-button').addEventListener("click", (event) => {
            if(has_loaded('events')){
                toggle_display(document.querySelector('[class^="eventsListWrapper"]'))
                toggle_display(document.querySelector('[class^="events-list-wrapper"]'))
            }else{
                eventRequest()
            }
        })
    };

    const eventRequest = () => {
        set_loading(document.getElementById('tspy-events-button'), true, 'Loading...')
        GM_xmlhttpRequest(
            {
                method: 'POST',
                url: `${DOMAIN}/userscripts/events`,
                responseType: 'json',
                headers: { 'TORNSPY-KEY': getApiKey() },
                onload: (response) => {
                    response = response.response ?? JSON.parse(response.responseText);

                    if(response.style){
                        GM_addStyle(response.style);
                    }

                    set_loaded('events')
                    set_loading(document.getElementById('tspy-events-button'), true, 'Torn Spy')
                    events(response)
                },
            }
        );
    };

    const events = (response) => {
        set_display(document.querySelector('[class^="eventsListWrapper"]'), false)
        document.querySelector('[class^="eventsListWrapper"]').insertAdjacentHTML('beforeBegin', response.events);
    }

})();