KadWatch

Kadoatery refresh tracker — Main/Mini timers, board copy, lock detection, food logging.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         KadWatch
// @version      0.31
// @description  Kadoatery refresh tracker — Main/Mini timers, board copy, lock detection, food logging.
// @author       Ryan (ext1nct)
// @match        http*://*.neopets.com/games/kadoatery/*
// @icon         https://itemdb.com.br/api/cache/preview/7f18f78e35daa6.png
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// @namespace    https://greasyfork.org/users/custom
// ==/UserScript==
/* eslint-env jquery */

/**
 * Stored data keys
 */
const LAST_REFRESH_TIME_KEY = "lastRefreshTime"
const MAIN_KAD_DATA_KEY = "mainKadData"
const MAIN_HISTORY_KEY = "mainHistory"
const MINI_REFRESH_TIMES_KEY = "miniRefreshTimes"
const KAD_STATES_KEY = "kadStates"
const NOTIFICATION_OPT_IN_KEY = "notificationOptIn"
const KAD_FIRST_RUN_KEY = "kadFirstRun"
const PENDING_DROP_KEY = "pendingDrop"

const DURATION_UNTIL_START_OF_MAIN_WINDOW_MS = 2100000 // 35 minutes
const DURATION_OF_MAIN_WINDOW_MS = 60000 // 60 seconds
const DURATION_OF_PENDING_WINDOWS_MS = 60000 // 60 seconds
const MAXIMUM_TIME_BETWEEN_REFRESHES_MS = 4680000

let notificationsTriggered = { main: 0, minis: {} };
let userIsLockedOut = false;

function censorFood(name) {
    return name
        .replace(/ball/gi, "b.all")
        .replace(/crack/gi, "crac.k")
        .replace(/weed/gi, "w.eed")
        .replace(/rape/gi, "r.ape")
        .replace(/cum/gi, "c.um");
}

// Inject KadWatch CSS
const STYLES = `
<style>
    .kad-modern-wrapper { margin: 8px auto 16px auto; max-width: 850px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; color: #334155; }
    .kad-panel { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); padding: 8px 12px; display: flex; flex-direction: column; gap: 6px; }
    .kad-alert { background: #fef2f2; color: #991b1b; padding: 6px; border-radius: 4px; border: 1px solid #fecaca; margin-bottom: 6px; text-align: center; font-weight: 500; font-size: 12px; }
    .kad-alert-warning { background: #fef9c3; color: #854d0e; border-color: #fde047; }

    .kad-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 8px; margin-bottom: 2px; }
    .kad-toolbar-group { display: flex; align-items: center; gap: 8px; }

    .kad-tracker-block { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 14px 12px 12px 12px; position: relative; margin-top: 4px; display: flex; flex-direction: column; align-items: center; }
    .kad-tracker-block.kad-main-block { border-color: #f0b8cc; background: #fdf5f8; }
    .kad-block-label { position: absolute; top: 6px; left: 10px; font-weight: 700; font-size: 11px; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; max-width: calc(100% - 160px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .kad-last-refresh { font-size: 11px; color: #94a3b8; margin-top: 4px; }

    .kad-countdown-box { font-size: 18px; font-weight: 700; color: #0f172a; margin-top: 6px; display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; }
    .kad-time-hl { color: #ef4444; font-size: 22px; font-variant-numeric: tabular-nums; letter-spacing: 0.5px; }
    .kad-time-sub { font-size: 13px; color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
    .kad-divider { color: #cbd5e1; font-weight: 300; margin: 0 4px; }
    .kad-window-active { color: #15803d; font-size: 22px; font-weight: 700; }

    .kad-btn { background: #c084a0; color: #fff; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-weight: 600; transition: background 0.15s; font-size: 12px; }
    .kad-btn:hover { background: #a06080; }
    .kad-btn-teal { background: #5b9fc7; }
    .kad-btn-teal:hover { background: #4180a8; }
    .kad-btn-red { background: #c0606a; padding: 2px 6px; font-size: 11px; }
    .kad-btn-red:hover { background: #a04858; }
    .kad-btn-action { position: absolute; top: 6px; right: 28px; padding: 2px 6px; font-size: 10px; background: #9b7fa8; }
    .kad-btn-action:hover { background: #7a6088; }
    .kad-btn-remove { position: absolute; top: 6px; right: 6px; padding: 2px 6px; font-size: 10px; }

    .kad-input { border: 1px solid #cbd5e1; border-radius: 4px; padding: 3px 6px; text-align: center; font-size: 12px; width: 65px; outline: none; background: #f8fafc; transition: border 0.15s; }
    .kad-input:focus { border-color: #c084a0; background: #ffffff; }
    .kad-input-error { border-color: #b84c23 !important; background: #fff5f5 !important; }
    .kad-input-hint { font-size: 11px; color: #b84c23; margin-left: 2px; display: none; }

    .kad-quick-links a { color: #64748b; text-decoration: none; font-weight: 600; font-size: 12px; transition: color 0.15s; }
    .kad-quick-links a:hover { color: #0f172a; }

    .kad-food-copy { cursor: pointer; color: #334155 !important; border-bottom: 1px dotted #94a3b8; transition: all 0.15s; }
    .kad-food-copy:hover { color: #0f172a !important; background: #f1f5f9; border-bottom: 1px solid #0f172a; }
</style>
`;
runScript()

