Greasy Fork

Greasy Fork is available in English.

MZ - NT Player Search

Searches for players who match specific requirements (NC/NCA only)

当前为 2025-04-06 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         MZ - NT Player Search
// @namespace    douglaskampl
// @version      2.99
// @description  Searches for players who match specific requirements (NC/NCA only)
// @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
// @connect      https://www.managerzone.com/
// @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');.nt-search-open-btn{display:inline-block;padding:4px 8px;color:white;font-weight:bold;text-decoration:none;font-size:12px;font-family:'Space Mono',monospace;background:linear-gradient(135deg, #ff6e40, #ff5252, #448aff);border-radius:4px;transition:all .2s ease-in-out;border:1px solid rgba(138,43,226,.1);text-shadow:1px 1px 2px rgba(0,0,0,.3);margin-left:10px;vertical-align:middle}.nt-search-open-btn:hover{background:linear-gradient(145deg,rgba(40,40,70,.9),rgba(50,50,90,.9));color:lightgray;text-decoration:none;box-shadow:inset 0 0 5px rgba(138,43,226,.2)}.nt-search-open-btn i{margin-left:5px;color:violet}.nt-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)}.nt-search-container.visible{visibility:visible;opacity:1;transform:translate(-50%,-50%) scale(1)}.nt-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)}.nt-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)}.nt-search-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-bottom:1.5rem}.nt-search-field{display:flex;flex-direction:column;gap:.5rem}.nt-search-field label{color:#ff9966;font-size:.875rem;text-transform:uppercase;letter-spacing:1px}.nt-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}.nt-search-field select:focus{outline:none;border-color:#ff9966;box-shadow:0 0 0 2px rgba(138,43,226,.2)}.nt-search-field select:disabled{opacity:0.5;cursor:not-allowed;background:#333}.nt-search-buttons{display:flex;justify-content:center;align-items:center;gap:1rem;margin-top:1rem}.nt-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)}.nt-search-button:not(:disabled):hover{transform:translateY(-2px);box-shadow:0 6px 8px rgba(0,0,0,.2)}.nt-search-button:disabled{opacity:0.5;cursor:not-allowed;background:#666}.nt-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}.nt-search-log::-webkit-scrollbar{width:8px;height:8px}.nt-search-log::-webkit-scrollbar-track{background:#1a1a2e;border-radius:4px}.nt-search-log::-webkit-scrollbar-thumb{background:#6366f1;border-radius:4px}.nt-search-log::-webkit-scrollbar-thumb:hover{background:#4834d4}.nt-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)}}.nt-search-guestbook-link{position:fixed;bottom:1rem;right:1rem;color:#ff9966;transition:all .2s}.nt-search-guestbook-link:hover{color:#6366f1;transform:scale(1.1)}.nt-search-country-select{width:200px}.nt-search-country-select select{width:100%}.nt-search-loading{display:flex;justify-content:center;align-items:center;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(10,10,20,.9);z-index:10000;visibility:hidden;opacity:0;transition:opacity .3s}.nt-search-loading.visible{visibility:visible;opacity:1}.nt-orbital-spinner{position:relative;width:60px;height:60px}.nt-orbiter{position:absolute;width:10px;height:10px;border-radius:50%;top:50%;left:50%;margin:-5px}.nt-orbiter:nth-child(1){background:violet;animation:orbit1 2s linear infinite}.nt-orbiter:nth-child(2){background:#ff9966;animation:orbit2 2s linear infinite 0.2s}.nt-orbiter:nth-child(3){background:#00ffff;animation:orbit3 2s linear infinite 0.4s}@keyframes orbit1{0%{transform:rotate(0deg) translateX(25px) rotate(0deg) scale(1)}50%{transform:rotate(180deg) translateX(25px) rotate(-180deg) scale(0.7)}100%{transform:rotate(360deg) translateX(25px) rotate(-360deg) scale(1)}}@keyframes orbit2{0%{transform:rotate(120deg) translateX(25px) rotate(-120deg) scale(1)}50%{transform:rotate(300deg) translateX(25px) rotate(-300deg) scale(0.7)}100%{transform:rotate(480deg) translateX(25px) rotate(-480deg) scale(1)}}@keyframes orbit3{0%{transform:rotate(240deg) translateX(25px) rotate(-240deg) scale(1)}50%{transform:rotate(420deg) translateX(25px) rotate(-420deg) scale(0.7)}100%{transform:rotate(600deg) translateX(25px) rotate(-600deg) scale(1)}}.nt-search-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}.nt-search-results-button:hover{transform:translateY(-2px);box-shadow:0 6px 8px rgba(0,0,0,.2)}.nt-search-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%)}}.nt-search-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}.nt-search-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)}.nt-search-results-close{background:none;border:none;color:#ff9966;font-size:1.5rem;cursor:pointer;transition:all 0.2s;padding:0.5rem}.nt-search-results-close:hover{color:#6366f1;transform:scale(1.1)}.nt-search-results-content{padding:1.5rem;height:calc(90vh - 5rem);overflow-y:auto;scrollbar-width:thin;scrollbar-color:#6366f1 #1a1a2e}.nt-search-results-content::-webkit-scrollbar{width:8px}.nt-search-results-content::-webkit-scrollbar-track{background:#1a1a2e}.nt-search-results-content::-webkit-scrollbar-thumb{background:#6366f1;border-radius:4px}.nt-search-results-content::-webkit-scrollbar-thumb:hover{background:#4834d4}.nt-search-players-container{display:flex;flex-wrap:wrap;gap:1.5rem;margin:1rem 0}.nt-search-player-card{background:rgba(26,26,46,.5);border-radius:8px;padding:1rem;transition:all 0.2s;border:1px solid rgba(138,43,226,.1);flex:1 1 calc(50% - 1rem);box-sizing:border-box;display:flex;flex-direction:column;min-width:350px}.nt-search-player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(83,11,237,.2)}.nt-search-player-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.nt-search-player-info{flex:1}.nt-search-player-name{font-size:1.1rem;font-weight:bold;color:#fff;margin:0 0 0.5rem 0}.nt-search-player-name a{color:inherit;text-decoration:none}.nt-search-player-name a:hover{color:violet}.nt-search-player-details{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.5rem;color:#ff9966;font-size:0.875rem}.nt-search-skills-list{margin-top:1rem;display:flex;flex-direction:column;gap:4px}.nt-search-skill-row{display:flex;align-items:center;background:transparent;padding:0;box-shadow:none;min-height:24px}.nt-search-skill-name{font-size:.8rem;color:#f0f0f0;flex-basis:80px;margin-right:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.nt-search-skill-value{display:flex;align-items:center;gap:4px;color:#ff9966;flex-shrink:0}.nt-search-skill-value img{height:.9em;width:auto;vertical-align:middle}.nt-search-skill-value-text{font-size:.8rem;white-space:nowrap}.nt-search-player-total-balls{margin-top:0.75rem;font-weight:bold;color:#ffcc66;text-align:right;font-size:0.9rem}.nt-search-results-pagination{display:flex;justify-content:center;align-items:center;gap:1rem;padding:1rem 0;border-top:1px solid rgba(138,43,226,.1);border-bottom:1px solid rgba(138,43,226,.1);margin:0 -1.5rem 1rem -1.5rem}.nt-search-results-pagination.bottom{border-top:1px solid rgba(138,43,226,.1);border-bottom:none;margin-top:1rem;margin-bottom:0}.nt-search-results-pagination.top{border-bottom:1px solid rgba(138,43,226,.1);border-top:none;margin-bottom:1rem;margin-top:0}.nt-search-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}.nt-search-pagination-button:not(:disabled):hover{background:#2a2a4e;transform:translateY(-1px)}.nt-search-pagination-button:disabled{opacity:0.5;cursor:not-allowed}.nt-search-pagination-info{color:#ff9966;font-size:0.875rem}.nt-search-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}.nt-search-export-button:hover{background:#2a2a4e;transform:translateY(-1px)}.nt-search-export-button:active{transform:translateY(1px)}.nt-search-header-controls{display:flex;align-items:center;gap:1rem}`);

    const MASSIVE_COUNTRIES = ['BR', 'CN', 'AR', 'SE', 'PL', 'TR'];
    const PLAYERS_PER_PAGE = 10;
    const ORDERED_SKILL_KEYS = [
        "speed", "stamina", "playIntelligence", "passing", "shooting", "heading",
        "keeping", "ballControl", "tackling", "aerialPassing", "setPlays", "experience"
    ];

    class Logger {
        constructor(container, flushInterval = 400) {
            this.container = container;
            this.flushInterval = flushInterval;
            this.queue = [];
            this.timeout = null;
            this.scheduled = false;
        }
        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.scheduled) {
                this.scheduled = true;
                this.timeout = setTimeout(() => this.flush(), this.flushInterval);
            }
        }
        flush() {
            if (!this.queue.length || !this.container) {
                this.scheduled = false;
                return;
            }
            const fragment = document.createDocumentFragment();
            this.queue.forEach(({ message, type }) => {
                const entry = document.createElement('div');
                entry.className = `nt-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;
            }
            this.scheduled = false;
        }
    }

    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 NTPlayerParser {
        constructor(minRequirements) {
            this.minRequirements = minRequirements;
            this.logger = null;
        }
        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;
            }
            let skillRows = Array.from(rows);
            if (skillRows.length > ORDERED_SKILL_KEYS.length) { skillRows = skillRows.slice(0, ORDERED_SKILL_KEYS.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[ORDERED_SKILL_KEYS[index]] = value;
                }
            });
            if (Object.keys(skills).length === 0) return null;
            ORDERED_SKILL_KEYS.forEach(key => {
                if (!(key in skills)) {
                    skills[key] = 0;
                }
            });
            if (!this.validateSkills(skills)) return null;
            return { skills, totalBalls };
        }
        validateSkills(skills) {
            return Object.entries(this.minRequirements)
                .filter(([key]) => key in skills && typeof skills[key] === 'number')
                .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);
                if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
                const html = await response.text();
                return this.parseSkills(html);
            } catch (error) {
                 if (this.logger) this.logger.log(`Error parsing player ${playerId}: ${error.message}`, '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() {
            const row = {
                'ID': this.id,
                'Name': this.name,
                'Team': this.teamName,
                'Age': this.age,
                'Value': this.value,
                'Salary': this.salary,
                'Total Balls': this.totalBalls,
            };
            ORDERED_SKILL_KEYS.forEach(key => {
                row[NTPlayerSearcher.prototype.formatSkillName(key)] = this.skills[key] || 0;
            });
            return row;
        }
    }

    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: 40,
                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();
            this.loadingElement = null;
            this.logger = null;
            this.countries = [];
            this.userCountry = null;
            this.username = null;
            this.currentResultsPage = 1;
            this.resultsListeners = { prev: null, next: null, esc: null };
        }
        async fetchTopPlayers(country, page = 0, isU21 = false) {
            try {
                const baseUrl = `https://mzlive.eu/mzlive.php?action=list&type=top100&mode=players&country=${country}&cy=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),
                            ontimeout: () => reject(new Error(`Timeout fetching Top100 page ${page}`))
                        });
                    })
                );
                const data = JSON.parse(response.responseText);
                const players = (data.players || []).filter(player => {
                    return player.age >= this.searchValues.minAge && player.age <= this.searchValues.maxAge;
                });
                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) || 0,
                        salary: 0
                    }
                ]);
                this.playerIds = new Map([...this.playerIds, ...playerEntries]);
                return players.map(player => player.id.toString());
            } catch (error) {
                if (this.logger) this.logger.log(`Error fetching Top100 players (page ${page}): ${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 = [];
            if (this.logger) this.logger.log(`Fetching Top100 players...`);
            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));
            }
            if (this.logger) this.logger.log(`Finished fetching Top100 players.`);
            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),
                    ontimeout: () => reject(new Error('Timeout fetching countries list'))
                });
            });
        }
        async fetchUserCountry() {
            const usernameElem = document.querySelector('#header-username');
            if (!usernameElem) return { userCountry: null, username: null };
            const username = usernameElem.textContent.trim();
            try {
                const response = await fetch(`https://www.managerzone.com/xml/manager_data.php?sport_id=1&username=${username}`);
                 if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                const text = await response.text();
                const parser = new DOMParser();
                const xmlDoc = parser.parseFromString(text, "text/xml");
                const parserError = xmlDoc.querySelector('parsererror');
                 if (parserError) {
                     console.error('XML parsing error:', parserError.textContent);
                     return { userCountry: null, username: username };
                 }
                 const countryCode = xmlDoc.querySelector('UserData')?.getAttribute('countryShortname') || null;
                return { userCountry: countryCode, username: username };
            } catch (error) {
                 console.error("Error fetching user country:", error);
                 if (this.logger) this.logger.log(`Error fetching user country: ${error.message}`, 'error');
                 return { userCountry: null, username: username };
            }
        }
        async checkUserRole(ntid, cid, username) {
            if (!ntid || !cid || !username) {
                if (this.logger) this.logger.log("Missing ntid, cid, or username for role check.", "warn");
                return false;
            }
            const url = `https://www.managerzone.com/ajax.php?p=nationalTeams&sub=team&ntid=${ntid}&cid=${cid}&type=national_team&sport=soccer`;
            try {
                const response = await fetch(url);
                if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
                const html = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(html, 'text/html');
                const profileLinks = doc.querySelectorAll('table.padding a[href*="/?p=profile&uid="]');
                for (const link of profileLinks) {
                    if (link.textContent.trim() === username) {
                        if (this.logger) this.logger.log(`User ${username} confirmed as NC/NCA.`, 'info');
                        return true;
                    }
                }
                if (this.logger) this.logger.log(`User ${username} is not NC or NCA for this country.`, 'info');
                return false;
            } catch (error) {
                console.error(`Error checking user role: ${error.message}`);
                if (this.logger) this.logger.log(`Error checking user role: ${error.message}`, 'error');
                return false;
            }
        }
        async init() {
            this.createLoadingElement();
            this.showLoading();
            try {
                const [countries, { userCountry, username }] = await Promise.all([
                    this.fetchCountriesList(),
                    this.fetchUserCountry()
                ]);
                this.countries = countries || [];
                this.userCountry = userCountry;
                this.username = username;
                let isAuthorized = false;
                let userCountryData = null;
                if (this.userCountry && this.username && this.countries.length > 0) {
                    userCountryData = this.countries.find(c => c.code === this.userCountry);
                    if (userCountryData) {
                        this.searchValues.country = this.userCountry;
                        this.searchValues.countryData = { ntid: userCountryData.ntid, cid: userCountryData.cid };
                    }
                 }
                 const tempLogContainer = document.createElement('div');
                 this.logger = new Logger(tempLogContainer);
                 if (userCountryData && this.username) {
                    isAuthorized = await this.checkUserRole(userCountryData.ntid, userCountryData.cid, this.username);
                 } else {
                     if (this.logger) this.logger.log("Could not find user country data or username.", "warn");
                 }

                if (isAuthorized) {
                    const appended = await this.appendSearchTab();
                    if (!appended) {
                        throw new Error("Failed to append search tab elements.");
                    }
                    const logContainer = document.querySelector('.nt-search-log');
                    if (logContainer) {
                         this.logger.container = logContainer;
                        if (!this.userCountry) {
                             this.logger.log("Could not determine user country data.", "warn");
                        }
                    } else {
                        console.error("Log container not found after appending tab.");
                        this.logger = { log: console.log, flush: () => {} };
                    }
                    this.setUpEvents();
                } else {
                    console.log("User is not authorized (NC/NCA) or required data missing. NT Search UI not added.");
                    if (this.logger) this.logger.log("User not authorized (NC/NCA) or essential data missing. Search tool disabled.", "warn");
                }
            } catch (error) {
                 console.error("Initialization failed:", error);
                 if (this.logger) {
                    this.logger.log(`Initialization failed: ${error.message}`, 'error');
                 } else {
                    alert(`Initialization failed: ${error.message}`);
                 }
            }
            finally {
                if (this.logger) this.logger.flush();
                this.hideLoading();
            }
        }
        createLoadingElement() {
            if (this.loadingElement) return;
            this.loadingElement = document.createElement('div');
            this.loadingElement.className = 'nt-search-loading';
            const spinnerContainer = document.createElement('div');
            spinnerContainer.className = 'nt-orbital-spinner';
            spinnerContainer.innerHTML = `
                <div class="nt-orbiter"></div>
                <div class="nt-orbiter"></div>
                <div class="nt-orbiter"></div>
            `;
            this.loadingElement.appendChild(spinnerContainer);
            document.body.appendChild(this.loadingElement);
        }
        showLoading() {
            if (this.loadingElement) {
                this.loadingElement.classList.add('visible');
            }
        }
        hideLoading() {
            if (this.loadingElement) {
                this.loadingElement.classList.remove('visible');
            }
        }
        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),
                            ontimeout: () => reject(new Error('Timeout fetching leagues'))
                        });
                    })
                );
                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', ''));
                         if(isNaN(divLevel)) return true;
                        return divLevel <= maxDivision;
                    }
                    return true;
                }).map(league => league.id);
            } catch (error) {
                if (this.logger) this.logger.log(`Error fetching leagues: ${error.message}`, 'error');
                 return [];
            }
        }
        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}`)
                );
                 if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for league ${leagueId}`);
                const text = await response.text();
                const parser = new DOMParser();
                const xmlDoc = parser.parseFromString(text, "text/xml");
                const parserError = xmlDoc.querySelector('parsererror');
                 if (parserError) {
                     console.error(`XML parsing error for league ${leagueId}:`, parserError.textContent);
                     throw new Error(`XML parsing error for league ${leagueId}`);
                 }
                const teams = xmlDoc.getElementsByTagName('Team');
                return Array.from(teams).map(team => team.getAttribute('teamId'));
            } catch (error) {
                if (this.logger) this.logger.log(`Error fetching teams for league ${leagueId}: ${error.message}`, 'error');
                return [];
            }
        }
        async processLeagueBatch(leagueIds) {
            if (!leagueIds || leagueIds.length === 0) {
                 if (this.logger) this.logger.log("No league IDs to process.", "warn");
                 return;
            }
             if (this.logger) this.logger.log(`Processing ${leagueIds.length} leagues...`);
            await this.chunkProcessor.process(
                leagueIds,
                async (leagueId) => {
                    try {
                        const teamIds = await this.getTeamIds(leagueId);
                         if (teamIds && teamIds.length > 0) {
                             teamIds.forEach(id => this.teamIds.add(id));
                         }
                        this.processedLeagues++;
                    } catch (error) {
                         if (this.logger) this.logger.log(`Failed to process league ${leagueId}: ${error}`, 'error');
                    }
                }
            );
             if (this.logger) this.logger.log(`Finished processing leagues. Found ${this.teamIds.size} unique teams.`);
        }
        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}`)
                );
                 if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for team ${teamId}`);
                const text = await response.text();
                const parser = new DOMParser();
                const xmlDoc = parser.parseFromString(text, "text/xml");
                const parserError = xmlDoc.querySelector('parsererror');
                 if (parserError) {
                    console.error(`XML parsing error for team ${teamId}:`, parserError.textContent);
                    throw new Error(`XML parsing error for team ${teamId}`);
                 }
                const teamPlayers = xmlDoc.querySelector('TeamPlayers');
                 if (!teamPlayers) {
                     if (this.logger) this.logger.log(`No TeamPlayers data found for team ${teamId}`, 'warn');
                     return;
                 }
                const teamName = teamPlayers.getAttribute('teamName') || `Team ${teamId}`;
                const actualTeamId = teamPlayers.getAttribute('teamId') || teamId;
                const players = xmlDoc.getElementsByTagName('Player');
                const targetCountry = this.searchValues.country.toLowerCase();
                Array.from(players).forEach(player => {
                    const age = parseInt(player.getAttribute('age'));
                    const countryCodeAttr = player.getAttribute('countryShortname');
                     if(!countryCodeAttr) return;
                    const countryCode = countryCodeAttr.toLowerCase();
                    if (age >= this.searchValues.minAge && age <= this.searchValues.maxAge && countryCode === targetCountry) {
                         const playerId = player.getAttribute('id');
                         const playerName = player.getAttribute('name');
                         const value = parseInt(player.getAttribute('value')) || 0;
                         const salary = parseInt(player.getAttribute('salary')) || 0;
                         if (playerId && playerName) {
                            this.playerIds.set(playerId, {
                                id: playerId,
                                name: playerName,
                                teamName: teamName,
                                teamId: actualTeamId,
                                age: age,
                                value: value,
                                salary: salary
                            });
                        }
                    }
                });
            } catch (error) {
                if (this.logger) this.logger.log(`Error fetching players for team ${teamId}: ${error.message}`, 'error');
            }
        }
        async processTeamBatch(teamIds) {
             if (!teamIds || teamIds.length === 0) {
                 if (this.logger) this.logger.log("No team IDs to process.", "warn");
                 return;
             }
             const totalTeams = teamIds.length;
             let processedTeams = 0;
             if (this.logger) this.logger.log(`Processing ${totalTeams} teams...`);
             await this.chunkProcessor.process(
                 teamIds,
                 async (teamId) => {
                     await this.fetchPlayerList(teamId);
                     processedTeams++;
                     if (processedTeams % 100 === 0 || processedTeams === totalTeams) {
                        if (this.logger) this.logger.log(`Team processing: ${processedTeams}/${totalTeams}`);
                     }
                 }
             );
             if (this.logger) this.logger.log(`Finished processing teams. Found ${this.playerIds.size} potential players.`);
        }
        async searchForPlayers() {
            if (!this.searchValues.country || !this.searchValues.countryData) {
                if (this.logger) this.logger.log('Country not selected or country data missing.', 'error');
                alert('Please ensure a country is selected.');
                return;
            }
            this.teamIds = new Set();
            this.playerIds = new Map();
            this.processedLeagues = 0;
            this.totalLeagues = 0;
            this.validPlayers = new Map();
            this.requestQueue.reset();
            this.currentResultsPage = 1;
            const countryCode = this.searchValues.country;
            if (this.logger) this.logger.log(`Starting search for country: ${countryCode}`);
            try {
                if (this.searchValues.maxAge > 18) {
                    await this.fetchAllTop100Players(countryCode);
                    if (this.logger) this.logger.log(`Found ${this.playerIds.size} players from Top100.`);
                }
                const leagueIds = await this.getLeagueIds(countryCode);
                this.totalLeagues = leagueIds.length;
                if(this.totalLeagues === 0 && this.playerIds.size === 0){
                   if (this.logger) this.logger.log(`No leagues found and no top players matched. Stopping search.`, 'warn');
                   return;
                }
                await this.processLeagueBatch(leagueIds);
                await this.processTeamBatch(Array.from(this.teamIds));
                const { ntid, cid } = this.searchValues.countryData;
                const ntPlayerParser = new NTPlayerParser(this.searchValues);
                ntPlayerParser.logger = this.logger;
                const playerEntries = Array.from(this.playerIds.entries());
                 if(playerEntries.length === 0) {
                    if (this.logger) this.logger.log('No potential players found after gathering IDs.', 'warn');
                    return;
                 }
                if (this.logger) this.logger.log(`Processing skills for ${playerEntries.length} players...`);
                let processedCount = 0;
                let validCount = 0;
                 const totalPlayersToParse = playerEntries.length;
                 const yieldFrequency = 25;
                await this.chunkProcessor.process(
                    playerEntries,
                    async ([playerId, playerData]) => {
                        try {
                            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
                                ));
                                validCount++;
                            }
                        } catch (parseError) {
                             if (this.logger) this.logger.log(`Error processing player ${playerId} (${playerData.name}): ${parseError.message}`, 'error');
                        } finally {
                             processedCount++;
                             if (processedCount % 100 === 0 || processedCount === totalPlayersToParse) {
                                 if (this.logger) this.logger.log(`Skill check progress: ${processedCount}/${totalPlayersToParse}`);
                             }
                             if (processedCount % yieldFrequency === 0) {
                                 await new Promise(res => setTimeout(res, 0));
                             }
                        }
                    }
                );
                if (this.logger) this.logger.log('Finishing skill processing...');
                await new Promise(resolve => setTimeout(resolve, 200));
                const finalCount = this.validPlayers.size;
                if (this.logger) this.logger.log(`Search complete: found ${finalCount} players matching all criteria.`);
                const resultsButton = document.querySelector('.nt-search-results-button');
                 if(resultsButton) resultsButton.style.display = finalCount > 0 ? "inline-block" : "none";
                return Array.from(this.validPlayers.keys());
            } catch (error) {
                 if (this.logger) this.logger.log(`Critical error during search: ${error.message}`, 'error');
                 console.error('Search failed critically:', error);
                 alert(`An error occurred during the search: ${error.message}`);
            } finally {
                 if (this.logger) this.logger.flush();
            }
        }
        async performSearch() {
            if (this.isSearching) {
                if(this.logger) this.logger.log("Search already in progress.", "warn");
                return;
            }
             if (!this.searchValues.country || !this.searchValues.countryData) {
                 alert("Please select a country before searching.");
                 return;
             }
            this.isSearching = true;
            const internalSearchButton = document.querySelector('.nt-search-container .nt-search-button');
            if(internalSearchButton) internalSearchButton.disabled = true;
            const logContainer = document.querySelector('.nt-search-log');
            const resultsButton = document.querySelector('.nt-search-results-button');
            this.showLoading();
            if(logContainer) logContainer.innerHTML = '';
            if(resultsButton) resultsButton.style.display = 'none';
             if (!this.logger || !this.logger.container) {
                 console.error("Logger not initialized before search start.");
                 const logCont = document.querySelector('.nt-search-log');
                 if(logCont) this.logger = new Logger(logCont);
                 else this.logger = { log: console.log, flush: () => {} };
             }
            try {
                await this.searchForPlayers();
            } catch (error) {
                if (this.logger) this.logger.log(`Unhandled error during search execution: ${error.message}`, 'error');
                console.error('Search execution failed:', error);
                 alert(`An unexpected error occurred: ${error.message}`);
            } finally {
                this.isSearching = false;
                 if(internalSearchButton) internalSearchButton.disabled = false;
                this.hideLoading();
                 if(this.logger) this.logger.flush();
            }
        }
        getFiltersAppliedText() {
            const filters = [];
             const countryName = this.countries.find(c => c.code === this.searchValues.country)?.name || this.searchValues.country;
             if (countryName) {
                filters.push(`Country: ${countryName}`);
            }
            filters.push(`Age: ${this.searchValues.minAge} - ${this.searchValues.maxAge}`);
            filters.push(`Min Total Balls: ${this.searchValues.totalBalls}`);
            ORDERED_SKILL_KEYS.forEach(skill => {
                if (this.searchValues[skill] > 0) {
                    filters.push(`Min ${this.formatSkillName(skill)}: ${this.searchValues[skill]}`);
                }
            });
            return filters.join('; ');
        }
        createPaginationControls(page, totalPages) {
            const container = document.createElement('div');
            container.className = 'nt-search-results-pagination';
            if (totalPages > 1) {
                const prevBtn = document.createElement('button');
                prevBtn.className = 'nt-search-pagination-button';
                prevBtn.textContent = 'Previous';
                prevBtn.disabled = page === 1;
                prevBtn.dataset.action = "prev";
                const pageInfo = document.createElement('span');
                pageInfo.className = 'nt-search-pagination-info';
                pageInfo.textContent = `Page ${page} of ${totalPages}`;
                const nextBtn = document.createElement('button');
                nextBtn.className = 'nt-search-pagination-button';
                nextBtn.textContent = 'Next';
                nextBtn.disabled = page === totalPages;
                nextBtn.dataset.action = "next";
                container.appendChild(prevBtn);
                container.appendChild(pageInfo);
                container.appendChild(nextBtn);
            }
            return container;
        }
        renderResultsPage(page) {
            const playersContainer = document.querySelector('.nt-search-players-container');
            const paginationTopContainer = document.querySelector('.nt-search-results-pagination.top');
            const paginationBottomContainer = document.querySelector('.nt-search-results-pagination.bottom');
            const modalContent = document.querySelector('.nt-search-results-content');
            if (!playersContainer || !paginationTopContainer || !paginationBottomContainer || !modalContent) return;
            this.currentResultsPage = page;
            playersContainer.textContent = '';
            paginationTopContainer.textContent = '';
            paginationBottomContainer.textContent = '';
            const playersArray = Array.from(this.validPlayers.values())
                .sort((a, b) => b.totalBalls - a.totalBalls);
            const totalPages = Math.ceil(playersArray.length / PLAYERS_PER_PAGE);
            const startIndex = (page - 1) * PLAYERS_PER_PAGE;
            const pagePlayers = playersArray.slice(startIndex, startIndex + PLAYERS_PER_PAGE);
            this.removePaginationListeners();
            this.resultsListeners.prev = () => {
                if (this.currentResultsPage > 1) {
                    this.renderResultsPage(this.currentResultsPage - 1);
                    if(modalContent) modalContent.scrollTop = 0;
                }
            };
            this.resultsListeners.next = () => {
                if (this.currentResultsPage < totalPages) {
                    this.renderResultsPage(this.currentResultsPage + 1);
                    if(modalContent) modalContent.scrollTop = 0;
                }
            };
            const topControls = this.createPaginationControls(page, totalPages);
            const bottomControls = this.createPaginationControls(page, totalPages);
            paginationTopContainer.appendChild(topControls);
            paginationBottomContainer.appendChild(bottomControls);
            this.addPaginationListeners(paginationTopContainer);
            this.addPaginationListeners(paginationBottomContainer);
             const fragment = document.createDocumentFragment();
            pagePlayers.forEach(player => {
                let skillsHTML = '';
                ORDERED_SKILL_KEYS.forEach((skillKey) => {
                    const value = player.skills[skillKey] || 0;
                    const skillName = this.formatSkillName(skillKey);
                    skillsHTML += `
                        <div class="nt-search-skill-row" title="${skillName}: ${value}">
                            <span class="nt-search-skill-name">${skillName}</span>
                            <div class="nt-search-skill-value">
                                <img src="/img/soccer/wlevel_${value}.gif" alt="${value}">
                                <span class="nt-search-skill-value-text">(${value})</span>
                            </div>
                        </div>
                    `;
                });
                const skillsContainerHTML = `<div class="nt-search-skills-list">${skillsHTML}</div>`;
                const playerCard = document.createElement('div');
                playerCard.className = 'nt-search-player-card';
                playerCard.innerHTML = `
                    <div class="nt-search-player-header">
                        <div class="nt-search-player-info">
                            <h3 class="nt-search-player-name">
                                <a href="https://www.managerzone.com/?p=players&pid=${player.id}" target="_blank" title="View player profile (ID: ${player.id})">
                                    ${player.name}
                                </a>
                            </h3>
                            <div class="nt-search-player-details">
                                <span>Team: ${player.teamId ? `<a href="https://www.managerzone.com/?p=team&tid=${player.teamId}" target="_blank" title="View team profile">${player.teamName}</a>` : player.teamName}</span>
                                <span>Age: ${player.age}</span>
                                <span>Value: ${new Intl.NumberFormat('en-US').format(player.value)} USD</span>
                            </div>
                        </div>
                    </div>
                    ${skillsContainerHTML}
                    <div class="nt-search-player-total-balls">Total Balls: ${player.totalBalls}</div>
                `;
                fragment.appendChild(playerCard);
            });
            playersContainer.appendChild(fragment);
        }
        addPaginationListeners(container) {
            const prevBtn = container.querySelector('[data-action="prev"]');
            const nextBtn = container.querySelector('[data-action="next"]');
            if (prevBtn && this.resultsListeners.prev) prevBtn.addEventListener('click', this.resultsListeners.prev);
            if (nextBtn && this.resultsListeners.next) nextBtn.addEventListener('click', this.resultsListeners.next);
        }
        removePaginationListeners() {
            const containers = document.querySelectorAll('.nt-search-results-pagination');
            containers.forEach(container => {
                const prevBtn = container.querySelector('[data-action="prev"]');
                const nextBtn = container.querySelector('[data-action="next"]');
                if (prevBtn && this.resultsListeners.prev) prevBtn.removeEventListener('click', this.resultsListeners.prev);
                if (nextBtn && this.resultsListeners.next) nextBtn.removeEventListener('click', this.resultsListeners.next);
            });
        }
        showResults() {
            if (this.validPlayers.size === 0) {
                alert("No valid players found matching the criteria.");
                return;
            }
            const existingModal = document.querySelector('.nt-search-results-modal');
             if (existingModal) existingModal.remove();
            this.removePaginationListeners();
            if (this.resultsListeners.esc) {
                document.removeEventListener('keydown', this.resultsListeners.esc);
                this.resultsListeners.esc = null;
            }
            const modal = document.createElement('div');
            modal.className = 'nt-search-results-modal';
            const modalHeader = document.createElement('div');
            modalHeader.className = 'nt-search-results-header';
            const headerControls = document.createElement('div');
            headerControls.className = 'nt-search-header-controls';
            const closeButton = document.createElement('button');
            closeButton.className = 'nt-search-results-close';
            closeButton.innerHTML = '×';
            closeButton.title = 'Close Results (Esc)';
            const exportButton = document.createElement('button');
            exportButton.className = 'nt-search-export-button';
            exportButton.textContent = 'Export';
            exportButton.title = 'Export results to Excel (.xlsx)';
            exportButton.onclick = () => this.exportToExcel();
            headerControls.appendChild(exportButton);
            headerControls.appendChild(closeButton);
            modalHeader.innerHTML = `
                <div>
                    <h2 class="nt-search-results-title">Search Results (${this.validPlayers.size})</h2>
                    <div class="nt-search-results-filters" style="font-size: 0.8rem; color: #bbb; margin-top: 0.5rem; max-width: 700px; line-height: 1.4;">
                        <strong style="color: #ff9966;">Filters:</strong> ${this.getFiltersAppliedText()}
                    </div>
                </div>`;
            modalHeader.appendChild(headerControls);
            const modalContent = document.createElement('div');
            modalContent.className = 'nt-search-results-content';
            const paginationTopContainer = document.createElement('div');
            paginationTopContainer.className = 'nt-search-results-pagination top';
            const playersContainer = document.createElement('div');
            playersContainer.className = 'nt-search-players-container';
            const paginationBottomContainer = document.createElement('div');
            paginationBottomContainer.className = 'nt-search-results-pagination bottom';
            modalContent.appendChild(paginationTopContainer);
            modalContent.appendChild(playersContainer);
            modalContent.appendChild(paginationBottomContainer);
            modal.appendChild(modalHeader);
            modal.appendChild(modalContent);
            document.body.appendChild(modal);
            this.renderResultsPage(this.currentResultsPage);
            const closeModal = () => {
                 this.removePaginationListeners();
                 if (this.resultsListeners.esc) {
                     document.removeEventListener('keydown', this.resultsListeners.esc);
                     this.resultsListeners.esc = null;
                 }
                 modal.remove();
             };
            closeButton.addEventListener('click', closeModal);
            this.resultsListeners.esc = (e) => {
                if (e.key === 'Escape') {
                    closeModal();
                }
            };
            document.addEventListener('keydown', this.resultsListeners.esc);
        }
        formatSkillName(skill) {
            const names = {
                speed: 'Speed',
                stamina: 'Stamina',
                playIntelligence: 'Play Int',
                passing: 'Passing',
                shooting: 'Shooting',
                heading: 'Heading',
                keeping: 'Keeping',
                ballControl: 'Ball Ctrl',
                tackling: 'Tackling',
                aerialPassing: 'Aerial Pass',
                setPlays: 'Set Plays',
                experience: 'Experience'
            };
            return names[skill] || skill.charAt(0).toUpperCase() + skill.slice(1);
        }
        exportToExcel() {
            if (this.validPlayers.size === 0) {
                 alert("No players to export.");
                 return;
             }
            try {
                 const dataToExport = Array.from(this.validPlayers.values())
                    .sort((a,b) => b.totalBalls - a.totalBalls)
                    .map(player => player.toExcelRow());
                 if(dataToExport.length === 0) {
                    alert("No data formatted for export.");
                    return;
                 }
                 const worksheet = XLSX.utils.json_to_sheet(dataToExport);
                 const workbook = XLSX.utils.book_new();
                 XLSX.utils.book_append_sheet(workbook, worksheet, "Players");
                 const date = new Date().toISOString().slice(0, 10);
                 const countryCode = this.searchValues.country || 'export';
                 const filename = `MZ_NT_Search_${countryCode}_${date}.xlsx`;
                 XLSX.writeFile(workbook, filename);
                 if (this.logger) this.logger.log(`Exported ${dataToExport.length} players to ${filename}`);
             } catch (error) {
                 console.error("Excel export failed:", error);
                 alert(`Excel export failed: ${error.message}`);
                 if (this.logger) this.logger.log(`Excel export failed: ${error.message}`, 'error');
             }
        }
        async appendSearchTab() {
            const targetSelect = document.querySelector('#menuForm select#type');
            if (!targetSelect) {
                console.error('Target select element (#menuForm select#type) not found.');
                return false;
            }
            const existingButton = document.querySelector('.nt-search-open-btn');
             if (existingButton) existingButton.remove();
             const existingContainer = document.querySelector('.nt-search-container');
             if (existingContainer) existingContainer.remove();
            const openButton = document.createElement('a');
            openButton.href = '#';
            openButton.className = 'nt-search-open-btn';
            openButton.innerHTML = 'NTPlayerSearch <i class="fa fa-search" style="margin-left: 5px;"></i>';
            openButton.title = 'Open National Team Player Search';
            targetSelect.insertAdjacentElement('afterend', openButton);
            const searchContainer = document.createElement('div');
            searchContainer.className = 'nt-search-container';
            const goText = ((lang) => ({
                 pt: 'Buscar', es: 'Buscar', fr: 'Chercher', de: 'Suchen', it: 'Cerca',
                 pl: 'Szukaj', tr: 'Ara', ro: 'Caută', sv: 'Sök', nl: 'Zoeken'
             }[lang.slice(0, 2)] || 'Search'))(navigator.language);
            const skillFields = ORDERED_SKILL_KEYS.map(key => [key, this.formatSkillName(key)]);
            const skillsHTML = skillFields.map(([field, label]) => `
                <div class="nt-search-field">
                    <label title="Minimum ${label}">${label}</label>
                    <select name="${field}" title="Select minimum ${label}">
                        ${this.generateOptions(10, 0, field)}
                    </select>
                </div>
            `).join('');
            const countryOptionsHTML = this.countries && this.countries.length > 0
                ? this.generateCountryOptions()
                : `<option value="" disabled selected>Could not load countries</option>`;
            searchContainer.innerHTML = `
                <div class="nt-search-header">
                    <h2>NT Player Search</h2>
                    <button class="nt-search-results-close" title="Close Panel (Esc)" style="font-size: 1.2rem; padding: 0.3rem 0.6rem;">×</button>
                </div>
                <div class="nt-search-grid">
                    ${skillsHTML}
                    <div class="nt-search-field">
                        <label title="Minimum Total Balls">Total Balls</label>
                        <select name="totalBalls" title="Select minimum Total Balls">
                            ${this.generateOptions(110, 9, 'totalBalls')}
                        </select>
                    </div>
                    <div class="nt-search-field">
                        <label title="Minimum Age">Min Age</label>
                        <select name="minAge" title="Select minimum Age">
                            ${this.generateOptions(96, 16, 'minAge')}
                        </select>
                    </div>
                    <div class="nt-search-field">
                        <label title="Maximum Age">Max Age</label>
                        <select name="maxAge" title="Select maximum Age">
                            ${this.generateOptions(96, 16, 'maxAge')}
                        </select>
                    </div>
                    <div class="nt-search-country-select nt-search-field">
                        <label>Country</label>
                        <select name="country" required title="Select country (only your country is enabled)">
                            ${countryOptionsHTML}
                        </select>
                    </div>
                </div>
                <div class="nt-search-buttons">
                    <button class="nt-search-button" title="Start searching">${goText}</button>
                    <button class="nt-search-results-button" style="display: none;" title="Show found players">Show Results</button>
                </div>
                <div class="nt-search-log" title="Search process log"></div>
                <a href="https://www.managerzone.com/?p=guestbook&uid=8577497"
                   class="nt-search-guestbook-link"
                   target="_blank"
                   title="Visit Author's Guestbook">
                    <i class="fa fa-book" aria-hidden="true"></i>
                </a>`;
            document.body.appendChild(searchContainer);
            const panelCloseButton = searchContainer.querySelector('.nt-search-header .nt-search-results-close');
             if (panelCloseButton) {
                 panelCloseButton.addEventListener('click', () => {
                     searchContainer.classList.remove('visible');
                 });
             }
             return true;
        }
        generateCountryOptions() {
             if (!this.countries || this.countries.length === 0) {
                 return `<option value="" disabled selected>Error loading countries</option>`;
             }
             const placeholder = `<option value="" disabled ${!this.userCountry ? 'selected' : ''}>Select country</option>`;
             const options = this.countries
                .sort((a, b) => a.name.localeCompare(b.name))
                .map(country => {
                     const isUserCountry = country.code === this.userCountry;
                     const displayName = country.name === 'Czech Republic' ? 'Czechia' :
                         country.name === 'Macedonia' ? 'North Macedonia' : country.name;
                     const selectedAttr = isUserCountry ? 'selected' : '';
                     const disabledAttr = !isUserCountry ? 'disabled' : '';
                     return `
                         <option value="${country.code}"
                                 data-ntid="${country.ntid}"
                                 data-cid="${country.cid}"
                                 ${selectedAttr}
                                 ${disabledAttr}>
                             ${displayName}
                         </option>`;
                 }).join('');
             return placeholder + options;
        }
        generateOptions(max, min = 0, name) {
             let optionsHTML = '';
             const defaultValue = this.searchValues[name];
             for (let i = min; i <= max; i++) {
                 const selected = (defaultValue === i) ? 'selected' : '';
                 optionsHTML += `<option value="${i}" ${selected}>${i}</option>`;
             }
             return optionsHTML;
        }
        handleSelectChange(e) {
            const select = e.target;
            const value = select.value;
            if (select.name === 'country') {
                const option = select.selectedOptions[0];
                 if (option && option.value) {
                     this.searchValues.country = value;
                     this.searchValues.countryData = {
                         ntid: option.dataset.ntid,
                         cid: option.dataset.cid
                     };
                     if(this.logger) this.logger.log(`Country set to: ${option.textContent.trim()}`);
                 } else {
                     this.searchValues.country = '';
                     this.searchValues.countryData = null;
                     if(this.logger) this.logger.log(`Country selection cleared.`, 'warn');
                 }
            } else {
                 const numValue = parseInt(value);
                 if (!isNaN(numValue)) {
                    this.searchValues[select.name] = numValue;
                     if (select.name === 'minAge' && numValue > this.searchValues.maxAge) {
                         this.searchValues.maxAge = numValue;
                         const maxAgeSelect = document.querySelector('select[name="maxAge"]');
                         if (maxAgeSelect) maxAgeSelect.value = numValue;
                     } else if (select.name === 'maxAge' && numValue < this.searchValues.minAge) {
                         this.searchValues.minAge = numValue;
                         const minAgeSelect = document.querySelector('select[name="minAge"]');
                         if (minAgeSelect) minAgeSelect.value = numValue;
                     }
                 }
            }
        }
        setUpEvents() {
            const openButton = document.querySelector('.nt-search-open-btn');
            const searchContainer = document.querySelector('.nt-search-container');
            if (!searchContainer) {
                 console.error("Search container not found for event setup.");
                 return;
             }
            const internalSearchButton = searchContainer.querySelector('.nt-search-button');
            const resultsButton = searchContainer.querySelector('.nt-search-results-button');
            const selects = searchContainer.querySelectorAll('select');
            const panelCloseButton = searchContainer.querySelector('.nt-search-header .nt-search-results-close');
            if (openButton) {
                 openButton.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                     if (searchContainer) {
                        searchContainer.classList.add('visible');
                    } else {
                         console.error("Search container not found when trying to open.");
                     }
                });
            } else {
                console.error("Open button not found during event setup.");
            }
            if (!panelCloseButton?.onclick && panelCloseButton) {
                panelCloseButton.addEventListener('click', () => {
                    if (searchContainer) searchContainer.classList.remove('visible');
                });
             }
            document.addEventListener('click', (e) => {
                 if (searchContainer?.classList.contains('visible') &&
                    !searchContainer.contains(e.target) &&
                    !openButton?.contains(e.target)) {
                     searchContainer.classList.remove('visible');
                 }
             });
            document.addEventListener('keydown', (e) => {
                 if (e.key === 'Escape' && searchContainer?.classList.contains('visible')) {
                     searchContainer.classList.remove('visible');
                 }
            });
            if(selects.length > 0){
                 selects.forEach(select => {
                    if (this.searchValues.hasOwnProperty(select.name)) {
                       select.value = this.searchValues[select.name];
                    }
                    select.addEventListener('change', (e) => this.handleSelectChange(e));
                });
                 const countrySelect = searchContainer.querySelector('select[name="country"]');
                 if(countrySelect && this.searchValues.country){
                    countrySelect.value = this.searchValues.country;
                 } else if(countrySelect && !this.userCountry) {
                     countrySelect.selectedIndex = 0;
                 }
             } else {
                 console.error("Select elements not found inside search container.");
             }
            if (internalSearchButton) {
                 internalSearchButton.addEventListener('click', () => this.performSearch());
            } else {
                 console.error("Internal search button not found.");
             }
            if (resultsButton) {
                resultsButton.addEventListener('click', () => this.showResults());
            } else {
                 console.error("Results button not found.");
            }
        }
    }
    try {
        const searcher = new NTPlayerSearcher();
        if (document.readyState === 'interactive' || document.readyState === 'complete') {
            searcher.init();
        } else {
            document.addEventListener('DOMContentLoaded', () => searcher.init());
        }
    } catch (e) {
         console.error(e);
         alert("Failed to initialize NTPlayerSearch. Check the console for details.");
    }
})();