maha4india / WH Stats (Ban-Safe Single-Run)

// ==UserScript==
// @name         WH Stats (Ban-Safe Single-Run)
// @match        https://sports.williamhill.com/betting/*
// @match        https://sports.whcdn.net/scoreboards/*
// @run-at       document-end
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ---------------- utils ----------------
  function readStat(statKey) {
    const root = document.querySelector(`div[data-stat="${statKey}"]`);
    if (!root) return null;
    const h = root.querySelector('span.home')?.textContent?.trim() || '0';
    const a = root.querySelector('span.away')?.textContent?.trim() || '0';
    const toInt = (s) => parseInt((s || '').replace('%', ''), 10) || 0;
    return {
      home: toInt(h),
      away: toInt(a)
    };
  }

// ---------------- MODE A: WHCDN iframe (RUN ONCE, WAIT FOR REAL STATS) ----------------
if (location.host === 'sports.whcdn.net') {

  (function () {
    const qs = new URLSearchParams(location.search);
    const eventId = qs.get('eventId');
    if (!eventId) return;

    let sent = false;
    let tries = 0;
    const MAX_TRIES = 20; // ~10 seconds max

    function tryRead() {
      if (sent) return;
      tries++;

      // Try to open stats panel if needed
      const btn = document.querySelector('#toggleStatistic');
      if (btn && !btn.classList.contains('active')) {
        btn.click();
      }

      const sot = readStat('shotsOnTarget');
      const dang = readStat('dangerousAttacks');
      const poss = readStat('possession');

      // Must exist
      if (!sot || !dang || !poss) {
        if (tries < MAX_TRIES) return;
        cleanup();
        return;
      }

      const totalActivity =
        sot.home + sot.away +
        dang.home + dang.away;

      // Wait until we see REAL data (not empty placeholders)
      if (totalActivity === 0 && tries < MAX_TRIES) {
        return;
      }

      // SEND ONCE
      sent = true;

      window.parent.postMessage({
        type: 'WH_STATS',
        eventId,
        homeSOT: sot.home,
        awaySOT: sot.away,
        homeDangerous: dang.home,
        awayDangerous: dang.away,
        homePoss: poss.home,
        awayPoss: poss.away,
        ts: Date.now()
      }, '*');

      cleanup();
    }

    function cleanup() {
      clearInterval(timer);
      // Kill iframe content ASAP
      setTimeout(() => {
        try {
          document.body.innerHTML = '';
          window.stop();
        } catch (e) {}
      }, 100);
    }

    const timer = setInterval(tryRead, 500);

  })();

  return;
}


  function strengthFromProb(p) {
    const pct = p * 100;
    if (pct >= 70) return 'STRONG';
    if (pct >= 55) return 'MEDIUM';
    return 'WEAK';
  }

  function fmtProb(tag, p) {
    if (p === null || p === undefined || isNaN(p)) return `${tag} --`;
    p = clamp01(p);
    const s = strengthFromProb(p);
    return `${tag} ${(p*100).toFixed(0)}% ${s}`;
  }

  // ---------------- MODE B: betting page ----------------
  if (location.host === 'sports.williamhill.com') {

    // ---------------- Esports Filter ----------------
    function isLiveView() {
      return document.querySelectorAll('[id^="OB_EV"]').length > 0;
    }

    function isEsportsText(txt) {
      txt = (txt || '').toLowerCase();
      return txt.includes('esports') || txt.includes('esoccer') || txt.includes('e-soccer') || txt.includes('fifa');
    }

    function hideEsportsInLive() {
      if (!isLiveView()) return;
      const evs = document.querySelectorAll('[id^="OB_EV"]');
      for (let i = 0; i < evs.length; i++) {
        const el = evs[i];
        if (el.style.display === 'none') continue;
        const t = (el.textContent || '').slice(0, 300);
        if (isEsportsText(t)) el.style.display = 'none';
      }
      const tys = document.querySelectorAll('[id^="comp-OB_TY"], [id^="OB_TY"]');
      for (let i = 0; i < tys.length; i++) {
        const el = tys[i];
        if (isEsportsText(el.textContent.slice(0, 300))) {
          const wrap = el.closest('article, section, div.markets-group-container') || el;
          wrap.style.display = 'none';
        }
      }
    }

    // ---------------- Math & Prob Helpers ----------------
    function clamp01(x) {
      return Math.max(0, Math.min(1, x));
    }

    function poissonProbsFast(lambda) {
      lambda = Math.max(0, lambda || 0);
      const e = Math.exp(-lambda);
      const p0 = e;
      const p1 = lambda * e;
      const p2 = (lambda * lambda * 0.5) * e;
      return {
        pGE1: clamp01(1 - p0),
        pGE2: clamp01(1 - (p0 + p1)),
        pGE3: clamp01(1 - (p0 + p1 + p2))
      };
    }

    function parseScoreFromEvent(eventEl) {
      const hTxt = eventEl.querySelector('.btmarketlivescore-item.team-a')?.textContent?.trim();
      const aTxt = eventEl.querySelector('.btmarketlivescore-item.team-b')?.textContent?.trim();
      const h = parseInt(hTxt, 10);
      const a = parseInt(aTxt, 10);
      if (Number.isFinite(h) && Number.isFinite(a)) return {
        GH: h,
        GA: a,
        G: h + a
      };
      return {
        GH: 0,
        GA: 0,
        G: 0
      };
    }

    function parseMinuteFromEvent(eventEl) {
      const raw = eventEl.querySelector('.event__status')?.textContent?.trim() || '';
      const m = raw.match(/(\d{1,2})/);
      if (m) return Math.max(1, Math.min(90, parseInt(m[1], 10)));
      return 1;
    }

    function parse1X2OddsFromEvent(eventEl) {
      const oddsEls = eventEl.querySelectorAll('.betbutton.oddsbutton');
      const odds = [];
      for (let i = 0; i < oddsEls.length && i < 3; i++) {
        const v = parseFloat(oddsEls[i].textContent);
        if (Number.isFinite(v) && v > 1) odds.push(v);
      }
      if (odds.length >= 3) return {
        hOdds: odds[0],
        dOdds: odds[1],
        aOdds: odds[2]
      };
      return {
        hOdds: null,
        dOdds: null,
        aOdds: null
      };
    }

    function estimateLambdaRemaining(t, d, score) {
      const minutesLeft = Math.max(1, 90 - t);
      const baseRate = 2.6 / 90;
      const timePlayed = Math.max(5, t);
      const sotRateH = d.homeSOT / timePlayed;
      const sotRateA = d.awaySOT / timePlayed;
      const daRateH = d.homeDangerous / timePlayed;
      const daRateA = d.awayDangerous / timePlayed;

      const pressure = 15.0 * (sotRateH + sotRateA) + 0.4 * (daRateH + daRateA);

      const goalDiff = Math.abs(score.GH - score.GA);
      let state = 1.0;
      if (goalDiff === 1 && t > 60) state = 1.15;
      if (goalDiff >= 2 && t > 60) state = 0.85;

      let goalRate = baseRate * (0.4 + pressure) * state;
      goalRate = Math.max(0.005, Math.min(0.15, goalRate));
      const lambda = goalRate * minutesLeft;

      const totalSOT = d.homeSOT + d.awaySOT;
      const pH = totalSOT > 0 ? (d.homeSOT / totalSOT) : 0.5;
      const smoothedPH = (pH * totalSOT + 0.5 * 2) / (totalSOT + 2);

      return {
        lambda,
        lambdaH: lambda * smoothedPH,
        lambdaA: lambda * (1 - smoothedPH)
      };
    }

    function computeMarketProbs(t, d, score, odds1x2) {
      const est = estimateLambdaRemaining(t, d, score);
      const pp = poissonProbsFast(est.lambda);

      const pCSH = score.GA > 0 ? 0 : Math.exp(-est.lambdaA);
      const pCSA = score.GH > 0 ? 0 : Math.exp(-est.lambdaH);

      let pBTTS = 0;
      if (score.GH > 0 && score.GA > 0) pBTTS = 1;
      else if (score.GH > 0) pBTTS = 1 - Math.exp(-est.lambdaA);
      else if (score.GA > 0) pBTTS = 1 - Math.exp(-est.lambdaH);
      else pBTTS = (1 - Math.exp(-est.lambdaH)) * (1 - Math.exp(-est.lambdaA));

      // FIXED THRESHOLDS for 15-79 min window
      let verdict = 'SKIP';

      if (pp.pGE2 >= 0.55 && est.lambda > 0.8) {
        verdict = 'OVER';
      }
      else if (pBTTS >= 0.58) {
        verdict = 'BTTS';
      }
      else if (pp.pGE1 <= 0.25 && est.lambda < 0.3) {
        verdict = 'UNDER';
      }

      // Odds veto (keep this)
      if (odds1x2 && verdict === 'UNDER') {
        const {
          hOdds,
          aOdds
        } = odds1x2;
        if (hOdds && aOdds) {
          if ((hOdds < 1.5 && score.GH <= score.GA) || (aOdds < 1.5 && score.GA <= score.GH)) {
            verdict = 'SKIP';
          }
        }
      }

      return {
        pO05: pp.pGE1,
        pO15: pp.pGE2,
        pO25: pp.pGE3,
        pCSH,
        pCSA,
        pBTTS,
        lambda: est.lambda,
        verdict
      };
    }

    function makeBtn() {
      const btn = document.createElement('button');
      btn.textContent = 'Update Stats';
      btn.style.cssText = 'position:fixed;top:60px;right:10px;z-index:99999;background:#27ae60;color:#fff;border:1px solid #000;padding:5px 10px;cursor:pointer;font-weight:bold;';
      return btn;
    }

    function renderIntoEvent(eventEl, d) {
      if (eventEl.style.display === 'none') return;
      const old = eventEl.querySelector('.match-stats-compact');
      if (old) old.remove();

      const score = parseScoreFromEvent(eventEl);
      const t = parseMinuteFromEvent(eventEl);
      const odds1x2 = parse1X2OddsFromEvent(eventEl);
      const probs = computeMarketProbs(t, d, score, odds1x2);

      // Original PI formula
      const totalPI =
        (d.homeSOT * 3 + d.homeDangerous * 0.2) +
        (d.awaySOT * 3 + d.awayDangerous * 0.2);

      // Adjusted thresholds for 15-79min matches
      let piStatus = 'WEAK',
        piColor = '#ff0000';
      if (totalPI >= 18) {
        piStatus = 'STRONG';
        piColor = '#00ff00';
      }
      else if (totalPI >= 10) {
        piStatus = 'MEDIUM';
        piColor = '#ffff00';
      }

      let bgColor = '#ffeb3b';
      if (probs.verdict === 'OVER') bgColor = '#00e676';
      if (probs.verdict === 'BTTS') bgColor = '#00e676';
      if (probs.verdict === 'UNDER') bgColor = '#ff5722';

      const box = document.createElement('div');
      box.className = 'match-stats-compact';
      box.style.cssText = `background:${bgColor};color:#000;padding:2px 4px;font:10px monospace;margin:1px 0;font-weight:bold;border-left:4px solid ${piColor};`;

      box.textContent = `${probs.verdict} | λ${probs.lambda.toFixed(2)} | O.5:${(probs.pO05*100).toFixed(0)}% | O1.5:${(probs.pO15*100).toFixed(0)}% | BTTS:${(probs.pBTTS*100).toFixed(0)}% | SOT:${d.homeSOT}-${d.awaySOT} | ${t}' | PI:${totalPI.toFixed(1)} ${piStatus}`;

      eventEl.prepend(box);

      if (probs.verdict === 'BTTS') {
        setTimeout(() => {
          const headers = eventEl.querySelectorAll('h2');
          for (const h of headers) {
            if (h.textContent.includes('Both Teams To Score')) {
              const toggle = h.closest('.header-dropdown')?.querySelector('a');
              if (toggle && !toggle.closest('.header-dropdown').classList.contains('-expanded')) {
                toggle.click();
              }
              break;
            }
          }
        }, 300);
      }
    }

    // THROTTLED iframe creation (500ms delay between each)
    let iframeQueue = [];
    let processing = false;

    function ensureIframe(eventId) {
      const id = `WH_STATS_FRAME_${eventId}`;
      if (document.getElementById(id)) return;
      iframeQueue.push(eventId);
    }

    function processQueue() {
      if (processing || iframeQueue.length === 0) return;
      processing = true;
      const eventId = iframeQueue.shift();
      const id = `WH_STATS_FRAME_${eventId}`;
      const fr = document.createElement('iframe');
      fr.id = id;
      fr.style.display = 'none';
      fr.src = `https://sports.whcdn.net/scoreboards/app/football/index.html?eventId=${eventId}&sport=football&locale=en-gb`;
      document.body.appendChild(fr);
      setTimeout(() => {
        processing = false;
        processQueue();
      }, 500); // 500ms delay between iframes
    }

    let lastScan = 0;

    function start() {
      if (Date.now() - lastScan < 3000) return;
      lastScan = Date.now();

      hideEsportsInLive();
      const events = document.querySelectorAll('[id^="OB_EV"]');

      for (const el of events) {
        if (el.style.display === 'none') continue;

        // 1. Time filter (15-79 min)
        const minute = parseMinuteFromEvent(el);
        if (minute < 15 || minute > 79) continue;

        // 2. Score filter (skip blowouts)
        const score = parseScoreFromEvent(el);
        const goalDiff = Math.abs(score.GH - score.GA);
        if (goalDiff >= 3) continue;

        // 4. Odds filter (skip one-sided + suspended)
        const odds1x2 = parse1X2OddsFromEvent(el);
        if (!odds1x2.hOdds || !odds1x2.dOdds || !odds1x2.aOdds) continue;
        const minOdds = Math.min(odds1x2.hOdds, odds1x2.aOdds);
        const maxOdds = Math.max(odds1x2.hOdds, odds1x2.aOdds);
        if (minOdds < 1.15 || maxOdds > 12.0) continue;

        const m = el.id.match(/^OB_EV(\d+)/);
        if (m) ensureIframe(m[1]);
      }
      processQueue();
    }

    window.addEventListener('message', (e) => {
      if (e.data?.type !== 'WH_STATS') return;
      const el = document.getElementById(`OB_EV${e.data.eventId}`);
      if (el) renderIntoEvent(el, e.data);
    });

    (function init() {
      const loop = setInterval(() => {
        if (!document.body) return;
        clearInterval(loop);
        const btn = makeBtn();
        btn.onclick = () => {
          btn.disabled = true;
          btn.textContent = 'Loading...';
          start();
          setTimeout(() => {
            btn.disabled = false;
            btn.textContent = 'Update Stats';
          }, 5000);
        };
        document.body.appendChild(btn);
        hideEsportsInLive(); // Run once on load
      }, 500);
    })();

    return;
  }
})();