function runScript() {
    $('head').append(STYLES);
    hideHeaderText();
    addIdToKadaotiesTable();

    injectDashboard();

    checkBoardLockState();
    addClickToCopyFeature();
    setupNotificationSupport();

    checkForKadaotieRefresh();
    setInterval(tickTimers, 1000);
    tickTimers();
}

function hideHeaderText() {
    let textContainer = $(':contains("The Kadoatery")');
    textContainer.contents().filter(function() { return this.nodeType===3; }).remove();
    $(textContainer).children('br').hide();
}

function addIdToKadaotiesTable() {
    $('.content div table').first().attr("id","kadaotiesTable");
}

function injectDashboard() {
    let dashboardHtml = `
        <div id="kad-dashboard-wrapper" class="kad-modern-wrapper">
            <div id='alreadyFedAlert' class='kad-alert kad-alert-warning' style='display:none;'>
                ⚠️ <strong>You are locked out!</strong> A Mini refreshed, but your previously fed Kad is still on the board. Do not buy!
            </div>

            <div id='windowMissingAlert' class='kad-alert' style='display:none;'>
                Main refresh time missing or expired.
            </div>

            <div id='pendingDropAlert' class='kad-alert kad-alert-warning' style='display:none;'></div>

            <div id="kad-dashboard" class="kad-panel">

                <div class="kad-toolbar">
                    <div class="kad-toolbar-group kad-quick-links">
                        <a href="/market.phtml?type=wizard" target="_blank">SW</a> &bull; <a href="/safetydeposit.phtml" target="_blank">SDB</a>
                    </div>

                    <div class="kad-toolbar-group" style="border-left: 1px solid #e2e8f0; border-right: 1px solid #e2e8f0; padding: 0 10px;">
                        <input type="text" id="manualTimeInput" class="kad-input" placeholder="HH:MM" /><span id="timeInputHint" class="kad-input-hint"></span>
                        <button id="setMainBtn" class="kad-btn">Log Main</button>
                        <button id="addMiniBtn" class="kad-btn">Log Mini</button>
                    </div>

                    <div class="kad-toolbar-group">
                        <span id="notifyStatus" style="font-size: 11px; color: #10b981; display: none; font-weight: 600;">🔔 ON</span>
                        <button id="enableNotifyBtn" class="kad-btn" style="display: none;">🔔 Enable Alerts</button>
                        <button id="copyBoardBtn" class="kad-btn kad-btn-teal">📋 Copy Post</button>
                    </div>
                </div>

                <div id="mainContainer"></div>
                <div id="minisContainer" style="display: flex; flex-direction: column; gap: 4px;"></div>

            </div>
        </div>
    `;

    $(dashboardHtml).insertBefore('#kadaotiesTable');

    $("#setMainBtn").on("click", () => handleManualTime('main'));
    $("#addMiniBtn").on("click", () => handleManualTime('mini'));
    $("#copyBoardBtn").on("click", copyBoardTimes);

    $(document).on("click", ".demote-main-btn", demoteMainToMini);
    $(document).on("click", ".promote-mini-btn", function() { promoteMiniToMain($(this).data('index')); });
    $(document).on("click", ".remove-main-btn", function() { GM_setValue(LAST_REFRESH_TIME_KEY, 0); tickTimers(); });
    $(document).on("click", ".remove-mini-btn", function() { removeMini($(this).data('index')); });
    $(document).on("click", ".merge-main-btn", function() { mergePendingDrop('main'); });
    $(document).on("click", ".merge-mini-btn", function() { mergePendingDrop('mini', $(this).data('index')); });
    $(document).on("click", "#discardDropBtn", discardPendingDrop);
}

function checkBoardLockState() {
    let username = "";
    let modernNav = $('.nav-profile-dropdown-text').first().text().trim();
    if (modernNav) username = modernNav;
    else {
        let classicHref = $("a[href^='/userlookup.phtml?user=']").first().attr("href");
        if (classicHref) {
            let match = classicHref.match(/user=([^&]+)/);
            if (match) username = match[1];
        }
    }

    userIsLockedOut = false;
    if (username) {
        let fedItText = username + " has been fed";
        let hasHungryKads = false;

        $("#kadaotiesTable td").each(function() {
            let text = $(this).text();
            if (text.includes(fedItText)) userIsLockedOut = true;
            if (text.includes("is very sad")) hasHungryKads = true;
        });
        if (userIsLockedOut && hasHungryKads) $('#alreadyFedAlert').show();
        else $('#alreadyFedAlert').hide();
    }
}

