MZ - WLMatches

Detects potentially weird match results in world leagues

// ==UserScript==
// @name         MZ - WLMatches
// @namespace    douglaskampl
// @version      3.1
// @description  Detects potentially weird match results in world leagues
// @author       Douglas
// @match        https://www.managerzone.com/?p=match&sub=livescores_overview
// @icon         https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @resource     weirdWLMatchesStyles https://br18.org/mz/userscript/other/worldLeagueMatches.css
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    GM_addStyle(GM_getResourceText('weirdWLMatchesStyles'));

    const improvedSpinnerStyles = `
        .mz-loader {
            box-sizing: border-box;
            font-size: 0;
            width: 40px;
            height: 40px;
            position: fixed;
            top: calc(50% - 20px);
            left: calc(50% - 20px);
            z-index: 10001;
            border-radius: 50%;
            border: 4px solid rgba(150, 150, 150, 0.3);
            border-top-color: #555555;
            animation: mz-loader-spin 0.8s linear infinite;
        }

        @keyframes mz-loader-spin {
            to {
                transform: rotate(360deg);
            }
        }
    `;
    GM_addStyle(improvedSpinnerStyles);

    const CONSTANTS = {
        LEAGUE_TYPES: ['senior', 'u18_world', 'u21_world', 'u23_world'],
        LEAGUE_DISPLAY_NAMES: {
            'senior': 'Senior',
            'u18_world': 'U18',
            'u21_world': 'U21',
            'u23_world': 'U23'
        },
        LEAGUE_LIMITS: {
            div1: 1,
            div2: 13,
            div3: 40,
            div4: 121,
            div5: 291
        },
        ROUND_PAIRS: [
            { first: '11', second: '12', display: '11/12' },
            { first: '10', second: '13', display: '10/13' },
            { first: '9', second: '14', display: '9/14' },
            { first: '8', second: '15', display: '8/15' },
            { first: '7', second: '16', display: '7/16' },
            { first: '6', second: '17', display: '6/17' },
            { first: '5', second: '18', display: '5/18' },
            { first: '4', second: '19', display: '4/19' },
            { first: '3', second: '20', display: '3/20' },
            { first: '2', second: '21', display: '2/21' },
            { first: '1', second: '22', display: '1/22' }
        ],
        ERROR_MESSAGES: {
            FETCH_ERROR: 'Error fetching data from league',
            NO_DISCREPANCIES: 'No result inconsistencies found in this league.',
            INVALID_INPUT: 'Please enter a number between 1 and 10.'
        },
        DOM: {
            NAV_UL_SELECTOR: '#leftnav-wrapper ul.leftnav',
            BUTTON_CLASS: 'wl-check-button',
            BUTTON_TEXT: 'CHECK RESULTS',
            BUTTON_TITLE: 'Find inconsistent results between home and away matches'
        },
        REGEX: {
            SCORE: /<a[^>]*>([^<]+)<\/a>/,
            LINK: /<a href="([^"]+)"/,
            UNPLAYED_SCORE: /X\s*-\s*X/
        }
    };

    function getDivisionName(sid) {
        if (sid === CONSTANTS.LEAGUE_LIMITS.div1) return "Top Series";
        if (sid >= 2 && sid <= 4) return `1.${sid - 1}`;
        if (sid >= 5 && sid <= CONSTANTS.LEAGUE_LIMITS.div2) return `2.${sid - 4}`;
        if (sid >= 14 && sid <= CONSTANTS.LEAGUE_LIMITS.div3) return `3.${sid - 13}`;
        return `4.${sid - 40}`;
    }

    async function fetchLeagueData(leagueType, worldLeagueId) {
        let finalLeagueType = leagueType === 'senior' ? 'world' : leagueType;
        const url = `/ajax.php?p=league&type=${finalLeagueType}&sid=${worldLeagueId}&tid=1&sport=soccer&sub=schedule`;
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
            return await response.text();
        } catch (error) {
            console.error(`${CONSTANTS.ERROR_MESSAGES.FETCH_ERROR} ${worldLeagueId}:`, error.message);
            return null;
        }
    }

    function extractMatchesFromHTML(html) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const roundHeaders = doc.querySelectorAll('h2.subheader');
        const matches = [];

        roundHeaders.forEach((header, index) => {
            const roundNumber = index + 1;
            const nextElement = header.nextElementSibling;

            if (nextElement && nextElement.classList.contains('mainContent')) {
                const matchRows = nextElement.querySelectorAll('tr');

                matchRows.forEach(row => {
                    const teams = row.querySelectorAll('td');
                    const scoreLink = row.querySelector('a');

                    if (teams.length >= 3 && scoreLink) {
                        const match = {
                            teams: [
                                teams[0].textContent.trim(),
                                teams[2].textContent.trim()
                            ],
                            score: scoreLink.textContent.trim(),
                            link: scoreLink.getAttribute('href'),
                            round: roundNumber.toString()
                        };
                        matches.push(match);
                    }
                });
            }
        });

        return matches;
    }

    function checkDiscrepancy(homeScore, awayScore) {
        const homeDiff = homeScore[0] - homeScore[1];
        const awayDiff = awayScore[1] - awayScore[0];

        const getWinner = diff => diff > 0 ? 0 : diff < 0 ? 1 : 2;
        const homeWinner = getWinner(homeDiff);
        const awayWinner = getWinner(awayDiff);

        return {
            totalDiff: Math.abs(homeDiff) + Math.abs(awayDiff),
            isDiscrepancy: homeWinner !== 2 && awayWinner !== 2 && homeWinner !== awayWinner
        };
    }

    async function processLeague(leagueType, worldLeagueId, minTotalDiff, roundNumbers) {
        const html = await fetchLeagueData(leagueType, worldLeagueId);
        if (!html) return { discrepancies: [] };

        const matches = extractMatchesFromHTML(html);
        const homeResults = new Map();
        const awayResults = new Map();
        const discrepancies = [];

        matches.forEach(match => {
            if (CONSTANTS.REGEX.UNPLAYED_SCORE.test(match.score)) return;

            const matchKey = `${match.teams[0]} vs ${match.teams[1]}`;

            if (match.round === roundNumbers.first) {
                homeResults.set(matchKey, match);
            } else if (match.round === roundNumbers.second) {
                awayResults.set(matchKey, match);
            }
        });

        for (const [key, awayMatch] of awayResults) {
            const homeKey = `${awayMatch.teams[1]} vs ${awayMatch.teams[0]}`;
            const homeMatch = homeResults.get(homeKey);

            if (!homeMatch) continue;

            const homeScore = homeMatch.score.split(" - ").map(Number);
            const awayScore = awayMatch.score.split(" - ").map(Number);

            const { totalDiff, isDiscrepancy } = checkDiscrepancy(homeScore, awayScore);

            if (totalDiff >= minTotalDiff && isDiscrepancy) {
                discrepancies.push({
                    division: getDivisionName(worldLeagueId),
                    divisionId: worldLeagueId,
                    homeMatch: { ...homeMatch },
                    awayMatch: { ...awayMatch }
                });
            }
        }

        return { discrepancies };
    }

    function createModal(discrepancies, leagueType, minDiff, roundNumbers) {
        const modalHtml = `
            <div class="mz-modal-overlay mz-modern">
                <div class="mz-modal mz-modern">
                    <div class="mz-modal-header mz-modern">
                        <h2 class="mz-modal-title">
                            ${CONSTANTS.LEAGUE_DISPLAY_NAMES[leagueType]} Inconsistent Results [R${roundNumbers.first}/${roundNumbers.second} | Min Diff: ${minDiff}]
                        </h2>
                        <button class="mz-modal-close">×</button>
                    </div>
                    <div class="mz-modal-content mz-modern">
                        ${discrepancies.map(d => `
                            <div class="mz-discrepancy mz-modern">
                                <div class="mz-discrepancy-header mz-modern">
                                    DIVISION: <a href="/?p=league&type=placeholder&sid=${d.divisionId}" class="mz-div-link">${d.division}</a>
                                </div>
                                <div>
                                    R${d.homeMatch.round}: <a href="${d.homeMatch.link}" class="mz-match-link" target="_blank">
                                        ${d.homeMatch.teams[0]} vs ${d.homeMatch.teams[1]} (${d.homeMatch.score})
                                    </a>
                                </div>
                                <div>
                                    R${d.awayMatch.round}: <a href="${d.awayMatch.link}" class="mz-match-link" target="_blank">
                                        ${d.awayMatch.teams[0]} vs ${d.awayMatch.teams[1]} (${d.awayMatch.score})
                                    </a>
                                </div>
                            </div>
                        `).join('')}
                    </div>
                </div>
            </div>
        `;

        const modalElement = document.createElement('div');
        modalElement.innerHTML = modalHtml;
        document.body.appendChild(modalElement);

        const links = modalElement.querySelectorAll('.mz-div-link');
        for (const a of links) {
            const href = a.getAttribute('href');
            const replaced = href.replace('placeholder', encodeURIComponent(leagueType));
            a.setAttribute('href', replaced);
        }

        const closeButton = modalElement.querySelector('.mz-modal-close');
        const overlay = modalElement.querySelector('.mz-modal-overlay');
        const closeModal = () => modalElement.remove();

        closeButton.addEventListener('click', closeModal);
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) closeModal();
        });
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') closeModal();
        }, { once: true });
    }

    function createSpinner() {
        const spinner = document.createElement('div');
        spinner.className = 'mz-loader';
        spinner.style.display = 'none';
        document.body.appendChild(spinner);
        return spinner;
    }

    function promptForParams() {
        const modalHtml = `
            <div class="mz-modal-overlay mz-modern">
                <div class="input-modal mz-modern" onclick="event.stopPropagation()">
                    <h3>SELECT LEAGUE TYPE</h3>
                    <select id="leagueSelect">
                        ${CONSTANTS.LEAGUE_TYPES.map(l => `
                            <option value="${l}">${CONSTANTS.LEAGUE_DISPLAY_NAMES[l]}</option>
                        `).join('')}
                    </select>
                    <h3>SELECT ROUNDS TO COMPARE</h3>
                    <select id="roundSelect">
                        ${CONSTANTS.ROUND_PAIRS.map((pair, index) => `
                            <option value="${index}">${pair.display}</option>
                        `).join('')}
                    </select>
                    <h3>MINIMUM GOAL DIFFERENCE</h3>
                    <input type="text" id="diffInput" placeholder="1-10">
                    <div>
                        <button class="confirm">SEARCH</button>
                        <button class="cancel">CANCEL</button>
                    </div>
                </div>
            </div>
        `;

        return new Promise((resolve, reject) => {
            const modalElement = document.createElement('div');
            modalElement.innerHTML = modalHtml;
            document.body.appendChild(modalElement);

            const leagueSelect = modalElement.querySelector('#leagueSelect');
            const roundSelect = modalElement.querySelector('#roundSelect');
            const input = modalElement.querySelector('#diffInput');
            const confirmBtn = modalElement.querySelector('.confirm');
            const cancelBtn = modalElement.querySelector('.cancel');
            const overlay = modalElement.querySelector('.mz-modal-overlay');

            const cleanup = () => modalElement.remove();

            [leagueSelect, roundSelect, input].forEach(el => {
                el.addEventListener('click', e => e.stopPropagation());
                el.addEventListener('keydown', e => { if (e.key === 'Enter') e.preventDefault(); });
            });

            const handleConfirm = () => {
                const val = parseInt(input.value);
                if (val >= 1 && val <= 10) {
                    const selectedPair = CONSTANTS.ROUND_PAIRS[parseInt(roundSelect.value)];
                    cleanup();
                    resolve({
                        leagueType: leagueSelect.value,
                        diff: val,
                        roundNumbers: {
                            first: selectedPair.first,
                            second: selectedPair.second
                        }
                    });
                } else {
                    alert(CONSTANTS.ERROR_MESSAGES.INVALID_INPUT);
                }
            };

            input.addEventListener('keydown', e => {
                if (e.key === 'Enter') handleConfirm();
            });

            confirmBtn.addEventListener('click', handleConfirm);
            cancelBtn.addEventListener('click', () => {
                cleanup();
                reject('Cancelled');
            });

            overlay.addEventListener('click', (e) => {
                if (e.target === overlay) {
                    cleanup();
                    reject('Cancelled');
                }
            });

            setTimeout(() => leagueSelect.focus(), 0);
        });
    }

    let selectedLeagueType;

    async function init() {
        const navList = document.querySelector(CONSTANTS.DOM.NAV_UL_SELECTOR);
        if (!navList) return;

        const buttonContainer = document.createElement('li');
        const button = document.createElement('button');
        button.className = CONSTANTS.DOM.BUTTON_CLASS;
        button.innerHTML = CONSTANTS.DOM.BUTTON_TEXT;
        button.title = CONSTANTS.DOM.BUTTON_TITLE;

        buttonContainer.appendChild(button);
        navList.appendChild(buttonContainer);

        const spinner = createSpinner();

        button.addEventListener('click', async () => {
            try {
                const params = await promptForParams();
                selectedLeagueType = params.leagueType;
                const minTotalDiff = params.diff;
                const roundNumbers = params.roundNumbers;
                const allDiscrepancies = [];

                spinner.style.display = 'block';
                button.style.cursor = 'wait';
                button.disabled = true;

                for (let worldLeagueId = CONSTANTS.LEAGUE_LIMITS.div1; worldLeagueId <= CONSTANTS.LEAGUE_LIMITS.div2; worldLeagueId++) {
                    const result = await processLeague(selectedLeagueType, worldLeagueId, minTotalDiff, roundNumbers);
                    allDiscrepancies.push(...result.discrepancies);
                }

                spinner.style.display = 'none';
                if (allDiscrepancies.length) {
                    createModal(allDiscrepancies, selectedLeagueType, minTotalDiff, roundNumbers);
                } else {
                    alert(CONSTANTS.ERROR_MESSAGES.NO_DISCREPANCIES);
                }
            } catch (error) {
                spinner.style.display = 'none';
                if (error !== 'Cancelled') {
                    console.error('Error:', error);
                }
            } finally {
                button.style.cursor = 'pointer';
                button.disabled = false;
            }
        });
    }

    init();
})();