Greasy Fork is available in English.

MZ - NT Player Search

Searches for players who match specific requirements

// ==UserScript==
// @name         MZ - NT Player Search
// @namespace    douglaskampl
// @version      2.97
// @description  Searches for players who match specific requirements
// @author       Douglas Vieira
// @match        https://www.managerzone.com/?p=national_teams&type=senior
// @match        https://www.managerzone.com/?p=national_teams&type=u21
// @icon         https://yt3.googleusercontent.com/ytc/AIdro_mDHaJkwjCgyINFM7cdUV2dWPPnL9Q58vUsrhOmRqkatg=s160-c-k-c0x00ffffff-no-rj
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      mzlive.eu
// @connect      pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev
// @require      https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    GM_addStyle(`@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');#leftmenu_nt_search{margin-top:5px}.mz-search-open-btn{display:block;padding:2px 4px;color:#000;text-decoration:none;font-size:12px;font-family:Arial,sans-serif}.mz-search-open-btn:hover{background-color:#f0f0f0;text-decoration:none}.mz-search-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(.95);background:linear-gradient(135deg,#0a0a0a 0%,#1a1a2e 100%);color:#f0f0f0;padding:2rem;border-radius:12px;box-shadow:0 8px 32px rgba(83,11,237,.3),0 4px 8px rgba(0,0,0,.2);z-index:9999;visibility:hidden;width:800px;max-width:99%;opacity:0;transition:all .3s cubic-bezier(0.4,0,0.2,1);border:1px solid rgba(138,43,226,.1)}.mz-search-container.visible{visibility:visible;opacity:1;transform:translate(-50%,-50%) scale(1)}.mz-search-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:2rem;padding-bottom:1rem;border-bottom:1px solid rgba(138,43,226,.2)}.mz-search-header h2{font-family:'Space Mono',monospace;margin:0;color:violet;font-size:1.5rem;text-shadow:0 0 10px rgba(138,43,226,.5)}.mz-search-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-bottom:1.5rem}.mz-search-field{display:flex;flex-direction:column;gap:.5rem}.mz-search-field label{color:#ff9966;font-size:.875rem;text-transform:uppercase;letter-spacing:1px}.mz-search-field select{padding:.75rem;border:1px solid rgba(138,43,226,.3);border-radius:8px;background:#1a1a2e;color:#f0f0f0;font-size:1rem;transition:all .2s;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ff9966' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .75rem center;background-size:1rem}.mz-search-field select:focus{outline:none;border-color:#ff9966;box-shadow:0 0 0 2px rgba(138,43,226,.2)}.mz-search-button{width:auto;max-width:300px;padding:0.5rem 1rem;background:#009b3a;color:#ffdf00;border:none;border-radius:8px;font-weight:500;font-size:0.9rem;cursor:pointer;transition:all .2s;text-transform:uppercase;letter-spacing:2px;box-shadow:0 4px 6px rgba(0,0,0,.1);display:block;margin:1rem auto}.mz-search-button:not(:disabled):hover{transform:translateY(-2px);box-shadow:0 6px 8px rgba(0,0,0,.2)}.mz-search-button:disabled{opacity:0.5;cursor:not-allowed;background:#666}.mz-progress{margin-top:1.5rem;padding:1rem;background:rgba(26,26,46,.5);border-radius:8px;visibility:hidden;opacity:0;transition:all .3s}.mz-progress.visible{visibility:visible;opacity:1}.mz-progress-info{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;color:#ff9966;font-size:.875rem}.mz-progress-bar{width:100%;height:6px;background:#1a1a2e;border-radius:3px;overflow:hidden}.mz-progress-fill{height:100%;width:0;background:linear-gradient(135deg,#4834d4 0%,#6366f1 100%);transition:width .3s ease-out}.mz-search-log{margin-top:1rem;padding:1rem;background:rgba(26,26,46,.3);border-radius:8px;font-family:monospace;font-size:.875rem;max-height:150px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#6366f1 #1a1a2e}.mz-search-log::-webkit-scrollbar{width:8px;height:8px}.mz-search-log::-webkit-scrollbar-track{background:#1a1a2e;border-radius:4px}.mz-search-log::-webkit-scrollbar-thumb{background:#6366f1;border-radius:4px}.mz-search-log::-webkit-scrollbar-thumb:hover{background:#4834d4}.mz-search-log-entry{margin-bottom:.5rem;padding:.5rem;background:rgba(26,26,46,.5);border-radius:4px;color:#00ffff;animation:slideIn 0.3s ease-out forwards;opacity:0;transform:translateX(-20px)}@keyframes slideIn{from{opacity:0;transform:translateX(-20px)}to{opacity:1;transform:translateX(0)}}.mz-guestbook-link{position:fixed;bottom:1rem;right:1rem;color:#ff9966;transition:all .2s}.mz-guestbook-link:hover{color:#6366f1;transform:scale(1.1)}.mz-country-select{width:200px}.mz-country-select select{width:100%}.mz-loading{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(10,10,20,.95);padding:1rem;border-radius:8px;z-index:10000;box-shadow:0 8px 32px rgba(83,11,237,.2);visibility:hidden;opacity:0;transition:all .3s}.mz-loading.visible{visibility:visible;opacity:1}.mz-spinner{position:relative;width:20px;height:20px}.mz-spinner::before,.mz-spinner::after{content:'';position:absolute;border-radius:50%;animation:pulse 1.8s ease-in-out infinite;transform-origin:center}.mz-spinner::before{width:100%;height:100%;background:rgba(99,102,241,.5);animation-delay:-0.9s;transform:scale(0.3)}.mz-spinner::after{width:75%;height:75%;background:rgba(99,102,241,.8);top:12.5%;transform:scale(0.3)}@keyframes pulse{0%,100%{transform:scale(0.3);opacity:1}50%{transform:scale(0.6);opacity:.25}}.mz-results-button{width:auto;max-width:300px;padding:0.5rem 1rem;background:#009b3a;color:#ffdf00;border:none;border-radius:8px;font-weight:500;font-size:0.9rem;cursor:pointer;transition:all .2s;text-transform:uppercase;letter-spacing:2px;box-shadow:0 4px 6px rgba(0,0,0,.1);display:none;margin:1rem auto}.mz-results-button:hover{transform:translateY(-2px);box-shadow:0 6px 8px rgba(0,0,0,.2)}.mz-results-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:linear-gradient(135deg,#0a0a0a 0%,#1a1a2e 100%);color:#f0f0f0;padding:0;border-radius:12px;z-index:10001;width:90%;height:90vh;overflow:hidden;box-shadow:0 8px 32px rgba(83,11,237,.3);animation:modalSlideIn 0.3s ease-out forwards}@keyframes modalSlideIn{from{opacity:0;transform:translate(-50%,-48%)}to{opacity:1;transform:translate(-50%,-50%)}}.mz-results-header{position:sticky;top:0;display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:inherit;border-bottom:1px solid rgba(138,43,226,.2);z-index:1}.mz-results-title{font-family:'Space Mono',monospace;margin:0;font-size:1.5rem;color:#fff;text-shadow:0 0 10px rgba(138,43,226,.5)}.mz-results-close{background:none;border:none;color:#ff9966;font-size:1.5rem;cursor:pointer;transition:all 0.2s;padding:0.5rem}.mz-results-close:hover{color:#6366f1;transform:scale(1.1)}.mz-results-content{padding:1.5rem;height:calc(90vh - 5rem);overflow-y:auto;scrollbar-width:thin;scrollbar-color:#6366f1 #1a1a2e}.mz-results-content::-webkit-scrollbar{width:8px}.mz-results-content::-webkit-scrollbar-track{background:#1a1a2e}.mz-results-content::-webkit-scrollbar-thumb{background:#6366f1;border-radius:4px}.mz-results-content::-webkit-scrollbar-thumb:hover{background:#4834d4}.mz-player-card{background:rgba(26,26,46,.5);border-radius:8px;margin-bottom:1.5rem;padding:1.5rem;transition:all 0.2s;border:1px solid rgba(138,43,226,.1)}.mz-player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(83,11,237,.2)}.mz-player-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.mz-player-info{flex:1}.mz-player-name{font-size:1.25rem;font-weight:bold;color:#fff;margin:0 0 0.5rem 0}.mz-player-details{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;color:#ff9966;font-size:0.875rem}.mz-player-skills{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:1rem;margin-top:1rem}.mz-skill-row{display:flex;align-items:center;padding:0.5rem;background:rgba(26,26,46,.3);border-radius:4px}.mz-skill-name{flex:1;font-size:0.875rem;color:#f0f0f0}.mz-skill-value{display:flex;align-items:center;gap:0.5rem}.mz-skill-level{width:100px;height:8px;background:#1a1a2e;border-radius:4px;overflow:hidden}.mz-skill-fill{height:100%;background:linear-gradient(135deg,#4834d4 0%,#6366f1 100%);transition:width 0.3s}.mz-skill-number{font-size:0.875rem;color:#ff9966;min-width:2rem;text-align:right}.mz-results-pagination{display:flex;justify-content:center;align-items:center;gap:1rem;margin:1rem 0;padding:1rem;border-bottom:1px solid rgba(138,43,226,.2)}.mz-pagination-button{background:#1a1a2e;color:#f0f0f0;border:1px solid rgba(138,43,226,.3);border-radius:4px;padding:0.5rem 1rem;cursor:pointer;transition:all 0.2s}.mz-pagination-button:not(:disabled):hover{background:#2a2a4e;transform:translateY(-1px)}.mz-pagination-button:disabled{opacity:0.5;cursor:not-allowed}.mz-pagination-info{color:#ff9966;font-size:0.875rem}.mz-results-total{padding:1rem;text-align:right;color:#888;font-size:0.875rem}.mz-export-button{background:#1a1a2e;color:#f0f0f0;border:1px solid rgba(138,43,226,.3);border-radius:4px;padding:0.5rem 1rem;cursor:pointer;transition:all 0.2s;margin-left:1rem}.mz-export-button:hover{background:#2a2a4e;transform:translateY(-1px)}.mz-export-button:active{transform:translateY(1px)}.mz-header-controls{display:flex;align-items:center;gap:1rem}`);

    const MASSIVE_COUNTRIES = ['BR', 'CN', 'AR', 'SE', 'PL', 'TR'];
    const PLAYERS_PER_PAGE = 20;

    class Logger {
        constructor(container, batchSize = 10) {
            this.container = container;
            this.batchSize = batchSize;
            this.queue = [];
            this.timeout = null;
        }
        getTimestamp() {
            const now = new Date();
            return `[${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}]`;
        }
        log(message, type = 'info') {
            this.queue.push({ message: `${this.getTimestamp()} ${message}`, type });
            if (this.queue.length >= this.batchSize) {
                this.flush();
            } else if (!this.timeout) {
                this.timeout = setTimeout(() => this.flush(), 100);
            }
        }
        flush() {
            if (!this.queue.length) return;
            const fragment = document.createDocumentFragment();
            this.queue.forEach(({ message, type }) => {
                const entry = document.createElement('div');
                entry.className = `mz-search-log-entry ${type}`;
                entry.textContent = message;
                fragment.appendChild(entry);
            });
            this.container.appendChild(fragment);
            this.container.scrollTop = this.container.scrollHeight;
            this.queue = [];
            if (this.timeout) {
                clearTimeout(this.timeout);
                this.timeout = null;
            }
        }
    }

    class RequestQueue {
        constructor(maxConcurrent = 5, delay = 100) {
            this.queue = [];
            this.maxConcurrent = maxConcurrent;
            this.delay = delay;
            this.running = 0;
            this.processed = 0;
        }
        add(request) {
            return new Promise((resolve, reject) => {
                const wrappedRequest = async () => {
                    try {
                        await new Promise(res => setTimeout(res, this.delay));
                        const result = await request();
                        this.processed++;
                        resolve(result);
                    } catch (error) {
                        reject(error);
                    } finally {
                        this.running--;
                        this.processNext();
                    }
                };
                this.queue.push(wrappedRequest);
                this.processNext();
            });
        }
        processNext() {
            while (this.running < this.maxConcurrent && this.queue.length > 0) {
                this.running++;
                const request = this.queue.shift();
                request();
            }
        }
        reset() {
            this.queue = [];
            this.running = 0;
            this.processed = 0;
        }
    }

    class ChunkProcessor {
        constructor(chunkSize = 25) {
            this.chunkSize = chunkSize;
        }
        async process(items, processFn, onChunkComplete) {
            const chunks = this.createChunks(items);
            let processed = 0;
            for (const chunk of chunks) {
                await Promise.all(chunk.map(processFn));
                processed += chunk.length;
                if (onChunkComplete) {
                    onChunkComplete(processed, items.length);
                }
                await new Promise(res => setTimeout(res, 50));
            }
        }
        createChunks(items) {
            const chunks = [];
            for (let i = 0; i < items.length; i += this.chunkSize) {
                chunks.push(items.slice(i, i + this.chunkSize));
            }
            return chunks;
        }
    }

    class ProgressManager {
        constructor(element, throttleMs = 100) {
            this.element = element;
            this.throttleMs = throttleMs;
            this.lastUpdate = 0;
            this.pendingUpdate = null;
        }
        update(percent) {
            const now = Date.now();
            if (now - this.lastUpdate >= this.throttleMs) {
                this.updateProgress(percent);
                this.lastUpdate = now;
            } else if (!this.pendingUpdate) {
                this.pendingUpdate = setTimeout(() => {
                    this.updateProgress(percent);
                    this.lastUpdate = Date.now();
                    this.pendingUpdate = null;
                }, this.throttleMs);
            }
        }
        updateProgress(percent) {
            if (this.element) {
                this.element.style.width = `${percent}%`;
            }
        }
    }

    class NTPlayerParser {
        constructor(minRequirements) {
            this.minRequirements = minRequirements;
        }
        parseSkills(html) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const rows = doc.querySelectorAll('.player_skills tr');
            if (!rows.length) return null;
            const skills = {};
            let totalBalls = 0;
            const totalBallsElement = doc.querySelector('td[title] span.bold');
            if (totalBallsElement) {
                totalBalls = parseInt(totalBallsElement.textContent, 10) || 0;
            }
            const orderedSkillKeys = [
                "speed",
                "stamina",
                "playIntelligence",
                "passing",
                "shooting",
                "heading",
                "keeping",
                "ballControl",
                "tackling",
                "aerialPassing",
                "setPlays",
                "experience"
            ];
            let skillRows = Array.from(rows);

            // Se houver uma linha extra (provavelmente "Form"/"Forma"), ignore
            if (skillRows.length > orderedSkillKeys.length) { skillRows = skillRows.slice(0, orderedSkillKeys.length); }

            skillRows.forEach((row, index) => {
                const valueCell = row.querySelector('.skillval');
                if (!valueCell) return;
                const rawValue = valueCell.textContent.replace(/[()]/g, "").trim();
                const value = parseInt(rawValue, 10);
                if (!isNaN(value)) {
                    skills[orderedSkillKeys[index]] = value;
                }
            });
            if (Object.keys(skills).length === 0) return null;
            if (!this.validateSkills(skills)) return null;
            return { skills, totalBalls };
        }
        validateSkills(skills) {
            return Object.entries(this.minRequirements)
                .filter(([key]) => key in skills)
                .every(([key, minValue]) => skills[key] >= minValue);
        }
        async fetchAndParsePlayer(playerId, ntid, cid) {
            const url = `https://www.managerzone.com/ajax.php?p=nationalTeams&sub=search&ntid=${ntid}&cid=${cid}&type=national_team&pid=${playerId}&sport=soccer`;
            try {
                const response = await fetch(url);
                const html = await response.text();
                return this.parseSkills(html);
            } catch (error) {
                return null;
            }
        }
    }

    class PlayerData {
        constructor(id, name, teamName, teamId, age, value, salary, totalBalls, skills) {
            this.id = id;
            this.name = name;
            this.teamName = teamName;
            this.teamId = teamId || null;
            this.age = age;
            this.value = value;
            this.salary = salary;
            this.totalBalls = totalBalls;
            this.skills = skills;
        }
        toExcelRow() {
            return {
                'ID': this.id,
                'Name': this.name,
                'Team': this.teamName,
                'Age': this.age,
                'Value': this.value,
                'Salary': this.salary,
                'Total Balls': this.totalBalls,
                'Speed': this.skills.speed || 0,
                'Stamina': this.skills.stamina || 0,
                'Play Intelligence': this.skills.playIntelligence || 0,
                'Short Passing': this.skills.passing || 0,
                'Shooting': this.skills.shooting || 0,
                'Heading': this.skills.heading || 0,
                'Keeping': this.skills.keeping || 0,
                'Ball Control': this.skills.ballControl || 0,
                'Tackling': this.skills.tackling || 0,
                'Aerial Passing': this.skills.aerialPassing || 0,
                'Set Plays': this.skills.setPlays || 0,
                'Experience': this.skills.experience || 0
            };
        }
    }

    class NTPlayerSearcher {
        constructor() {
            this.requestQueue = new RequestQueue(5, 100);
            this.chunkProcessor = new ChunkProcessor(25);
            this.searchValues = {
                speed: 0,
                stamina: 0,
                playIntelligence: 0,
                passing: 0,
                shooting: 0,
                heading: 0,
                keeping: 0,
                ballControl: 0,
                tackling: 0,
                aerialPassing: 0,
                setPlays: 0,
                experience: 0,
                minAge: 16,
                maxAge: 96,
                totalBalls: 9,
                country: '',
                countryData: null
            };
            this.isSearching = false;
            this.teamIds = new Set();
            this.playerIds = new Map();
            this.processedLeagues = 0;
            this.totalLeagues = 0;
            this.validPlayers = new Map();
        }
        async fetchTopPlayers(country, page = 0, isU21 = false) {
            try {
                const baseUrl = `https://mzlive.eu/mzlive.php?action=list&type=top100&mode=players&country=${country}&currency=EUR`;
                const url = isU21 ? `${baseUrl}&age=u21&page=${page}` : `${baseUrl}&page=${page}`;
                const response = await this.requestQueue.add(() =>
                    new Promise((resolve, reject) => {
                        GM_xmlhttpRequest({
                            method: 'GET',
                            url,
                            onload: res => resolve(res),
                            onerror: err => reject(err)
                        });
                    })
                );
                const data = JSON.parse(response.responseText);
                const players = data.players || [];
                const playerEntries = players.map(player => [
                    player.id.toString(),
                    {
                        id: player.id.toString(),
                        name: player.name,
                        teamName: player.team_name,
                        teamId: player.team_id?.toString() || null,
                        age: player.age,
                        value: parseInt(player.value),
                        salary: 0
                    }
                ]);
                this.playerIds = new Map([...this.playerIds, ...playerEntries]);
                return players.map(player => player.id.toString());
            } catch (error) {
                this.logger.log(`Error fetching top 100 players: ${error.message}`, 'error');
                return [];
            }
        }
        async fetchAllTop100Players(country) {
            const maxPages = MASSIVE_COUNTRIES.includes(country) ? 20 : 5;
            const isU21 = this.searchValues.maxAge <= 21;
            const pages = Array.from({ length: maxPages + 1 }, (_, i) => i);
            const chunkSize = 5;
            const results = [];
            for (let i = 0; i < pages.length; i += chunkSize) {
                const chunk = pages.slice(i, i + chunkSize);
                const chunkResults = await Promise.all(
                    chunk.map(page => this.fetchTopPlayers(country, page, isU21))
                );
                results.push(...chunkResults);
                await new Promise(res => setTimeout(res, 100));
            }
            return results.flat();
        }
        async fetchCountriesList() {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: 'https://pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev/json/countries.json',
                    onload: res => resolve(JSON.parse(res.responseText)),
                    onerror: err => reject(err)
                });
            });
        }
        async fetchUserCountry() {
            const usernameElem = document.querySelector('#header-username');
            if (!usernameElem) return null;
            const username = usernameElem.textContent.trim();
            const response = await fetch(`https://www.managerzone.com/xml/manager_data.php?sport_id=1&username=${username}`);
            const text = await response.text();
            const parser = new DOMParser();
            const xmlDoc = parser.parseFromString(text, "text/xml");
            return xmlDoc.querySelector('UserData')?.getAttribute('countryShortname') || null;
        }
        async init() {
            const loading = this.showLoading();
            loading.classList.add('visible');
            try {
                const [countries, userCountry] = await Promise.all([
                    this.fetchCountriesList(),
                    this.fetchUserCountry()
                ]);
                this.countries = countries;
                this.userCountry = userCountry;
                this.searchValues.country = userCountry;
                await this.appendSearchTab();
                this.setUpEvents();
                const logContainer = document.querySelector('.mz-search-log');
                this.logger = new Logger(logContainer);
                const progressBar = document.querySelector('.mz-progress-fill');
                this.progressManager = new ProgressManager(progressBar);
            } finally {
                loading.classList.remove('visible');
                setTimeout(() => loading.remove(), 300);
            }
        }
        showLoading() {
            const loading = document.createElement('div');
            loading.className = 'mz-loading';
            const spinner = document.createElement('div');
            spinner.className = 'mz-spinner';
            loading.appendChild(spinner);
            document.body.appendChild(loading);
            return loading;
        }
        async getLeagueIds(countryCode) {
            try {
                const response = await this.requestQueue.add(() =>
                    new Promise((resolve, reject) => {
                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: `https://mzlive.eu/mzlive.php?action=list&type=leagues&country=${countryCode}`,
                            onload: res => resolve(res),
                            onerror: err => reject(err)
                        });
                    })
                );
                const leagues = JSON.parse(response.responseText);
                const maxDivision = MASSIVE_COUNTRIES.includes(countryCode) ? 6 : 3;
                return leagues.filter(league => {
                    const name = league.name.toLowerCase();
                    if (name.startsWith('div')) {
                        const divLevel = parseInt(name.split('.')[0].replace('div', ''));
                        return divLevel <= maxDivision;
                    }
                    return true;
                }).map(league => league.id);
            } catch (error) {
                this.logger.log(`Error fetching leagues: ${error.message}`, 'error');
                throw error;
            }
        }
        async getTeamIds(leagueId) {
            try {
                const response = await this.requestQueue.add(() =>
                    fetch(`https://www.managerzone.com/xml/team_league.php?sport_id=1&league_id=${leagueId}`)
                );
                const text = await response.text();
                const parser = new DOMParser();
                const xmlDoc = parser.parseFromString(text, "text/xml");
                const teams = xmlDoc.getElementsByTagName('Team');
                return Array.from(teams).map(team => team.getAttribute('teamId'));
            } catch (error) {
                this.logger.log(`Error fetching teams for league ${leagueId}: ${error.message}`, 'error');
                return [];
            }
        }
        async processLeagueBatch(leagueIds) {
            await this.chunkProcessor.process(
                leagueIds,
                async (leagueId) => {
                    try {
                        const teamIds = await this.getTeamIds(leagueId);
                        teamIds.forEach(id => this.teamIds.add(id));
                        this.processedLeagues++;
                        this.logger.log(`Processed league ${leagueId}`);
                    } catch (error) {
                        this.logger.log(`Failed to process league ${leagueId}: ${error}`, 'error');
                    }
                },
                (processed, total) => {
                    const progressPercent = (processed / total) * 100;
                    this.progressManager.update(progressPercent);
                }
            );
        }
        async fetchPlayerList(teamId) {
            try {
                const response = await this.requestQueue.add(() =>
                    fetch(`https://www.managerzone.com/xml/team_playerlist.php?sport_id=1&team_id=${teamId}`)
                );
                const text = await response.text();
                const parser = new DOMParser();
                const xmlDoc = parser.parseFromString(text, "text/xml");
                const teamPlayers = xmlDoc.querySelector('TeamPlayers');
                const teamName = teamPlayers?.getAttribute('teamName') || '';
                const actualTeamId = teamPlayers?.getAttribute('teamId') || teamId;
                const players = xmlDoc.getElementsByTagName('Player');
                const targetCountry = this.searchValues.country.toLowerCase();
                const validPlayers = Array.from(players).filter(player => {
                    const age = parseInt(player.getAttribute('age'));
                    const countryCode = player.getAttribute('countryShortname').toLowerCase();
                    return age >= this.searchValues.minAge &&
                        age <= this.searchValues.maxAge &&
                        countryCode === targetCountry;
                });
                validPlayers.forEach(player => {
                    const playerId = player.getAttribute('id');
                    const playerName = player.getAttribute('name');
                    const value = parseInt(player.getAttribute('value')) || 0;
                    const salary = parseInt(player.getAttribute('salary')) || 0;
                    const age = parseInt(player.getAttribute('age'));
                    if (playerId) {
                        this.playerIds.set(playerId, {
                            id: playerId,
                            name: playerName,
                            teamName: teamName,
                            teamId: actualTeamId,
                            age: age,
                            value: value,
                            salary: salary
                        });
                    }
                });
            } catch (error) {
                this.logger.log(`Error fetching players for team ${teamId}: ${error.message}`, 'error');
            }
        }
        async processTeamBatch(teamIds) {
            await this.chunkProcessor.process(
                teamIds,
                async (teamId) => {
                    await this.fetchPlayerList(teamId);
                    this.logger.log(`Processed team ${teamId}`);
                }
            );
        }
        async searchForPlayers() {
            if (!this.searchValues.country) {
                this.logger.log('No country selected', 'error');
                return;
            }
            this.teamIds = new Set();
            this.playerIds = new Map();
            this.processedLeagues = 0;
            this.validPlayers = new Map();
            const countryCode = this.searchValues.country;
            this.logger.log(`Starting search for country ${countryCode}`);
            try {
                if (this.searchValues.maxAge > 18) {
                    await this.fetchAllTop100Players(countryCode);
                    this.logger.log(`Found ${this.playerIds.size} players from MZLists`);
                }
                const leagueIds = await this.getLeagueIds(countryCode);
                this.totalLeagues = leagueIds.length;
                this.logger.log(`Found ${leagueIds.length} leagues to process`);
                await this.processLeagueBatch(leagueIds);
                this.logger.log('Processing teams...');
                await this.processTeamBatch(Array.from(this.teamIds));
                const ntPlayerParser = new NTPlayerParser(this.searchValues);
                const { ntid, cid } = this.searchValues.countryData;
                this.logger.log('Processing player skills...');
                const playerEntries = Array.from(this.playerIds.entries());
                let processedCount = 0;
                await this.chunkProcessor.process(
                    playerEntries,
                    async ([playerId, playerData]) => {
                        const parsedData = await ntPlayerParser.fetchAndParsePlayer(playerId, ntid, cid);
                        if (parsedData && parsedData.totalBalls >= this.searchValues.totalBalls) {
                            this.validPlayers.set(playerId, new PlayerData(
                                playerId,
                                playerData.name,
                                playerData.teamName,
                                playerData.teamId,
                                playerData.age,
                                playerData.value,
                                playerData.salary,
                                parsedData.totalBalls,
                                parsedData.skills
                            ));
                            this.logger.log(`Player ${playerData.name} (${playerId}) meets the specified requirements`);
                        }
                        processedCount++;
                        if (processedCount % 10 === 0) {
                            this.progressManager.update((processedCount / playerEntries.length) * 100);
                        }
                    }
                );
                this.logger.log('Finishing…');
                await new Promise(resolve => setTimeout(resolve, 500));
                const finalCount = this.validPlayers.size;
                this.logger.log(`Done: found ${finalCount} valid players`);
                document.querySelector('.mz-results-button').style.display = finalCount > 0 ? "inline-block" : "none";
                return Array.from(this.validPlayers.keys());
            } catch (error) {
                this.logger.log(`Error during search: ${error.message}`, 'error');
                console.error('Search failed:', error);
                throw error;
            }
        }
        async performSearch() {
            if (this.isSearching || !this.searchValues.country) return;
            this.isSearching = true;
            const internalSearchButton = document.querySelector('.mz-search-container .mz-search-button');
            internalSearchButton.disabled = true;
            const loading = this.showLoading();
            const progress = document.querySelector('.mz-progress');
            const logContainer = document.querySelector('.mz-search-log');
            const resultsButton = document.querySelector('.mz-results-button');
            loading.classList.add('visible');
            progress.classList.add('visible');
            logContainer.innerHTML = '';
            resultsButton.style.display = 'none';
            try {
                await this.searchForPlayers();
            } catch (error) {
                this.logger.log(`Error during search: ${error.message}`, 'error');
                console.error('Search failed:', error);
            } finally {
                this.isSearching = false;
                internalSearchButton.disabled = false;
                progress.classList.remove('visible');
                loading.classList.remove('visible');
                setTimeout(() => loading.remove(), 300);
            }
        }
        getFiltersAppliedText() {
            const filters = [];
            if (this.searchValues.country) {
                filters.push(`Country: ${this.searchValues.country}`);
            }
            filters.push(`Age: ${this.searchValues.minAge} - ${this.searchValues.maxAge}`);
            filters.push(`Total Balls >= ${this.searchValues.totalBalls}`);
            const skillFields = [
                'speed', 'stamina', 'playIntelligence', 'passing', 'shooting',
                'heading', 'keeping', 'ballControl', 'tackling', 'aerialPassing',
                'setPlays', 'experience'
            ];
            skillFields.forEach(skill => {
                if (this.searchValues[skill] > 0) {
                    filters.push(`${this.formatSkillName(skill)} >= ${this.searchValues[skill]}`);
                }
            });
            return filters.join(', ');
        }
        showResults() {
            if (this.validPlayers.size === 0) {
                alert("No valid players found.");
                return;
            }
            const modal = document.createElement('div');
            modal.className = 'mz-results-modal';
            const modalHeader = document.createElement('div');
            modalHeader.className = 'mz-results-header';
            const headerControls = document.createElement('div');
            headerControls.className = 'mz-header-controls';
            const closeButton = document.createElement('button');
            closeButton.className = 'mz-results-close';
            closeButton.innerHTML = '&times;';
            const exportButton = document.createElement('button');
            exportButton.className = 'mz-export-button';
            exportButton.textContent = 'Export';
            exportButton.onclick = () => this.exportToExcel();
            headerControls.appendChild(exportButton);
            headerControls.appendChild(closeButton);
            modalHeader.innerHTML = `
                <div>
                    <h2 class="mz-results-title">Search Results</h2>
                    <div class="mz-results-total">Total players found: ${this.validPlayers.size}</div>
                    <div class="mz-results-filters" style="font-size: 0.9rem; color: #aaa; margin-top: 0.5rem;">
                        Filters applied: ${this.getFiltersAppliedText()}
                    </div>
                </div>`;
            modalHeader.appendChild(headerControls);
            const modalContent = document.createElement('div');
            modalContent.className = 'mz-results-content';
            const playersContainer = document.createElement('div');
            playersContainer.className = 'mz-players-container';
            const paginationContainer = document.createElement('div');
            paginationContainer.className = 'mz-results-pagination';
            const playersArray = Array.from(this.validPlayers.values())
                .sort((a, b) => b.totalBalls - a.totalBalls);
            let currentPage = 1;
            const totalPages = Math.ceil(playersArray.length / PLAYERS_PER_PAGE);
            const renderPage = (page) => {
                playersContainer.innerHTML = '';
                const startIndex = (page - 1) * PLAYERS_PER_PAGE;
                const pagePlayers = playersArray.slice(startIndex, startIndex + PLAYERS_PER_PAGE);
                const paginationMarkup = totalPages > 1 ? `
                    <button class="mz-pagination-button" ${page === 1 ? 'disabled' : ''} data-action="prev">Previous</button>
                    <span class="mz-pagination-info">Page ${page} of ${totalPages}</span>
                    <button class="mz-pagination-button" ${page === totalPages ? 'disabled' : ''} data-action="next">Next</button>
                ` : '';
                paginationContainer.innerHTML = paginationMarkup;
                if (totalPages > 1) {
                    const prevBtn = paginationContainer.querySelector('[data-action="prev"]');
                    const nextBtn = paginationContainer.querySelector('[data-action="next"]');
                    if (prevBtn) {
                        prevBtn.addEventListener('click', () => {
                            if (currentPage > 1) {
                                currentPage--;
                                renderPage(currentPage);
                            }
                        });
                    }
                    if (nextBtn) {
                        nextBtn.addEventListener('click', () => {
                            if (currentPage < totalPages) {
                                currentPage++;
                                renderPage(currentPage);
                            }
                        });
                    }
                }
                pagePlayers.forEach(player => {
                    const skillBars = Object.entries(player.skills)
                        .map(([skill, value]) => `
                            <div class="mz-skill-row">
                                <span class="mz-skill-name">${this.formatSkillName(skill)}</span>
                                <div class="mz-skill-value">
                                    <div class="mz-skill-level">
                                        <div class="mz-skill-fill" style="width: ${(value / 10) * 100}%"></div>
                                    </div>
                                    <span class="mz-skill-number">${value}</span>
                                </div>
                            </div>
                        `).join('');
                    const playerCard = document.createElement('div');
                    playerCard.className = 'mz-player-card';
                    playerCard.innerHTML = `
                        <div class="mz-player-header">
                            <div class="mz-player-info">
                                <h3 class="mz-player-name">
                                    <a href="https://www.managerzone.com/?p=players&pid=${player.id}" target="_blank">
                                        ${player.name} (${player.id})
                                    </a>
                                </h3>
                                <div class="mz-player-details">
                                    <div>Team: ${player.teamId ? `<a href="https://www.managerzone.com/?p=team&tid=${player.teamId}" target="_blank">${player.teamName}</a>` : player.teamName}</div>
                                    <div>Age: ${player.age}</div>
                                    <div>Value: ${new Intl.NumberFormat('en-US').format(player.value)} USD</div>
                                    <div>Total Balls: ${player.totalBalls}</div>
                                </div>
                            </div>
                        </div>
                        <div class="mz-player-skills">
                            ${skillBars}
                        </div>
                    `;
                    playersContainer.appendChild(playerCard);
                });
            };
            modalContent.appendChild(paginationContainer);
            modalContent.appendChild(playersContainer);
            modal.appendChild(modalHeader);
            modal.appendChild(modalContent);
            document.body.appendChild(modal);
            renderPage(currentPage);
            closeButton.addEventListener('click', () => {
                modal.remove();
            });
            document.addEventListener('keydown', (e) => {
                if (e.key === 'Escape') {
                    modal.remove();
                }
            });
        }
        formatSkillName(skill) {
            const names = {
                speed: 'Speed',
                stamina: 'Stamina',
                playIntelligence: 'Play Intelligence',
                passing: 'Short Passing',
                shooting: 'Shooting',
                heading: 'Heading',
                keeping: 'Keeping',
                ballControl: 'Ball Control',
                tackling: 'Tackling',
                aerialPassing: 'Aerial Passing',
                setPlays: 'Set Plays',
                experience: 'Experience'
            };
            return names[skill] || skill;
        }
        exportToExcel() {
            if (this.validPlayers.size === 0) return;
            const worksheet = XLSX.utils.json_to_sheet(
                Array.from(this.validPlayers.values()).map(player => player.toExcelRow())
            );
            const workbook = XLSX.utils.book_new();
            XLSX.utils.book_append_sheet(workbook, worksheet, "Players");
            const date = new Date().toISOString().split('T')[0];
            XLSX.writeFile(workbook, `mz_players_${date}.xlsx`);
        }
        async appendSearchTab() {
            const leftNav = document.querySelector('ul.leftnav');
            if (!leftNav) {
                console.error('Left navigation menu not found');
                return false;
            }
            const menuItem = document.createElement('li');
            menuItem.id = 'leftmenu_nt_search';
            const openButton = document.createElement('a');
            openButton.href = '#';
            openButton.className = 'mz-search-open-btn';
            openButton.innerHTML = 'NT Player Search <i class="fa fa-search"></i>';
            menuItem.appendChild(openButton);
            leftNav.appendChild(menuItem);
            const searchContainer = document.createElement('div');
            searchContainer.className = 'mz-search-container';
            const 씨발 = k => ({ go: { en: 'Go', pt: 'Buscar', es: 'Ir', fr: 'Aller', de: 'Los', it: 'Vai', nl: 'Ga', ru: 'Поехали', ja: '行く', zh: '去', ko: '가자', sv: 'Gå', no: 'Gå', da: 'Gå', fi: 'Mene', pl: 'Idź', cs: 'Jdi', sk: 'Choď', hu: 'Menj', tr: 'Git', el: 'Πήγαινε', ro: 'Du-te', bg: 'Отиди', hr: 'Idi', sr: 'Иди', lt: 'Eik', lv: 'Ejam', et: 'Mine', sl: 'Pojdi', is: 'Fara', ar: 'انطلق', hi: 'जाओ', bn: 'চলো', ur: 'چلو', vi: 'Đi', th: 'ไป', id: 'Pergi', ms: 'Pergi', fa: 'برو', he: 'לך', ca: 'Vés' }[navigator.language.slice(0, 2)] || 'Go' })[k];
            const skillFields = [
                ['speed', 'Speed'],
                ['stamina', 'Stamina'],
                ['playIntelligence', 'Play Intel.'],
                ['passing', 'Short Passing'],
                ['shooting', 'Shooting'],
                ['heading', 'Heading'],
                ['keeping', 'Keeping'],
                ['ballControl', 'Ball Control'],
                ['tackling', 'Tackling'],
                ['aerialPassing', 'Aerial Passing'],
                ['setPlays', 'Set Plays'],
                ['experience', 'Experience']
            ];
            const skillsHTML = skillFields.map(([field, label]) => `
                <div class="mz-search-field">
                    <label>Min. ${label}</label>
                    <select name="${field}">
                        ${this.generateOptions(10)}
                    </select>
                </div>
            `).join('');
            searchContainer.innerHTML = `
                <div class="mz-search-header">
                    <h2>NT Player Search</h2>
                </div>
                <div class="mz-search-grid">
                    ${skillsHTML}
                    <div class="mz-search-field">
                        <label>Min. TotalBalls</label>
                        <select name="totalBalls">
                            ${this.generateOptions(110, 9)}
                        </select>
                    </div>
                    <div class="mz-search-field">
                        <label>Min. Age</label>
                        <select name="minAge">
                            ${this.generateOptions(96, 16)}
                        </select>
                    </div>
                    <div class="mz-search-field">
                        <label>Max. Age</label>
                        <select name="maxAge">
                            ${this.generateOptions(96, 16)}
                        </select>
                    </div>
                    <div class="mz-country-select mz-search-field">
                        <label>Country</label>
                        <select name="country" required>
                            ${this.generateCountryOptions()}
                        </select>
                    </div>
                </div>
                <div class="mz-search-buttons">
                    <button class="mz-search-button">${씨발('go')}</button>
                    <button class="mz-results-button" style="display: none;">Show Results</button>
                </div>
                <div class="mz-progress">
                    <div class="mz-progress-info">
                        <span>Scanning players...</span>
                    </div>
                    <div class="mz-progress-bar">
                        <div class="mz-progress-fill"></div>
                    </div>
                </div>
                <div class="mz-search-log"></div>
                <a href="https://www.managerzone.com/?p=guestbook&uid=8577497"
                   class="mz-guestbook-link"
                   title="Questions?">
                    <i class="fa-solid fa-book"></i>
                </a>`;
            document.body.appendChild(searchContainer);
        }
        generateCountryOptions() {
            return `
                <option value="">Select country</option>
                ${this.countries.map(country => {
                    const isSelected = country.code === this.userCountry;
                    if (isSelected) {
                        this.searchValues.countryData = {
                            ntid: country.ntid,
                            cid: country.cid
                        };
                    }
                    const displayName = country.name === 'Czech Republic' ? 'Czechia' :
                        country.name === 'Macedonia' ? 'North Macedonia' : country.name;
                    return `
                        <option value="${country.code}"
                                data-ntid="${country.ntid}"
                                data-cid="${country.cid}"
                                ${isSelected ? 'selected' : ''}>
                            ${displayName}
                        </option>`;
                }).join('')}
            `;
        }
        generateOptions(max, min = 0) {
            return Array.from({ length: max - min + 1 }, (_, i) => {
                const value = i + min;
                return `<option value="${value}">${value}</option>`;
            }).join('');
        }
        handleSelectChange(e) {
            const select = e.target;
            if (select.name === 'country') {
                const option = select.selectedOptions[0];
                this.searchValues.country = select.value;
                this.searchValues.countryData = {
                    ntid: option.dataset.ntid,
                    cid: option.dataset.cid
                };
            } else {
                this.searchValues[select.name] = parseInt(select.value);
            }
        }
        setUpEvents() {
            const openButton = document.querySelector('.mz-search-open-btn');
            const searchContainer = document.querySelector('.mz-search-container');
            const internalSearchButton = searchContainer.querySelector('.mz-search-button');
            const resultsButton = searchContainer.querySelector('.mz-results-button');
            const selects = searchContainer.querySelectorAll('select');
            selects.forEach(select => {
                select.addEventListener('change', (e) => this.handleSelectChange(e));
            });
            openButton.addEventListener('click', (e) => {
                e.preventDefault();
                searchContainer.classList.add('visible');
            });
            document.addEventListener('click', (e) => {
                if (!searchContainer.contains(e.target) &&
                    !openButton.contains(e.target) &&
                    searchContainer.classList.contains('visible')) {
                    searchContainer.classList.remove('visible');
                }
            });
            document.addEventListener('keydown', (e) => {
                if (e.key === 'Escape' && searchContainer.classList.contains('visible')) {
                    searchContainer.classList.remove('visible');
                }
            });
            internalSearchButton.addEventListener('click', () => this.performSearch());
            resultsButton.addEventListener('click', () => this.showResults());
        }
    }

    const searcher = new NTPlayerSearcher();
    searcher.init();
})();