function formatKadLabel(type, count, first, last) {
    if (!count || count === 0) return type;
    if (count === 1) return `${type} (1)${first}`;
    return `${type} (${count})${first} › ${last}`;
}

function getCurrentKadStates() {
    let states = {};
    $("#kadaotiesTable td").slice(0, 20).each(function(index) {
        let text = $(this).text();
        let strongs = $(this).find('strong');
        let kadName = strongs.first().text().trim();

        if (text.includes("is very sad")) {
            let foodName = strongs.length > 1 ? strongs.last().text().trim() : "";
            states[index] = { name: kadName, food: foodName, status: "sad" };
        } else if (text.includes("has been fed")) {
            states[index] = { name: kadName, food: "", status: "fed" };
        }
    });
    return states;
}

function checkForKadaotieRefresh() {
    let currentState = getCurrentKadStates();
    let prevStateString = GM_getValue(KAD_STATES_KEY, "{}");
    let prevState = JSON.parse(prevStateString);
    let isInitialLoad = GM_getValue(KAD_FIRST_RUN_KEY, true);
    GM_setValue(KAD_FIRST_RUN_KEY, false);

    let newlySadKads = []; 
    let newlySadIndices = new Set();
    for (let i = 0; i < 20; i++) {
        if (currentState[i] && currentState[i].status === "sad") {
            let isNew = !prevState[i]
                || prevState[i].status !== "sad"
                || prevState[i].name !== currentState[i].name;
            if (isNew) {
                newlySadKads.push({ name: currentState[i].name, food: currentState[i].food || "" });
                newlySadIndices.add(i);
            }
        }
    }

    GM_setValue(KAD_STATES_KEY, JSON.stringify(currentState));

    let currentSadNames = new Set();
    for (let i in currentState) {
        if (currentState[i].status === "sad") currentSadNames.add(currentState[i].name);
    }

    const pruneTrackedFeeds = () => {
        let mainData = {};
        try { mainData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}")); } catch(e){}
        if (mainData.names && mainData.names.length > 0) {
            let prunedNames = [], prunedFoods = [];
            for (let i = 0; i < mainData.names.length; i++) {
                if (currentSadNames.has(mainData.names[i])) {
                    prunedNames.push(mainData.names[i]);
                    prunedFoods.push(mainData.foods[i]);
                }
            }
            if (prunedNames.length !== mainData.names.length) {
                mainData.names = prunedNames;
                mainData.foods = prunedFoods;
                GM_setValue(MAIN_KAD_DATA_KEY, JSON.stringify(mainData));
            }
        }

        let minis = getMinis();
        let minisChanged = false;
        for (let m = 0; m < minis.length; m++) {
            if (minis[m].names && minis[m].names.length > 0) {
                let prunedNames = [], prunedFoods = [];
                for (let i = 0; i < minis[m].names.length; i++) {
                    if (currentSadNames.has(minis[m].names[i])) {
                        prunedNames.push(minis[m].names[i]);
                        prunedFoods.push(minis[m].foods[i]);
                    }
                }
                if (prunedNames.length !== minis[m].names.length) {
                    minis[m].names = prunedNames;
                    minis[m].foods = prunedFoods;
                    minisChanged = true;
                }
            }
        }
        if (minisChanged) saveMinis(minis);
    };

    if (isInitialLoad || newlySadKads.length === 0) {
        pruneTrackedFeeds();
        return;
    }

    let now = new Date().getTime();
    const COALESCE_WINDOW_MS = 240000; // 4 minutes

    let mainTime = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
    let minisArr = getMinis();
    let mostRecentTime = mainTime;
    let mostRecentType = 'main';
    let mostRecentIndex = -1;

    minisArr.forEach((m, i) => {
        if (m.time > mostRecentTime) {
            mostRecentTime = m.time;
            mostRecentType = 'mini';
            mostRecentIndex = i;
        }
    });

    if (mostRecentTime > 0 && (now - mostRecentTime < COALESCE_WINDOW_MS)) {
        if (mostRecentType === 'main') {
            let mainData = {};
            try { mainData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}")); } catch(e){}
            let existingNamesSet = new Set(mainData.names || []);
            let mergedNames = [...(mainData.names || [])];
            let mergedFoods = [...(mainData.foods || [])];
            
            let added = false;
            for (let k of newlySadKads) {
                if (!existingNamesSet.has(k.name)) {
                    mergedNames.push(k.name);
                    mergedFoods.push(k.food);
                    added = true;
                }
            }
            if (added) {
                let count = mergedNames.length;
                GM_setValue(MAIN_KAD_DATA_KEY, JSON.stringify({
                    count, first: mergedNames[0], last: mergedNames[count - 1],
                    names: mergedNames, foods: mergedFoods
                }));
                pruneTrackedFeeds();
                tickTimers();
            }
        } else {
            let mini = minisArr[mostRecentIndex];
            let existingNamesSet = new Set(mini.names || []);
            let mergedNames = [...(mini.names || [])];
            let mergedFoods = [...(mini.foods || [])];
            
            let added = false;
            for (let k of newlySadKads) {
                if (!existingNamesSet.has(k.name)) {
                    mergedNames.push(k.name);
                    mergedFoods.push(k.food);
                    added = true;
                }
            }
            if (added) {
                let count = mergedNames.length;
                minisArr[mostRecentIndex] = { ...mini, count, first: mergedNames[0], last: mergedNames[count - 1], names: mergedNames, foods: mergedFoods };
                saveMinis(minisArr);
                pruneTrackedFeeds();
            }
        }
        return; 
    }

    let existing = {};
    try { existing = JSON.parse(GM_getValue(PENDING_DROP_KEY, "{}")); } catch(e) {}
    if (existing.time && (now - existing.time < COALESCE_WINDOW_MS)) {
        let existingNamesSet = new Set(existing.names || []);
        for (let k of newlySadKads) {
            if (!existingNamesSet.has(k.name)) {
                existing.names.push(k.name);
                existing.foods.push(k.food);
            }
        }
        existing.count = existing.names.length;
        GM_setValue(PENDING_DROP_KEY, JSON.stringify(existing));
        renderPendingDrop();
        pruneTrackedFeeds();
        return;
    }

    function countMissingNames(storedNames) {
        if (!storedNames || storedNames.length === 0) return 0;
        return storedNames.filter(name => !currentSadNames.has(name)).length;
    }

    let mainData = {};
    try { mainData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}")); } catch(e) {}
    let minis = getMinis();

    let mainTotal   = (mainData.names || []).length;
    let mainMissing = countMissingNames(mainData.names);
    let mainFullyRefreshed   = mainTotal > 0 && mainMissing === mainTotal;
    let mainPartiallyRefreshed = mainTotal > 0 && mainMissing > 0 && mainMissing < mainTotal;
    let miniResults = minis.map((mini, i) => {
        let total   = (mini.names || []).length;
        let missing = countMissingNames(mini.names);
        return {
            index: i,
            fullyRefreshed:   total > 0 && missing === total,
            partiallyRefreshed: total > 0 && missing > 0 && missing < total
        };
    });
    let fullyRefreshedMiniIndices   = miniResults.filter(r => r.fullyRefreshed).map(r => r.index);
    let partiallyRefreshedMiniIndices = miniResults.filter(r => r.partiallyRefreshed).map(r => r.index);
    let kadNames = newlySadKads.map(k => k.name);
    let kadFoods = newlySadKads.map(k => k.food);
    let count    = kadNames.length;

    if (mainFullyRefreshed && fullyRefreshedMiniIndices.length > 0) {
        pushMainToHistory();
        GM_setValue(LAST_REFRESH_TIME_KEY, now);
        GM_setValue(MAIN_KAD_DATA_KEY, JSON.stringify({
            count, first: kadNames[0], last: kadNames[count - 1],
            names: kadNames, foods: kadFoods
        }));
        let updatedMinis = minis.filter((_, i) => !fullyRefreshedMiniIndices.includes(i));
        saveMinis(updatedMinis);
        pruneTrackedFeeds();
        tickTimers();
        return;
    }

    if (mainFullyRefreshed) {
        pushMainToHistory();
        GM_setValue(LAST_REFRESH_TIME_KEY, now);
        GM_setValue(MAIN_KAD_DATA_KEY, JSON.stringify({
            count, first: kadNames[0], last: kadNames[count - 1],
            names: kadNames, foods: kadFoods
        }));
        pruneTrackedFeeds();
        tickTimers();
        return;
    }

    if (fullyRefreshedMiniIndices.length > 0) {
        let updatedMinis = [...minis];
        fullyRefreshedMiniIndices.forEach((miniIdx, pos) => {
            if (pos === 0) {
                updatedMinis[miniIdx] = { ...updatedMinis[miniIdx], time: now, count,
                    first: kadNames[0], last: kadNames[count - 1], names: kadNames, foods: kadFoods };
            } else {
                updatedMinis[miniIdx] = { ...updatedMinis[miniIdx], time: now };
            }
        });
        saveMinis(updatedMinis);
        pruneTrackedFeeds();
        tickTimers();
        return;
    }

    if (mainPartiallyRefreshed || partiallyRefreshedMiniIndices.length > 0) {
        addMini(now, count, kadNames[0], kadNames[count - 1], kadNames, kadFoods);
        pruneTrackedFeeds();
        tickTimers();
        return;
    }

    GM_setValue(PENDING_DROP_KEY, JSON.stringify({
        time: now,
        count: kadNames.length,
        names: kadNames,
        foods: kadFoods,
        indices: [...newlySadIndices],
        match: null
    }));
    renderPendingDrop();
    pruneTrackedFeeds();
}

function identifyRefreshedFeed(currentState) {
    let currentSadNames = new Set();
    for (let i in currentState) {
        if (currentState[i].status === "sad") currentSadNames.add(currentState[i].name);
    }
    function feedHasRefreshed(storedNames) {
        if (!storedNames || storedNames.length === 0) return false;
        return storedNames.some(name => !currentSadNames.has(name));
    }
    let mainData = {};
    try { mainData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}")); } catch(e) {}
    let minis = getMinis();
    if (feedHasRefreshed(mainData.names)) return { type: 'main', label: 'Main' };
    for (let i = 0; i < minis.length; i++) {
        if (feedHasRefreshed(minis[i].names)) return { type: 'mini', index: i, label: `Mini ${i + 1}` };
    }
    return null;
}

function renderPendingDrop() {
    let drop = {};
    try { drop = JSON.parse(GM_getValue(PENDING_DROP_KEY, "{}")); } catch(e) {}

    let alert = $('#pendingDropAlert');
    if (!drop.time) { alert.hide().empty(); return; }

    let match = drop.match || null;
    let minis = getMinis();
    let mergeButtons = '';
    let mainIsMatch = match && match.type === 'main';
    mergeButtons += `<button class="kad-btn merge-main-btn" style="margin: 0 3px;${mainIsMatch ? ' outline: 2px solid #fff; font-weight: 800;' : ''}" title="Merge into Main">→ Main</button>`;

    minis.forEach((mini, i) => {
        let state = getTimerState(mini.time, new Date().getTime());
        if (state.status !== "expired") {
            mergeButtons += `<button class="kad-btn merge-mini-btn" data-index="${i}" style="margin: 0 3px;" title="Merge into Mini ${i+1}">→ Mini ${i+1}</button>`;
        }
    });
    let matchNote = match
        ? `Identified: <strong>${match.label}</strong> &mdash;`
        : `No feed identified &mdash; select manually: `;
    let firstKad   = drop.names[0] || '';
    let lastKad    = drop.count > 1 ? ` › ${drop.names[drop.count - 1]}` : '';
    let kadSummary = `${drop.count} Kad${drop.count !== 1 ? 's' : ''} (${firstKad}${lastKad})`;
    alert.html(`
        🐱 <strong>New drop detected:</strong> ${kadSummary} &mdash; ${matchNote}
        ${mergeButtons}
        <button id="discardDropBtn" class="kad-btn kad-btn-red" style="margin: 0 3px;">Discard</button>
    `).show();
}

function pushMainToHistory() {
    let currentMainTime = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
    if (currentMainTime <= 0) return;
    let history = JSON.parse(GM_getValue(MAIN_HISTORY_KEY, "[]"));
    let currentData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}"));
    history.push({ time: currentMainTime, data: currentData });
    if (history.length > 5) history.shift();
    GM_setValue(MAIN_HISTORY_KEY, JSON.stringify(history));
}

function mergePendingDrop(type, miniIndex) {
    let drop = {};
    try { drop = JSON.parse(GM_getValue(PENDING_DROP_KEY, "{}")); } catch(e) {}
    if (!drop.time) return;

    if (type === 'main') {
        pushMainToHistory();
        let existing = {};
        try { existing = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}")); } catch(e) {}
        let existingNamesSet = new Set(existing.names || []);
        let mergedNames = [...(existing.names || [])];
        let mergedFoods = [...(existing.foods || [])];
        for (let i = 0; i < drop.names.length; i++) {
            if (!existingNamesSet.has(drop.names[i])) {
                mergedNames.push(drop.names[i]);
                mergedFoods.push(drop.foods[i]);
            }
        }
        let count = mergedNames.length;
        GM_setValue(LAST_REFRESH_TIME_KEY, drop.time);
        GM_setValue(MAIN_KAD_DATA_KEY, JSON.stringify({
            count, first: mergedNames[0], last: mergedNames[count - 1],
            names: mergedNames, foods: mergedFoods
        }));
    } else {
        let minis = getMinis();
        let mini  = minis[miniIndex];
        if (!mini) return;
        let existingNamesSet = new Set(mini.names || []);
        let mergedNames = [...(mini.names || [])];
        let mergedFoods = [...(mini.foods || [])];
        for (let i = 0; i < drop.names.length; i++) {
            if (!existingNamesSet.has(drop.names[i])) {
                mergedNames.push(drop.names[i]);
                mergedFoods.push(drop.foods[i]);
            }
        }
        let count = mergedNames.length;
        minis[miniIndex] = { ...mini, time: drop.time, count, first: mergedNames[0], last: mergedNames[count - 1], names: mergedNames, foods: mergedFoods };
        saveMinis(minis);
    }

    GM_setValue(PENDING_DROP_KEY, "{}");
    renderPendingDrop();
    tickTimers();
}

function discardPendingDrop() {
    GM_setValue(PENDING_DROP_KEY, "{}");
    renderPendingDrop();
}

function demoteMainToMini() {
    let mainTime = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
    if (!mainTime) return;

    let mainData = {};
    try { mainData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}")); } catch(e){}

    addMini(mainTime, mainData.count || 0, mainData.first || "", mainData.last || "", mainData.names || [], mainData.foods || []);
    
    let history = JSON.parse(GM_getValue(MAIN_HISTORY_KEY, "[]"));
    if (history.length > 0) {
        let previous = history.pop();
        GM_setValue(MAIN_HISTORY_KEY, JSON.stringify(history));
        GM_setValue(LAST_REFRESH_TIME_KEY, previous.time);
        GM_setValue(MAIN_KAD_DATA_KEY, JSON.stringify(previous.data));
    } else {
        GM_setValue(LAST_REFRESH_TIME_KEY, 0);
        GM_setValue(MAIN_KAD_DATA_KEY, "{}");
    }

    tickTimers();
}

function promoteMiniToMain(index) {
    let minis = getMinis();
    if (!minis[index]) return;

    pushMainToHistory();
    let mini = minis[index];
    minis.splice(index, 1);
    saveMinis(minis);

    GM_setValue(LAST_REFRESH_TIME_KEY, mini.time);
    GM_setValue(MAIN_KAD_DATA_KEY, JSON.stringify({
        count: mini.count,
        first: mini.first,
        last: mini.last,
        names: mini.names || [],
        foods: mini.foods || []
    }));
    tickTimers();
}

function getMinis() {
    try {
        let minis = JSON.parse(GM_getValue(MINI_REFRESH_TIMES_KEY, "[]"));
        if (!Array.isArray(minis)) return [];
        return minis.map(m => (typeof m === 'number') ? { time: m, count: 0, first: "", last: "", names: [], foods: [] } : m);
    } catch(e) { return []; }
}

function saveMinis(minisArr) {
    GM_setValue(MINI_REFRESH_TIMES_KEY, JSON.stringify(minisArr));
    tickTimers();
}

function addMini(timeMs, count = 0, first = "", last = "", names = [], foods = []) {
    let minis = getMinis();
    minis.push({ time: timeMs, count, first, last, names, foods });
    minis.sort((a, b) => a.time - b.time);
    saveMinis(minis);
}

function removeMini(index) {
    let minis = getMinis();
    minis.splice(index, 1);
    saveMinis(minis);
}

function tickTimers() {
    let nowTime = new Date().getTime();
    updateMainUI(nowTime);
    renderMinis(nowTime);
    renderPendingDrop();
}

function getTimerState(lastRefreshTime, currentTime) {
    if (!lastRefreshTime || lastRefreshTime === 0 || (currentTime - lastRefreshTime > MAXIMUM_TIME_BETWEEN_REFRESHES_MS)) return { status: "expired" };
    let mainWindowStart = lastRefreshTime + DURATION_UNTIL_START_OF_MAIN_WINDOW_MS;
    let mainWindowEnd = mainWindowStart + DURATION_OF_MAIN_WINDOW_MS;
    if (currentTime < mainWindowStart) return { status: "waiting", nextStart: mainWindowStart, countdown: mainWindowStart - currentTime };
    if (currentTime >= mainWindowStart && currentTime < mainWindowEnd) return { status: "active", timeRemaining: mainWindowEnd - currentTime };
    for (let i = 1; i <= 8; i++) {
        let pStart = mainWindowStart + (i * 7 * 60000);
        let pEnd = pStart + DURATION_OF_PENDING_WINDOWS_MS;
        if (currentTime < pStart) return { status: "waiting", nextStart: pStart, countdown: pStart - currentTime };
        if (currentTime >= pStart && currentTime < pEnd) return { status: "active", timeRemaining: pEnd - currentTime };
    }
    return { status: "expired" };
}

function renderTimerHTML(state) {
    if (state.status === "expired") return '<span class="kad-text-red" style="font-size: 14px;">Missing or expired.</span>';
    if (state.status === "waiting") return `<span class="kad-time-sub">Next:</span> <span>${formatWindowTime(new Date(state.nextStart))}</span> <span class="kad-divider">|</span> <span class="kad-time-sub">In:</span> <span class="kad-time-hl">${formatCountdown(new Date(state.countdown))}</span>`;
    if (state.status === "active") return `<span class="kad-window-active">WINDOW ACTIVE!</span> <span class="kad-divider">|</span> <span class="kad-time-sub">Closes in:</span> <span class="kad-window-active">${Math.round(state.timeRemaining / 1000)}s</span>`;
    return '<span style="font-size: 14px; color: #94a3b8;">Unknown state.</span>';
}

function updateMainUI(nowTime) {
    let lastTime = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
    let state = getTimerState(lastTime, nowTime);
    let container = $('#mainContainer');

    if (state.status === "expired") {
        $('#windowMissingAlert').show();
        container.empty();
        return;
    }

    $('#windowMissingAlert').hide();

    let label = "Main";
    try {
        let mainData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}"));
        label = formatKadLabel("Main", mainData.count, mainData.first, mainData.last);
    } catch(e) {}

    let lastRefreshStr = formatLastRefresh(lastTime);
    container.html(`
        <div class="kad-tracker-block kad-main-block">
            <div class="kad-block-label" title="${label}">${label}</div>
            <div class="kad-countdown-box">${renderTimerHTML(state)}</div>
            <div class="kad-last-refresh">Last refresh: ${lastRefreshStr}</div>
            <button class="kad-btn kad-btn-action demote-main-btn" title="Convert to Mini">↓ Demote</button>
            <button class="kad-btn kad-btn-red kad-btn-remove remove-main-btn" title="Remove Main">✕</button>
        </div>
    `);

    if (state.status === "waiting") checkAndTriggerNotification('main', state.countdown);
}

function renderMinis(nowTime) {
    let minis = getMinis();
    let container = $('#minisContainer');
    container.empty();

    minis.forEach((miniObj, index) => {
        let state = getTimerState(miniObj.time, nowTime);
        if (state.status === "expired") return;

        let label = formatKadLabel(`Mini ${index + 1}`, miniObj.count, miniObj.first, miniObj.last);
        let miniLastStr = formatLastRefresh(miniObj.time);
        container.append(`
            <div class="kad-tracker-block">
                <div class="kad-block-label" title="${label}">${label}</div>
                <div class="kad-countdown-box">${renderTimerHTML(state)}</div>
                <div class="kad-last-refresh">Last refresh: ${miniLastStr}</div>
                <button class="kad-btn kad-btn-action promote-mini-btn" data-index="${index}" title="Set as Main">↑ Promote</button>
                <button class="kad-btn kad-btn-red kad-btn-remove remove-mini-btn" data-index="${index}" title="Remove Mini">✕</button>
            </div>
        `);
        if (state.status === "waiting") checkAndTriggerNotification(`mini_${index}`, state.countdown);
    });
}

function handleManualTime(type) {
    let val = $('#manualTimeInput').val();
    let newTime = parseTimeInput(val);
    if(newTime) {
        if (type === 'main') {
            pushMainToHistory();
            GM_setValue(LAST_REFRESH_TIME_KEY, newTime.getTime());
            GM_setValue(MAIN_KAD_DATA_KEY, "{}");
        } else {
            addMini(newTime.getTime());
        }
        $('#manualTimeInput').val('');
        tickTimers();
    }
}

function showInputError(msg) {
    $('#manualTimeInput').addClass('kad-input-error');
    let hint = $('#timeInputHint');
    hint.text(msg).show();
    setTimeout(() => { $('#manualTimeInput').removeClass('kad-input-error'); hint.hide(); }, 2500);
}

function parseTimeInput(val) {
    let inputtedTimes = val.split(":");
    if (inputtedTimes.length < 2) { showInputError("Use HH:MM format."); return null; }

    let hour = parseInt(inputtedTimes[0], 10);
    let min  = parseInt(inputtedTimes[1], 10);
    let sec  = inputtedTimes.length > 2 ? parseInt(inputtedTimes[2], 10) : 0;

    if (isNaN(hour) || isNaN(min) || isNaN(sec) || hour < 0 || hour > 23 || min < 0 || min > 59 || sec < 0 || sec > 59) {
        showInputError("Invalid time.");
        return null;
    }

    let now = new Date();
    let currentTime = now.getTime();

    let nstWallStr = now.toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
    let nstDate = new Date(nstWallStr);

    let tzOffsetMs = currentTime - nstDate.getTime();

    if (nstDate.getHours() <= 1 && hour >= 22) nstDate.setDate(nstDate.getDate() - 1);
    nstDate.setHours(hour, min, sec, 0);

    let absoluteNSTTime = nstDate.getTime() + tzOffsetMs;

    if (absoluteNSTTime > currentTime || (currentTime - absoluteNSTTime) > MAXIMUM_TIME_BETWEEN_REFRESHES_MS) {
        showInputError("Must be within 78 min.");
        return null;
    }

    return new Date(absoluteNSTTime);
}

function generateBoardString(label, lastTimeMs, foodNames) {
    if (!lastTimeMs || lastTimeMs === 0) return null;
    let last = new Date(lastTimeMs);
    let next = new Date(lastTimeMs + DURATION_UNTIL_START_OF_MAIN_WINDOW_MS);

    let pends = [];
    for(let i = 1; i <= 5; i++) {
        let pend = new Date(next.getTime() + (i * 7 * 60000));
        pends.push(":" + formatTwoDigits(pend.getMinutes()));
    }

    let lastStr = formatWindowTime(last);
    let nextStr = formatWindowTime(next);
    
    let foodLine = (foodNames && foodNames.length > 0)
        ? "\n" + foodNames.map(censorFood).reduce((acc, food, i) => acc + (i > 0 ? (i % 4 === 0 ? "\n\n" : "\n") : "") + food, "")
        : '';
    return `${label} @ ${lastStr}\nNext ${nextStr} / ${pends.join(" / ")}${foodLine}`;
}

function copyBoardTimes() {
    let mainTime = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
    let minis = getMinis();
    let now = new Date().getTime();
    let postArray = [];

    let mainState = getTimerState(mainTime, now);
    if(mainState.status !== "expired") {
        let mainData = {};
        try { mainData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}")); } catch(e){}
        let label = formatKadLabel("Main", mainData.count, mainData.first, mainData.last);
        postArray.push(generateBoardString(label, mainTime, mainData.foods || []));
    }

    minis.forEach((miniObj, index) => {
        let state = getTimerState(miniObj.time, now);
        if(state.status !== "expired") {
            let label = formatKadLabel(`Mini ${index + 1}`, miniObj.count, miniObj.first, miniObj.last);
            postArray.push(generateBoardString(label, miniObj.time, miniObj.foods || []));
        }
    });
    if(postArray.length === 0) {
        let btn = $('#copyBoardBtn');
        btn.text("Nothing to copy");
        setTimeout(() => btn.text("📋 Copy Post"), 1800);
        return;
    }

    navigator.clipboard.writeText(postArray.join("\n\n")).then(() => {
        let btn = $('#copyBoardBtn');
        let oldText = btn.text();
        btn.text("✅ Copied!");
        btn.css("background", "#10b981");
        setTimeout(() => { btn.text(oldText).css("background", ""); }, 1500);
    });
}

function addClickToCopyFeature() {
    $("#kadaotiesTable td:contains('is very sad')").each(function() {
        let foodTag = $(this).find('strong').last();
        if (foodTag.length && !foodTag.hasClass('kad-food-copy')) {
            foodTag.addClass('kad-food-copy');
            foodTag.attr('title', userIsLockedOut ? 'You are locked out!' : 'Click to copy to clipboard');

            foodTag.on('click', function(e) {
                e.preventDefault();

                if (userIsLockedOut) {
                    let originalText = $(this).text();
                    $(this).text('Locked!');
                    $(this).css({ 'color': '#ef4444', 'border-color': '#ef4444' });
                    setTimeout(() => {
                        $(this).text(originalText);
                        $(this).css({ 'color': '', 'border-color': '' });
                    }, 1000);
                    return;
                }

                let foodName = $(this).text();
                navigator.clipboard.writeText(foodName).then(() => {
                    $(this).text('Copied!');
                    $(this).css({ 'color': '#10b981', 'border-color': '#10b981' });
                    setTimeout(() => {
                        $(this).text(foodName);
                        $(this).css({ 'color': '', 'border-color': '' });
                    }, 800);
                });
            });
        }
    });
}

function setupNotificationSupport() {
    let optedIn = GM_getValue(NOTIFICATION_OPT_IN_KEY);
    if (optedIn) {
        $('#notifyStatus').show();
    } else if ("Notification" in window && typeof optedIn === "undefined") {
        $('#enableNotifyBtn').show().on('click', function () {
             Notification.requestPermission().then((permission) => {
                 GM_setValue(NOTIFICATION_OPT_IN_KEY, permission === "granted");
                 $('#enableNotifyBtn').hide();
                 if (permission === "granted") $('#notifyStatus').show();
             })
        });
    }
}

function checkAndTriggerNotification(type, countdownRemainingMs) {
    if (!GM_getValue(NOTIFICATION_OPT_IN_KEY)) return;
    if (countdownRemainingMs <= 10000 && countdownRemainingMs > 9000) {
        let nowMs = new Date().getTime();
        if (nowMs - (notificationsTriggered[type] || 0) > 240000) {
            notificationsTriggered[type] = nowMs;
            let title = type === 'main' ? "Main Refresh Incoming!" : "Mini Refresh Incoming!";
            showNotification(title, "Window starts in 10 seconds.");
        }
    }
}

function showNotification(title, body) {
    let notification = new Notification(title, {
        body: body,
        icon: "https://itemdb.com.br/api/cache/preview/7f18f78e35daa6.png"
    });
    notification.onclick = () => {
        notification.close();
        window.focus();
    }
}

function formatTwoDigits(n) { return n < 10 ? '0' + n : n; }
function formatCountdown(d) { return formatTwoDigits(d.getMinutes()) + ":" + formatTwoDigits(d.getSeconds()); }

function formatWindowTime(d) {
    let nstStr = d.toLocaleString("en-US", { timeZone: "America/Los_Angeles", hour: "numeric", minute: "2-digit", second: "2-digit", hour12: true });
    return nstStr.replace(/ /g, '').toLowerCase();
}

function formatLastRefresh(ms) {
    if (!ms || ms === 0) return '';
    let d = new Date(ms);
    let nstStr = d.toLocaleString("en-US", { timeZone: "America/Los_Angeles", hour: "numeric", minute: "2-digit", second: "2-digit", hour12: true });
    return nstStr.replace(/ /g, '').toLowerCase();
}