Analyse de Match

5
(1)
Les analyses des matchs de football

Les analyses des matchs de football

1ère Analyse

2ème Analyse

3ème Analyse

<?php

/** =========================================================================
 *  CONFIG + POLYFILLS (avec if/exists partout)
 * ========================================================================= */

// ✅ CONFIG (évite notices si get_option n’existe pas hors WP)
if (!defined('TP_APISPORTS_KEY')) {
    $envKey = function_exists('getenv') ? getenv('TP_APISPORTS_KEY') : '';
    $optKey = function_exists('get_option') ? (get_option('tp_apisports_key') ?: '') : '';
    define('TP_APISPORTS_KEY', $envKey ?: $optKey ?: '54064c5d72f6a5f59487a5309de88efe');
}
if (!defined('TP_DEBUG_ODDS')) define('TP_DEBUG_ODDS', false);

if (!defined('MINUTE_IN_SECONDS')) define('MINUTE_IN_SECONDS', 60);
if (!defined('HOUR_IN_SECONDS'))   define('HOUR_IN_SECONDS',   3600);
if (!defined('DAY_IN_SECONDS'))    define('DAY_IN_SECONDS',    86400);


// ✅ Injecter le CSS une seule fois (avec if function_exists)
if (!function_exists('tp_print_basket_css_once')) {
function tp_print_basket_css_once(){
    static $printed = false;
    if ($printed) return;
    $printed = true;

    // CSS fortement scopé sous .tp-basket pour éviter les conflits de thème
    echo '<style>
/* ================== BASKET THEME (scopé) ================== */
.tp-basket{
  --brand:#ff6f00; --brand-2:#ffb300; --ink:#0f172a; --muted:#64748b; --line:#d7b98e;
  --court:#f8e4c9; --card:#fff; --upcoming:#1e88e5; --live:#e53935; --finished:#2e7d32;
  --fs-title:18px; --fs-team:16px; --fs-chip:12px; --fs-th:12px; --fs-td:13px; --pad-th:10px; --pad-td:10px;
}

.tp-basket .basket-title{
  font:700 var(--fs-title)/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
  margin:14px 0 8px; color:var(--ink);
}

.tp-basket .match{
  border:1px solid rgba(15,23,42,.08); padding:8px; margin:8px 0 12px;
  background:linear-gradient(180deg,#fff,#fff),
    url("data:image/svg+xml,%3Csvg width=\'8\' height=\'8\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Crect width=\'8\' height=\'8\' fill=\'%23f8e4c9\'/%3E%3Cpath d=\'M0 4 H8 M4 0 V8\' stroke=\'%23d7b98e\' stroke-width=\'0.75\'/%3E%3C/svg%3E");
  background-blend-mode:multiply; border-radius:10px; box-shadow:0 6px 14px rgba(15,23,42,.06);
}

.tp-basket .match__header{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  margin-bottom:6px;
}
.tp-basket .match__meta{
  font:600 13px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; color:#475569;
}

.tp-basket .teamline{
  display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:nowrap;
}
.tp-basket .team{
  display:flex; align-items:center; gap:8px; min-width:0;
}
.tp-basket .team__name{
  font:600 14px/1.15 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}

.tp-basket .team img{
  width:56px!important; height:56px!important; max-width:56px!important; max-height:56px!important;
  object-fit:contain; display:block;
}

.tp-basket .status{
  display:inline-block; padding:6px 10px; border-radius:999px; color:#fff;
  font:700 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
  letter-spacing:.3px; text-transform:uppercase;
}
.tp-basket .status--upcoming{
  background:linear-gradient(90deg,var(--upcoming),#42a5f5);
}
.tp-basket .status--live{
  background:linear-gradient(90deg,var(--live),#fb8c00); animation:tpb-pulse 1.2s infinite;
}
.tp-basket .status--finished{
  background:linear-gradient(90deg,var(--finished),#66bb6a);
}
.tp-basket .status--neutral{
  background:#94a3b8;
}
@keyframes tpb-pulse{
  0%{box-shadow:0 0 0 0 rgba(229,57,53,.6);}
  70%{box-shadow:0 0 0 10px rgba(229,57,53,0);}
  100%{box-shadow:0 0 0 0 rgba(229,57,53,0);}
}

.tpb-score{
  display:inline-flex; align-items:center; gap:6px; width:max-content; max-width:100%;
  margin:0 6px; padding:0; background:transparent!important; box-shadow:none!important;
}
.tpb-score__box{
  display:inline-block; min-width:24px; padding:2px 6px; border-radius:8px;
  background:#0f172a!important; color:#fff!important;
  text-align:center; font:700 13px/1.1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
}
.tpb-score__sep{
  padding:0 4px; color:#111; opacity:.6;
}

.tp-basket .card{
  background:var(--card); border-radius:14px; border:1px solid rgba(15,23,42,.08);
  box-shadow:0 8px 22px rgba(15,23,42,.08); padding:10px; overflow:auto;
}
.tp-basket .table{
  width:100%; border-collapse:separate; border-spacing:0;
  background:#fff; border-radius:12px; overflow:hidden;
}
.tp-basket .table thead th{
  position:sticky; top:0; z-index:1;
  background:linear-gradient(90deg,var(--brand),var(--brand-2)); color:#fff; text-align:center;
  font:800 var(--fs-th)/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
  letter-spacing:.4px; padding:var(--pad-th); border-bottom:3px solid #ffcf66;
}
.tp-basket .table tbody td{
  padding:var(--pad-td) var(--pad-td); text-align:center; vertical-align:middle;
  border-bottom:1px dashed rgba(15,23,42,.08);
  font:600 var(--fs-td)/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
  color:#0f172a;
  background:linear-gradient(#fff,#fff) padding-box,
             linear-gradient(90deg, rgba(215,185,142,.35), rgba(215,185,142,0), rgba(215,185,142,.35)) border-box;
  border-left:1px solid transparent; border-right:1px solid transparent;
}
.tp-basket .table tbody tr:nth-child(even) td{
  background:linear-gradient(#fafafa,#fafafa) padding-box,
             linear-gradient(90deg, rgba(215,185,142,.35), rgba(215,185,142,0), rgba(215,185,142,.35)) border-box;
}
.tp-basket .table tbody tr:hover td{
  background-color:#fff7ed;
}
.tp-basket .table td.team-name{
  color:#334155; font-weight:700;
}
.tp-basket .table td.num{
  font-variant-numeric:tabular-nums;
}
.tp-basket .table td.prp{
  background-image:linear-gradient(180deg,rgba(255,111,0,.08),rgba(255,179,0,.12));
  font-weight:800; border-right:none;
}

.tp-basket .info-row td{
  background:#fff9f0; border-bottom:none;
  padding:12px 14px!important; text-align:left!important;
}
.tp-basket .info-row--duos td{ background:#eaf6ff; }
.tp-basket .info-row--fun td{ background:#f3ffe7; }
.tp-basket .info-row--award td{ background:#f9f5ff; }
.tp-basket .duo-line{
  margin:6px 0; font-weight:700; color:#0f172a;
}

.tp-basket .chip{
  display:inline-block; padding:3px 8px; border-radius:999px;
  font:700 var(--fs-chip)/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
}
.tp-basket .chip--accent{
  background:linear-gradient(90deg,var(--brand),var(--brand-2)); color:#fff;
}
.tp-basket .chip--brand{
  background:#111827; color:#fff;
}
.tp-basket .chip--neutral{
  background:#e2e8f0; color:#0f172a;
}
.tp-basket .chip--muted{
  background:#fff3cd; color:#8a6d3b;
}
  .tp-basket .chip--ok{
    background:#16a34a;
    color:#fff;
  }
  .tp-basket .chip--ko{
    background:#b91c1c;
    color:#fff;
  }
.tp-basket .card.compact{ padding:8px; }
.tp-basket .table.compact thead th{ font-size:10px; padding:6px; }
.tp-basket .table.compact tbody td{ font-size:11px; padding:6px; }
.tp-basket .table.compact td.prp{ font-weight:700; }
.tp-basket .chip.compact{ font-size:11px; padding:2px 6px; }

@media (max-width:720px){
  .tp-basket{
    --fs-title:16px; --fs-team:14px; --fs-chip:11px;
    --fs-th:11px; --fs-td:12px; --pad-th:8px; --pad-td:8px;
  }
  .tp-basket .match{
    padding:6px; margin:6px 0 10px; border-radius:9px;
  }
  .tp-basket .match__meta{ font-size:12px; }
  .tp-basket .team img{
    width:44px!important; height:44px!important;
    max-width:44px!important; max-height:44px!important;
  }
  .tp-basket .team__name{ font-size:13px; }
  .tp-basket .tpb-score{ gap:6px; margin:0 4px; }
  .tp-basket .tpb-score__box{
    min-width:22px; padding:2px 6px; font-size:12px; border-radius:7px;
  }
  .tp-basket .tpb-score__sep{ padding:0 4px; }
}
@media (max-width:420px){
  .tp-basket{
    --fs-title:14px; --fs-team:13px; --fs-chip:10px;
    --fs-th:10px; --fs-td:11px; --pad-th:6px; --pad-td:6px;
  }
  .tp-basket .team img{
    width:32px!important; height:32px!important;
    max-width:32px!important; max-height:32px!important;
  }
  .tp-basket .team__name{ font-size:12px; }
  .tp-basket .tpb-score{ gap:4px; }
  .tp-basket .tpb-score__box{
    min-width:18px; padding:1px 5px; font-size:11px; border-radius:6px;
  }
  .tp-basket .tpb-score__sep{ padding:0 3px; }
}
  /* --- Filtres match / statut --- */
  .tp-basket .tpb-filters{
    display:flex; flex-wrap:wrap; gap:8px; align-items:center;
    margin:10px 0 8px;
    font:500 13px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
  }
  .tp-basket .tpb-filters label{
    display:flex; align-items:center; gap:6px;
  }
  .tp-basket .tpb-filters input,
  .tp-basket .tpb-filters select{
    padding:4px 8px;
    border-radius:999px;
    border:1px solid #cbd5f5;
    font-size:12px;
    background:#fff;
  }

  /* --- Wrapper match pour les filtres --- */
  .tp-basket .tpb-match-block{
    margin-bottom:18px;
  }

 /* --- Badge Match feu vert --- */
  .tp-basket .tpb-match-flag{
    display:inline-flex;
    align-items:center;
    gap:6px;
    margin:8px 0 6px;
    padding:6px 14px;
    border-radius:999px;
    font:700 12px/1.1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
    text-transform:uppercase;
    letter-spacing:.04em;

    /* look néon */
    background:radial-gradient(circle at 0 0,#22c55e 0,#064e3b 55%,#022c22 100%);
    color:#bbf7d0;
    border:1px solid #4ade80;
    box-shadow:
      0 0 4px rgba(34,197,94,.9),
      0 0 8px rgba(34,197,94,.8),
      0 0 16px rgba(34,197,94,.6);

    text-shadow:
      0 0 2px #bbf7d0,
      0 0 6px #22c55e;

    /* clignotement / pulse néon */
    animation:tpb-neon-pulse 1.35s ease-in-out infinite alternate;
  }

  @keyframes tpb-neon-pulse{
    0%{
      opacity:.7;
      box-shadow:
        0 0 2px rgba(34,197,94,.6),
        0 0 6px rgba(34,197,94,.5),
        0 0 10px rgba(34,197,94,.3);
      transform:translateY(0);
    }
    100%{
      opacity:1;
      box-shadow:
        0 0 6px rgba(34,197,94,1),
        0 0 14px rgba(34,197,94,.9),
        0 0 26px rgba(34,197,94,.8);
      transform:translateY(-1px);
    }
  }

  /* --- Chips de confiance --- */
  .tp-basket .chip--conf-low{ background:#e2e8f0; color:#0f172a; }
  .tp-basket .chip--conf-med{ background:#16a34a; color:#fff; }
  .tp-basket .chip--conf-high{
    background:linear-gradient(90deg,#8b5cf6,#f97316); color:#fff;
  }

  /* --- Top snipers --- */
  .tp-basket .tpb-toplists h5{
    margin:8px 0 4px;
    font:700 13px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
  }
  .tp-basket .tpb-toplist{
    margin:0 0 4px;
    padding-left:18px;
    font-size:12px;
  }
  .tp-basket .tpb-toplist li{ margin:2px 0; }
  .tp-basket .tpb-toplist-match{ opacity:.7; font-size:11px; margin-left:4px; }

  /* --- Collapse FUN + Cotes --- */
  .tp-basket .tpb-collapse{ margin:8px 0 12px; border-radius:12px; background:transparent; }
  .tp-basket .tpb-collapse > summary{
    cursor:pointer; list-style:none;
    display:flex; align-items:center; justify-content:space-between;
  }
  .tp-basket .tpb-collapse > summary::-webkit-details-marker{ display:none; }
  .tp-basket .tpb-collapse-arrow{
    font-size:12px; margin-left:8px;
    transition:transform .2s ease, opacity .2s ease; opacity:.7;
  }
  .tp-basket .tpb-collapse[open] .tpb-collapse-arrow{ transform:rotate(180deg); opacity:1; }

  /* --- Bouton Stats --- */
  .tp-basket .tpb-btn-stats{
    margin-left:8px;
    padding:4px 10px;
    border-radius:999px;
    border:1px solid rgba(15,23,42,.18);
    background:#fff;
    font:800 11px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
    cursor:pointer;
  }
  .tp-basket .tpb-btn-stats:hover{ background:#fff7ed; }

  /* --- Modal Stats --- */
  .tpb-modal-wrap{ position:fixed; inset:0; z-index:999999; display:none; }
  .tpb-modal-wrap.is-open{ display:block; }
  .tpb-modal-backdrop{ position:absolute; inset:0; background:rgba(15,23,42,.55); }
  .tpb-modal{
    position:relative;
    width:min(920px, calc(100vw - 24px));
    max-height:calc(100vh - 24px);
    margin:12px auto;
    background:#fff;
    border-radius:16px;
    border:1px solid rgba(15,23,42,.12);
    box-shadow:0 18px 50px rgba(15,23,42,.35);
    overflow:auto;
  }
  .tpb-modal-bar{
    display:flex; align-items:center; justify-content:space-between;
    padding:12px 14px;
    border-bottom:1px solid rgba(15,23,42,.08);
  }
  .tpb-modal-close{
    border:none; background:#0f172a; color:#fff;
    border-radius:999px; padding:6px 10px;
    font:800 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
    cursor:pointer;
  }
  .tpb-modal-body{ padding:14px; }
  .tpb-modal-loading{ padding:16px; font-weight:700; color:#475569; }

  .tpb-modal-head{ margin-bottom:10px; }
  .tpb-modal-title{ font:900 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; color:#0f172a; }
  .tpb-modal-sub{ margin-top:4px; font:600 12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; color:#64748b; }

  .tpb-stat-grid{ display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:10px; }
  @media (max-width:720px){ .tpb-stat-grid{ grid-template-columns:1fr; } }

  .tpb-stat-card{
    border:1px solid rgba(15,23,42,.08);
    border-radius:14px;
    padding:10px;
    background:linear-gradient(#fff,#fff);
  }
  .tpb-stat-title{ font:900 13px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; margin-bottom:6px; }
  .tpb-stat-kpis{ display:flex; flex-wrap:wrap; gap:6px; margin-bottom:8px; }
  .tpb-kpi{ background:#f1f5f9; padding:4px 8px; border-radius:999px; font:700 11px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; }
  .tpb-stat-values{ display:flex; flex-wrap:wrap; gap:6px; }
  .tpb-pill{ background:#111827; color:#fff; padding:4px 8px; border-radius:999px; font:800 11px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; }
  .tpb-stat-empty{ color:#94a3b8; font-weight:700; }
</style>';
}
}

// ✅ Print JS une seule fois (avec if function_exists)
if (!function_exists('tp_print_basket_js_once')) {
function tp_print_basket_js_once(){
    static $printed_js = false;
    if ($printed_js) return;
    $printed_js = true;

    // Fallbacks WP si besoin
    $ajaxUrl = function_exists('admin_url') ? admin_url('admin-ajax.php') : '/wp-admin/admin-ajax.php';
    $ajaxUrl = function_exists('esc_js') ? esc_js($ajaxUrl) : addslashes($ajaxUrl);

    echo "<script>
    (function(){
      function initBasketFilters(root){
        if (!root) return;

        var blocks = root.querySelectorAll('.tpb-match-block');
        if (!blocks.length) return;

        var input  = root.querySelector('.tpb-filter-teams');
        var select = root.querySelector('.tpb-filter-status');
        var leagueSelect = root.querySelector('.tpb-filter-league');

        function applyFilters(){
          var q  = input  ? input.value.toLowerCase().trim() : '';
          var st = select ? select.value : 'all';

          Array.prototype.forEach.call(blocks, function(block){
            var teams  = (block.getAttribute('data-teams')  || '').toLowerCase();
            var status = (block.getAttribute('data-status') || 'all');
            var ok     = true;

            if (q && teams.indexOf(q) === -1) ok = false;
            if (st !== 'all' && status !== st) ok = false;

            block.style.display = ok ? '' : 'none';
          });
        }

        if (input)  input.addEventListener('input',  applyFilters);
        if (select) select.addEventListener('change', applyFilters);

        // ✅ Filtre Ligue = reload (évite d'afficher/calculer toutes les ligues d'un coup)
        if (leagueSelect){
          leagueSelect.addEventListener('change', function(){
            var val = leagueSelect.value || '12';
            var opt = leagueSelect.options[leagueSelect.selectedIndex];
            var season = opt ? (opt.getAttribute('data-season') || '') : '';

            try{
              var url = new URL(window.location.href);
              url.searchParams.set('tpb_league', val);
              if (season) url.searchParams.set('tpb_season', season);
              window.location.href = url.toString();
            }catch(e){
              // fallback très vieux navigateurs
              window.location.search = 'tpb_league=' + encodeURIComponent(val) + (season ? '&tpb_season=' + encodeURIComponent(season) : '');
            }
          });
        }

        applyFilters();
      }

      function bootBasketFilters(){
        var roots = document.querySelectorAll('.tp-basket');
        if (!roots.length) return;
        Array.prototype.forEach.call(roots, function(root){
          initBasketFilters(root);
        });
      }

      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', bootBasketFilters);
      } else {
        bootBasketFilters();
      }

      // ---------- STATS MODAL ----------
      var ajaxUrl = \"{$ajaxUrl}\";

      function ensureStatsModal(){
        var el = document.getElementById('tpb-stats-modal');
        if (el) return el;

        el = document.createElement('div');
        el.id = 'tpb-stats-modal';
        el.className = 'tpb-modal-wrap';
        el.innerHTML = \"\" +
          \"<div class='tpb-modal-backdrop' data-close='1'></div>\" +
          \"<div class='tpb-modal'>\" +
            \"<div class='tpb-modal-bar'>\" +
              \"<div style='font-weight:900'>Dashboard — Stats joueur</div>\" +
              \"<button class='tpb-modal-close' type='button' data-close='1'>Fermer</button>\" +
            \"</div>\" +
            \"<div class='tpb-modal-body'><div class='tpb-modal-loading'>Chargement…</div></div>\" +
          \"</div>\";

        document.body.appendChild(el);

        el.addEventListener('click', function(e){
          var tgt = e.target;
          if (tgt && tgt.getAttribute && tgt.getAttribute('data-close') === '1') {
            closeStatsModal();
          }
        });

        document.addEventListener('keydown', function(e){
          if (e.key === 'Escape') closeStatsModal();
        });

        return el;
      }

      function openStatsModal(html){
        var el = ensureStatsModal();
        var body = el.querySelector('.tpb-modal-body');
        if (body) body.innerHTML = html || '';
        el.classList.add('is-open');
      }
      function closeStatsModal(){
        var el = document.getElementById('tpb-stats-modal');
        if (!el) return;
        el.classList.remove('is-open');
      }

      function fetchPlayerStats(player){
        // ✅ Hook si tu as déjà une modal dashboard globale
        if (window.TP_DASHBOARD && typeof window.TP_DASHBOARD.openPlayerModal === 'function') {
          window.TP_DASHBOARD.openPlayerModal(player);
          return;
        }
        if (typeof window.tp_open_player_modal === 'function') {
          window.tp_open_player_modal(player);
          return;
        }

        ensureStatsModal();
        openStatsModal(\"<div class='tpb-modal-loading'>Chargement…</div>\");

        var url = ajaxUrl + \"?action=tpb_player_stats&player=\" + encodeURIComponent(player);

        fetch(url, { credentials: 'same-origin' })
          .then(function(r){ return r.json(); })
          .then(function(data){
            if (data && data.success && data.data && data.data.html){
              openStatsModal(data.data.html);
            } else {
              var msg = (data && data.data && data.data.message) ? data.data.message : \"Erreur de chargement\";
              openStatsModal(\"<div class='tpb-modal-loading'>❌ \" + msg + \"</div>\");
            }
          })
          .catch(function(){
            openStatsModal(\"<div class='tpb-modal-loading'>❌ Erreur réseau</div>\");
          });
      }

      // Event delegation: bouton \"Stats\"
      document.addEventListener('click', function(e){
        var btn = e.target && e.target.closest ? e.target.closest('.tpb-btn-stats') : null;
        if (!btn) return;
        var player = btn.getAttribute('data-player') || '';
        if (!player) return;
        fetchPlayerStats(player);
      });

    })();
    </script>";
}
}

// ✅ Dump debug (avec if function_exists)
if (!function_exists('tp_dump')) {
/** Petit dump lisible dans le HTML (replié) */
function tp_dump($label, $value, $open = false){
    if (!defined('TP_DEBUG_ODDS') || !TP_DEBUG_ODDS) return;

    // wp_json_encode peut ne pas exister hors WP
    $json = is_string($value)
        ? $value
        : (function_exists('wp_json_encode')
            ? wp_json_encode($value, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)
            : json_encode($value, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)
          );

    $openAttr = $open ? ' open' : '';

    $safeLabel = function_exists('esc_html') ? esc_html($label) : htmlspecialchars((string)$label, ENT_QUOTES, 'UTF-8');
    $safeJson  = function_exists('esc_html') ? esc_html($json)  : htmlspecialchars((string)$json,  ENT_QUOTES, 'UTF-8');

    echo "<details class='tp-debug'{$openAttr}><summary>{$safeLabel}</summary><pre style='white-space:pre-wrap;margin:8px 0 0;'>"
       . $safeJson
       . "</pre></details>";
}
}

/** =========================================================================
 *  HELPERS GÉNÉRAUX (avec if function_exists)
 * ========================================================================= */

// Polyfill PHP8 str_contains (ton polyfill est ok, on le garde + safe mb_strpos fallback)
if (!function_exists('str_contains')) {
    function str_contains($haystack, $needle){
        $haystack = (string)$haystack;
        $needle   = (string)$needle;
        if ($needle === '') return true;

        // mb_strpos si dispo sinon strpos
        if (function_exists('mb_strpos')) {
            return mb_strpos($haystack, $needle) !== false;
        }
        return strpos($haystack, $needle) !== false;
    }
}

if (!function_exists('tp_is_not_started')) {
function tp_is_not_started($status) {
    $short = function_exists('strtoupper') ? strtoupper($status['short'] ?? '') : ($status['short'] ?? '');
    $long  = function_exists('strtolower') ? strtolower($status['long']  ?? '') : ($status['long'] ?? '');

    return in_array($short, ['NS', 'SCH'], true)
        || str_contains($long, 'scheduled')
        || str_contains($long, 'not started');
}
}

if (!function_exists('tp_is_finished')) {
  function tp_is_finished($status) {
    $short = function_exists('strtoupper') ? strtoupper($status['short'] ?? '') : ($status['short'] ?? '');
    $long  = function_exists('strtolower') ? strtolower($status['long']  ?? '') : ($status['long'] ?? '');

    if (in_array($short, ['FT','AOT','F'], true)) return true;

    if (str_contains($long, 'finished') || str_contains($long, 'ended')) return true;
    if (str_contains($long, 'after ot')) return true;

    return false;
  }
}


if (!function_exists('tp_minutes_to_decimal')) {
function tp_minutes_to_decimal($minutes_str) {
    $minutes_str = (string)($minutes_str ?? '');
    if ($minutes_str === '' || strpos($minutes_str, ':') === false) return 0.0;

    $parts = explode(':', $minutes_str);
    $m = isset($parts[0]) ? (int)$parts[0] : 0;
    $s = isset($parts[1]) ? (int)$parts[1] : 0;

    return $m + ($s / 60);
}
}

if (!function_exists('tp_extract_final_scores')) {
  function tp_extract_final_scores($scores) {
    if (!$scores || !is_array($scores)) return [null, null];

    $h = $scores['home'] ?? [];
    $a = $scores['away'] ?? [];

    $ht = intval($h['total'] ?? 0);
    $at = intval($a['total'] ?? 0);

    if ($ht === 0 && $at === 0) {
      $sum = function($side){
        $side = is_array($side) ? $side : [];
        return intval($side['quarter_1'] ?? 0)
          + intval($side['quarter_2'] ?? 0)
          + intval($side['quarter_3'] ?? 0)
          + intval($side['quarter_4'] ?? 0)
          + intval($side['over_time'] ?? 0);
      };
      $ht = $sum($h);
      $at = $sum($a);
    }

    return [$ht ?: null, $at ?: null];
  }
}

if (!function_exists('tp_status_badge')) {
  function tp_status_badge(array $status): string {
    $short   = function_exists('strtoupper') ? strtoupper($status['short'] ?? '') : ($status['short'] ?? '');
    $longRaw = (string)($status['long'] ?? $short);
    $long    = function_exists('strtolower') ? strtolower($longRaw) : $longRaw;
    $timer   = $status['timer'] ?? '';

    if (function_exists('tp_is_not_started') && tp_is_not_started($status)) {
      return "<span class='status status--upcoming'>À venir</span>";
    }

    if (function_exists('tp_is_finished') && tp_is_finished($status)) {
      $label = 'Terminé';
      if ($short === 'AOT' || str_contains($long, 'overtime') || str_contains($long, 'after ot')) {
        $label = 'Terminé (OT)';
      }
      return function_exists('esc_html')
        ? "<span class='status status--finished'>".esc_html($label)."</span>"
        : "<span class='status status--finished'>".$label."</span>";
    }

    $label = 'LIVE';
    if ($timer) $label .= ' · '.$timer;

    return function_exists('esc_html')
      ? "<span class='status status--live'>".esc_html($label)."</span>"
      : "<span class='status status--live'>".$label."</span>";
  }
}

/* ================= Helpers Milestones (Bet365) ================= */

if (!function_exists('tp_norm')) {
  function tp_norm($s){
    $str = (string)($s ?? '');
    $str = str_replace('.', '', $str);
    $str = trim($str);
    return strtolower($str);
  }
}

if (!function_exists('tp_parse_milestone_row')) {
  function tp_parse_milestone_row($selection){
    if (!function_exists('preg_match')) return null;
    if (!preg_match('/^(.+?)\s*-\s*(-?\d+(?:\.\d+)?)$/u', (string)$selection, $m)) return null;
    return ['name'=>trim($m[1]), 'line'=>(float)$m[2]];
  }
}

if (!function_exists('tp_generic_milestones_index')) {
  function tp_generic_milestones_index(array $odds, string $kind){
    if (!function_exists('tp_parse_milestone_row')) return [];
    if (!function_exists('tp_norm')) return [];

    $needle = 'player '.$kind.' milestones';
    $out = [];

    foreach ($odds as $r){
      if (!is_array($r)) continue;
      if (($r['bookmaker_id'] ?? null) != 4) continue;

      $bet = strtolower((string)($r['bet'] ?? ''));
      if ($bet === '' || strpos($bet, $needle) === false) continue;

      $parsed = tp_parse_milestone_row($r['selection'] ?? '');
      if (!$parsed) continue;

      if (!isset($r['odd']) || !is_numeric($r['odd'])) continue;

      $name  = tp_norm($parsed['name']);
      $line  = (float)$parsed['line'];
      $odd   = (float)$r['odd'];

      $parts = preg_split('/\s+/', $name);
      if (!is_array($parts) || empty($parts)) continue;

      $first = $parts[0] ?? '';
      $last  = end($parts) ?: '';

      $k1 = ($first !== '' && $last !== '') ? substr($first,0,1).' '.$last : '';
      $k2 = ($last !== '') ? $last : '';

      if ($k1 !== '') $out[$k1][$line] = max($out[$k1][$line] ?? 0, $odd);
      if ($k2 !== '') $out[$k2][$line] = max($out[$k2][$line] ?? 0, $odd);
    }

    foreach ($out as &$g){
      if (is_array($g)) ksort($g, SORT_NUMERIC);
    }
    unset($g);

    return $out;
  }
}

if (!function_exists('tp_build_milestones_indexes')) {
function tp_build_milestones_indexes(array $odds){
    if (!function_exists('tp_generic_milestones_index')) return [
        'points'=>[], 'rebounds'=>[], 'assists'=>[]
    ];

    return [
        'points'   => tp_generic_milestones_index($odds,'points'),
        'rebounds' => tp_generic_milestones_index($odds,'rebounds'),
        'assists'  => tp_generic_milestones_index($odds,'assists'),
    ];
}
}

if (!function_exists('tp_find_milestone_odd')) {
function tp_find_milestone_odd(array $index, string $playerLabel, float $threshold){
    if (!function_exists('tp_norm')) return null;

    $name  = tp_norm($playerLabel);
    $parts = preg_split('/\s+/', $name);
    if (!is_array($parts) || empty($parts)) return null;

    $first = $parts[0] ?? '';
    $last  = $parts[count($parts)-1] ?? '';

    $keys = [];
    if ($first !== '' && $last !== ''){
        $keys[] = substr($first,0,1).' '.$last;
        $keys[] = substr($last,0,1).' '.$first; // (au cas où)
        $keys[] = $last;
        $keys[] = $first;
    } elseif ($last !== '') {
        $keys[] = $last;
    }

    $grids = [];
    foreach (array_unique(array_filter($keys)) as $k){
        if (!empty($index[$k]) && is_array($index[$k])) $grids[] = $index[$k];
    }
    if (!$grids) return null;

    $need     = max(0, (int)ceil($threshold));
    $bestLine = null;
    $bestOdd  = null;

    foreach ($grids as $g){
        foreach ($g as $line => $odd){
            if (!is_numeric($line) || !is_numeric($odd)) continue;

            $line = (float)$line;
            $odd  = (float)$odd;

            if ($line < $need) continue;

            if (
                $bestLine === null
                || $line < $bestLine
                || ($line == $bestLine && ($bestOdd === null || $odd > $bestOdd))
            ){
                $bestLine = $line;
                $bestOdd  = $odd;
            }
        }
    }

    return ($bestLine === null) ? null : ['line'=>$bestLine,'odd'=>$bestOdd];
}
}


/* ===== Helpers odds génériques (fallback + 3pts) ===== */

if (!function_exists('tp_market_key')) {
function tp_market_key(?string $bet): string {
    $b = strtolower(trim((string)($bet ?? '')));
    if ($b === '') return '';

    if (strpos($b, 'player points')   !== false) return 'points';
    if (strpos($b, 'player rebounds') !== false) return 'rebounds';
    if (strpos($b, 'player assists')  !== false) return 'assists';

    // 3pts (large)
    if (strpos($b, '3') !== false || strpos($b, 'three') !== false) return 'threes';

    return '';
}
}

if (!function_exists('tp_is_over')) {
function tp_is_over(?string $selection): bool {
    $s = strtolower(trim((string)($selection ?? '')));
    if ($s === '') return false;

    // "o" isolé type "o 2.5" (bookmakers)
    $has_o = function_exists('preg_match') ? (bool)preg_match('/\bo\b/', $s) : false;

    return (
        strpos($s, 'over') !== false
        || strpos($s, 'plus') !== false
        || $has_o
    );
}
}

if (!function_exists('tp_extract_line_from_row')) {
function tp_extract_line_from_row(array $row): ?float {
    if (isset($row['line']) && is_numeric($row['line'])) return (float)$row['line'];
    if (isset($row['handicap']) && is_numeric($row['handicap'])) return (float)$row['handicap'];

    $sel = (string)($row['selection'] ?? '');
    if ($sel !== '' && function_exists('preg_match')) {
        if (preg_match('/-?\d+(?:\.\d+)?/', $sel, $m)) return (float)$m[0];
    }
    return null;
}
}

if (!function_exists('tp_index_odds')) {
function tp_index_odds(array $rows): array {
    $idx = [];

    // dépendances
    if (!function_exists('tp_market_key')) return $idx;
    if (!function_exists('tp_extract_line_from_row')) return $idx;
    if (!function_exists('tp_is_over')) return $idx;

    foreach ($rows as $r){
        if (!is_array($r)) continue;

        $mk = tp_market_key($r['bet'] ?? '');
        if (!$mk) continue;

        $odd = null;
        if (isset($r['odd']) && is_numeric($r['odd'])) $odd = (float)$r['odd'];

        $idx[$mk][] = [
            'selection' => (string)($r['selection'] ?? ''),
            'bookmaker' => (string)($r['bookmaker'] ?? ''),
            'odd'       => $odd,
            'line'      => tp_extract_line_from_row($r),
            'is_over'   => tp_is_over($r['selection'] ?? ''),
        ];
    }

    return $idx;
}
}

if (!function_exists('tp_row_matches_player')) {
function tp_row_matches_player(array $row, string $player): bool {
    $sel = strtolower((string)($row['selection'] ?? ''));
    $player = trim((string)$player);
    if ($sel === '' || $player === '') return false;

    $vars = [
        strtolower($player),
        strtolower(str_replace('.', '', $player)),
    ];

    // last name
    $parts = preg_split('/\s+/', strtolower($player));
    if (is_array($parts) && !empty($parts)) {
        $vars[] = end($parts);
        // "J. Brown" pattern -> on ajoute "j brown" et "j. brown" simplifiés
        $vars[] = substr($parts[0], 0, 1) . ' ' . end($parts);
    }

    foreach (array_unique(array_filter($vars)) as $v) {
        if ($v !== '' && strpos($sel, $v) !== false) return true;
    }

    // fallback : regex initiale "J. Brown" si dispo
    if (function_exists('preg_quote') && function_exists('preg_match') && is_array($parts) && count($parts) >= 2) {
        $first = $parts[0] ?? '';
        $last  = end($parts) ?: '';
        if ($first !== '' && $last !== '') {
            $pat = '/\b' . preg_quote(substr($first, 0, 1), '/') . '\.\s*' . preg_quote($last, '/') . '\b/i';
            if (preg_match($pat, $sel)) return true;
        }
    }

    return false;
}
}

if (!function_exists('tp_find_over_odd_for')) {
function tp_find_over_odd_for(array $idx, string $market, string $player, float $threshold): ?array {
    if (empty($idx) || $market === '' || $player === '') return null;
    if (empty($idx[$market]) || !is_array($idx[$market])) return null;

    // dépendance : matching joueur
    if (!function_exists('tp_row_matches_player')) return null;

    $best = null;

    foreach ($idx[$market] as $row){
        if (!is_array($row)) continue;

        // sécurité champs
        $is_over = (bool)($row['is_over'] ?? false);
        if (!$is_over) continue;

        if (!tp_row_matches_player($row, $player)) continue;

        $line = $row['line'] ?? null;
        if ($line === null || !is_numeric($line)) continue;
        $line = (float)$line;

        $odd = isset($row['odd']) && is_numeric($row['odd']) ? (float)$row['odd'] : 0.0;

        $score = [abs($line - $threshold), -$odd];

        if ($best === null || $score < $best['score']) {
            $best = [
                'odd'      => $odd ?: null,
                'line'     => $line,
                'bookmaker'=> $row['bookmaker'] ?? null,
                'score'    => $score,
            ];
        }
    }

    return $best;
}
}

if (!function_exists('tp_best_odd_for')) {
function tp_best_odd_for(array $idxMil, array $idxGen, string $market, string $player, float $threshold): ?array {
    if ($market === '' || $player === '') return null;

    // 1) Milestones (si dispo)
    if (in_array($market, ['points','rebounds','assists'], true)) {
        if (function_exists('tp_find_milestone_odd')) {
            $milBucket = $idxMil[$market] ?? [];
            if (is_array($milBucket)) {
                $m = tp_find_milestone_odd($milBucket, $player, $threshold);
                if (is_array($m) && isset($m['line'], $m['odd'])) {
                    return [
                        'line' => is_numeric($m['line']) ? (float)$m['line'] : $m['line'],
                        'odd'  => is_numeric($m['odd'])  ? (float)$m['odd']  : $m['odd'],
                    ];
                }
            }
        }
    }

    // 2) Fallback sur index générique (over)
    if (!function_exists('tp_find_over_odd_for')) return null;

    $g = tp_find_over_odd_for($idxGen, $market, $player, $threshold);
    return (is_array($g) && isset($g['line'], $g['odd']))
        ? ['line' => $g['line'], 'odd' => $g['odd']]
        : null;
}
}

/* ===== Snapshot pré-match (cache local WP) ===== */

if (!function_exists('tp_pre_cache_get')) {
function tp_pre_cache_get($game_id){
    if (!$game_id) return null;

    $key = 'tp_pre_'.$game_id;
    $val = false;

    // 1) transient si possible
    if (function_exists('get_transient')) {
        $val = get_transient($key);
    }

    // 2) fallback option si transient vide
    if ($val === false && function_exists('get_option')) {
        $val = get_option($key);
    }

    if (!is_array($val)) return null;

    // purge si expiré (garde-fou)
    if (!empty($val['_expires_at']) && function_exists('time') && time() > (int)$val['_expires_at']) {
        if (function_exists('delete_transient')) delete_transient($key);
        if (function_exists('delete_option'))    delete_option($key);
        return null;
    }

    return $val;
}
}

if (!function_exists('tp_pre_cache_set')) {
function tp_pre_cache_set($game_id, array $data, $ttl = null){
    if (!$game_id) return false;

    // fallback TTL si constante WP absente
    if ($ttl === null) {
        $ttl = defined('DAY_IN_SECONDS') ? (1 * DAY_IN_SECONDS) : 86400;
    }

    $key = 'tp_pre_'.$game_id;

    // marqueur d’expiration
    $data['_expires_at'] = time() + (int)$ttl;

    // transient si possible
    if (function_exists('set_transient')) {
        set_transient($key, $data, (int)$ttl);
    }

    // Option WP persistée avec autoload=NO (si possible)
    if (function_exists('get_option') && function_exists('add_option') && function_exists('update_option')) {
        if (false === get_option($key, false)) {
            // add_option($option, $value, $deprecated, $autoload)
            add_option($key, $data, '', 'no');
        } else {
            update_option($key, $data);
        }
    }

    return true;
}
}


/** =========================================================================
 *  API HELPERS + CACHES
 * ========================================================================= */

if (!function_exists('tp_http_json')) {
function tp_http_json($url, array $args = [], $cache_key = '', $ttl = 0){

    // ✅ garde-fous WP
    if (!function_exists('wp_parse_args') || !function_exists('wp_remote_get')) {
        return null;
    }

    // ✅ cache (transients) si dispo
    if ($cache_key && $ttl > 0 && function_exists('get_transient')) {
        $cached = get_transient($cache_key);
        if ($cached !== false) return $cached;
    }

    // ✅ headers (clé API) avec fallback propre
    $apiKey = defined('TP_APISPORTS_KEY') ? TP_APISPORTS_KEY : (defined('LIVE2_APISPORTS_KEY') ? LIVE2_APISPORTS_KEY : '');
    $headers = [
        'x-apisports-key' => $apiKey,
        'x-rapidapi-host' => 'v1.basketball.api-sports.io',
    ];

    // si l'user a déjà mis des headers, on fusionne
    $args = wp_parse_args($args, [
        'headers' => [],
        'timeout' => 15,
    ]);
    if (!is_array($args['headers'])) $args['headers'] = [];
    $args['headers'] = array_merge($headers, $args['headers']);

    // ✅ call HTTP
    $res = wp_remote_get($url, $args);
    if (function_exists('is_wp_error') && is_wp_error($res)) return null;

    if (!function_exists('wp_remote_retrieve_body')) return null;
    $body = wp_remote_retrieve_body($res);
    $data = json_decode($body, true);

    // ✅ cache write
    if ($cache_key && $ttl > 0 && function_exists('set_transient')) {
        set_transient($cache_key, $data, $ttl);
    }

    return $data;
}}


/* =========================================================================
 *  HISTORIQUE + % DE CONFIANCE (lien avec le Dashboard)
 * ========================================================================= */

/**
 * Normalisation des noms de joueurs : doit être IDENTIQUE Dashboard / Spot.
 */
if (!function_exists('tp_nba_norm_name')) {
    function tp_nba_norm_name($name){
        $name = strtolower(trim($name));
        $name = str_replace('.', '', $name);
        return $name;
    }
}

/**
 * Récupère la map globale des historiques joueurs (Dashboard → transient).
 * Format attendu :
 *  $hist['cunningham cade']['PTS'] = [27, 31, 22, ...]
 *  $hist['cunningham cade']['REB'] = [...]
 *  $hist['cunningham cade']['AST'] = [...]
 *  $hist['cunningham cade']['3PM'] = [...]
 */
if (!function_exists('tp_nba_get_hist_global')) {
  function tp_nba_get_hist_global(){
    $key  = 'tp_hist_global_cache_2h';
    $hist = get_transient($key);
    if (is_array($hist) && $hist !== []) return $hist;

    // Dépendances indispensables
    $deps_ok =
      function_exists('tpnbap_get_matches_next_days') &&
      function_exists('tp_build_player_hist_map') &&
      function_exists('tp_merge_hist_maps');

    if (!$deps_ok) {
      // On évite de re-tenter à chaque clic si le core n'est pas chargé
      set_transient($key, [], 10 * MINUTE_IN_SECONDS);
      return [];
    }

    $matches = tpnbap_get_matches_next_days(TP_BASK_LEAGUE, TP_BASK_SEASON);
    $hist    = [];

    if (!empty($matches)) {
      foreach ($matches as $m){
        $home_id = (int)($m['teams']['home']['id'] ?? 0);
        $away_id = (int)($m['teams']['away']['id'] ?? 0);
        $ts      = (int)($m['ts'] ?? 0);
        if (!$ts && !empty($m['date']['timestamp'])) $ts = (int)$m['date']['timestamp'];

        if ($home_id) {
          $hist = tp_merge_hist_maps($hist, tp_build_player_hist_map($home_id, TP_BASK_LEAGUE, TP_BASK_SEASON, $ts));
        }
        if ($away_id) {
          $hist = tp_merge_hist_maps($hist, tp_build_player_hist_map($away_id, TP_BASK_LEAGUE, TP_BASK_SEASON, $ts));
        }
      }
    }

    // Petit meta debug (ne gêne pas ton foreach, tu peux ignorer)
    $hist['_BUILT_AT'] = time();

    set_transient($key, $hist, 3 * HOUR_IN_SECONDS);
    return $hist;
  }
}


/**
 * Calcule un % de réussite IDENTIQUE au Dashboard :
 * - même historique $hist
 * - même logique : % de matchs où v > seuil (sans pénalisation)
 */
if (!function_exists('tp_nba_confidence_for')) {
    function tp_nba_confidence_for($player_name, $market, $threshold){
        $hist = tp_nba_get_hist_global();
        if (empty($hist) || !is_array($hist)) {
            return null;
        }

        // 🔧 normalisation identique Dashboard
        $key = tp_nba_norm_name($player_name);
        $candidates = [];

        if ($key !== '') {
            $candidates[] = $key;
        }

        // Variante "nom prénom" / "prénom nom"
        $parts = preg_split('/\s+/', $key);
        if (count($parts) >= 2) {
            $rev = trim(implode(' ', array_reverse($parts)));
            if ($rev !== '' && $rev !== $key) {
                $candidates[] = $rev;
            }
        }

        // Recherche directe dans $hist
        $foundKey = null;
        foreach ($candidates as $k) {
            if (!empty($hist[$k][$market]) && is_array($hist[$k][$market])) {
                $foundKey = $k;
                break;
            }
        }

        // Fallback : on scanne tout l'historique pour prénom+nom présents
        if (!$foundKey && count($parts) >= 2) {
            $first = $parts[0];
            $last  = $parts[count($parts) - 1];

            foreach ($hist as $k => $perPlayer) {
                if (empty($perPlayer[$market]) || !is_array($perPlayer[$market])) continue;

                $kParts = preg_split('/\s+/', (string)$k);
                if (in_array($first, $kParts, true) && in_array($last, $kParts, true)) {
                    $foundKey = $k;
                    break;
                }
            }
        }

        if (!$foundKey) {
            return null; // pas trouvé dans le dashboard
        }

        $vals = $hist[$foundKey][$market];
        $n    = count($vals);
        if ($n < 3) { // même seuil minimal que la grille du dashboard
            return null;
        }

        // ⚠️ même condition que le dashboard : STRICTEMENT ">"
        $hits = 0;
        foreach ($vals as $v){
            if ($v > $threshold) {
                $hits++;
            }
        }

        // % brut, arrondi comme sur la carte
        return (int) round(100 * $hits / $n);
    }
}
if (!function_exists('tp_nba_conf_chip')) {
    function tp_nba_conf_chip($conf){
        if ($conf === null) return '';
        $cls = 'chip--conf-low';
        if ($conf >= 85) $cls = 'chip--conf-med';
        if ($conf >= 93) $cls = 'chip--conf-high';
        return " <span class=\"chip {$cls}\">".$conf."%</span>";
    }
}
/* ============================
   STATS BUTTON + MODAL (Dashboard cache)
   ============================ */

if (!function_exists('tpb_player_stats_button')) {
  function tpb_player_stats_button(string $player_name): string {
    $p = trim($player_name);
    if ($p === '') return '';
    return "<button type='button' class='tpb-btn-stats' data-player='".esc_attr($p)."'>Stats</button>";
  }
}

if (!function_exists('tpb_find_hist_key_for_player')) {
  function tpb_find_hist_key_for_player(string $player_name, array $hist): ?string {
    $key = tp_nba_norm_name($player_name);
    if ($key === '' || empty($hist)) return null;

    $candidates = [$key];

    // prénom nom <-> nom prénom
    $parts = preg_split('/\s+/', $key);
    if (count($parts) >= 2) {
      $rev = trim(implode(' ', array_reverse($parts)));
      if ($rev !== '' && $rev !== $key) $candidates[] = $rev;
    }

    foreach ($candidates as $k) {
      if (isset($hist[$k]) && is_array($hist[$k])) return $k;
    }

    // fallback scan : first+last présents
    if (count($parts) >= 2) {
      $first = $parts[0];
      $last  = $parts[count($parts)-1];
      foreach ($hist as $k => $perPlayer) {
        if (!is_array($perPlayer)) continue;
        $kParts = preg_split('/\s+/', (string)$k);
        if (in_array($first, $kParts, true) && in_array($last, $kParts, true)) {
          return (string)$k;
        }
      }
    }

    return null;
  }
}

/** AJAX: renvoie HTML de la modal Stats */
add_action('wp_ajax_tpb_player_stats', 'tpb_player_stats_ajax');
add_action('wp_ajax_nopriv_tpb_player_stats', 'tpb_player_stats_ajax');

if (!function_exists('tpb_player_stats_ajax')) {
  function tpb_player_stats_ajax(){
    $player = isset($_GET['player']) ? sanitize_text_field((string)$_GET['player']) : '';
    if ($player === '') wp_send_json_error(['message'=>'player manquant']);

    $hist = tp_nba_get_hist_global();
    if (empty($hist) || !is_array($hist)) {
      wp_send_json_error(['message'=>"Cache Dashboard introuvable (tp_hist_global_cache_2h)"]);
    }

    $foundKey = tpb_find_hist_key_for_player($player, $hist);
    if (!$foundKey || empty($hist[$foundKey]) || !is_array($hist[$foundKey])) {
      wp_send_json_error(['message'=>"Joueur non trouvé dans le cache Dashboard"]);
    }

    $per = $hist[$foundKey];

    $markets = [
      'PTS' => 'Points',
      'REB' => 'Rebonds',
      'AST' => 'Passes',
      '3PM' => '3 pts',
    ];

    $html  = "<div class='tpb-modal-head'>";
    $html .= "<div class='tpb-modal-title'>📊 Stats — ".esc_html($player)."</div>";
    $html .= "<div class='tpb-modal-sub'>Source : cache Dashboard · clé = <code>".esc_html($foundKey)."</code></div>";
    $html .= "</div>";

    $html .= "<div class='tpb-stat-grid'>";

    foreach ($markets as $mk => $label){
      $vals = [];
      if (!empty($per[$mk]) && is_array($per[$mk])) {
        $vals = array_values($per[$mk]);
      }
      $vals = array_slice($vals, 0, 10); // L10 max

      if (empty($vals)) {
        $html .= "<div class='tpb-stat-card'>
                    <div class='tpb-stat-title'>".esc_html($label)."</div>
                    <div class='tpb-stat-empty'>Aucune donnée</div>
                  </div>";
        continue;
      }

      $n   = count($vals);
      $sum = 0;
      $min = INF;
      $max = -INF;
      foreach ($vals as $v){
        $v = (float)$v;
        $sum += $v;
        if ($v < $min) $min = $v;
        if ($v > $max) $max = $v;
      }
      $avg = round($sum / max(1,$n), 1);

      $html .= "<div class='tpb-stat-card'>
                  <div class='tpb-stat-title'>".esc_html($label)."</div>
                  <div class='tpb-stat-kpis'>
                    <span class='tpb-kpi'>L{$n} avg <b>".esc_html($avg)."</b></span>
                    <span class='tpb-kpi'>min <b>".esc_html($min)."</b></span>
                    <span class='tpb-kpi'>max <b>".esc_html($max)."</b></span>
                  </div>
                  <div class='tpb-stat-values'>";

      foreach ($vals as $v){
        $html .= "<span class='tpb-pill'>".esc_html($v)."</span>";
      }

      $html .=     "</div>
                </div>";
    }

    $html .= "</div>";

    wp_send_json_success(['html'=>$html]);
  }
}

if (!function_exists('tp_nba_duo_confidence_pts')) {
    /**
     * % de validation d'un DUO sur les points :
     * - On prend les séries PTS des 2 joueurs dans $hist global (même logique que tp_nba_confidence_for)
     * - On combine index par index (last 6 games) et on compte les fois où P1+P2 > seuil
     */
    function tp_nba_duo_confidence_pts($player1, $player2, $threshold){
        $hist = tp_nba_get_hist_global();
        if (empty($hist) || !is_array($hist)) {
            return null;
        }

        // Sous-helper : récupère le tableau PTS pour un joueur
        $get_pts = function($player_name) use ($hist){
            $key = tp_nba_norm_name($player_name);
            $candidates = [];

            if ($key !== '') {
                $candidates[] = $key;
            }

            $parts = preg_split('/\s+/', $key);
            if (count($parts) >= 2) {
                $rev = trim(implode(' ', array_reverse($parts)));
                if ($rev !== '' && $rev !== $key) {
                    $candidates[] = $rev;
                }
            }

            $foundKey = null;
            foreach ($candidates as $k) {
                if (!empty($hist[$k]['PTS']) && is_array($hist[$k]['PTS'])) {
                    $foundKey = $k;
                    break;
                }
            }

            if (!$foundKey && count($parts) >= 2) {
                $first = $parts[0];
                $last  = $parts[count($parts) - 1];

                foreach ($hist as $k => $perPlayer) {
                    if (empty($perPlayer['PTS']) || !is_array($perPlayer['PTS'])) continue;
                    $kParts = preg_split('/\s+/', (string)$k);
                    if (in_array($first, $kParts, true) && in_array($last, $kParts, true)) {
                        $foundKey = $k;
                        break;
                    }
                }
            }

            if (!$foundKey) {
                return [];
            }

            return array_values($hist[$foundKey]['PTS']);
        };

        $p1 = $get_pts($player1);
        $p2 = $get_pts($player2);

        if (empty($p1) || empty($p2)) {
            return null;
        }

        // On travaille sur les 6 derniers matchs max
        $n = min(count($p1), count($p2), 6);
        if ($n < 1) {
            return null;
        }

        $hits = 0;
        for ($i = 0; $i < $n; $i++) {
            // même logique que le dashboard : STRICTEMENT ">"
            if (($p1[$i] + $p2[$i]) > $threshold) {
                $hits++;
            }
        }

        return (int) round(100 * $hits / $n);
    }
}
if (!function_exists('tpb_get_time_window_for_league')) {
function tpb_get_time_window_for_league(string $league_id, DateTimeZone $tzParis): array {

    // ✅ garde-fous
    if (!class_exists('DateTime') || !class_exists('DateTimeZone')) {
        return [null, null];
    }

    // ✅ si $tzParis n'est pas exploitable → fallback Europe/Paris
    if (!($tzParis instanceof DateTimeZone)) {
        try { $tzParis = new DateTimeZone('Europe/Paris'); }
        catch (Exception $e) { return [null, null]; }
    }

    try {
        switch ($league_id) {

            // 12 NBA : hier 18h -> demain 8h
            case '12':
                $startLocal  = new DateTime('yesterday 18:00', $tzParis);
                $cutoffLocal = new DateTime('tomorrow 08:00',  $tzParis);
                break;

            // 13 WNBA : hier 18h -> demain 8h
            case '13':
                $startLocal  = new DateTime('yesterday 18:00', $tzParis);
                $cutoffLocal = new DateTime('tomorrow 08:00',  $tzParis);
                break;

            // 1 NBL : aujourd'hui (00:00) -> demain 10h
            case '1':
                $startLocal  = new DateTime('today 00:00',     $tzParis);
                $cutoffLocal = new DateTime('tomorrow 10:00',  $tzParis);
                break;

            // 120 EuroLeague : hier (00:00) -> aujourd'hui (23:59)
            case '120':
                $startLocal  = new DateTime('yesterday 00:00', $tzParis);
                $cutoffLocal = new DateTime('today 23:59',     $tzParis);
                break;

            // fallback : fenêtre rolling si autre ligue
            default:
                $startLocal  = (new DateTime('now', $tzParis))->modify('-6 hours');
                $cutoffLocal = (new DateTime('now', $tzParis))->modify('+48 hours');
                break;
        }
    } catch (Exception $e) {
        // ✅ si DateTime throw (rare) → fallback safe
        $startLocal  = (new DateTime('now', $tzParis))->modify('-6 hours');
        $cutoffLocal = (new DateTime('now', $tzParis))->modify('+48 hours');
    }

    return [$startLocal, $cutoffLocal];
}}


if (!function_exists('get_nba_matches_on_next_2_days')) {
/** =========================================================================
 *  API CALLS (MATCHES + STATS)
 * ========================================================================= */
function get_nba_matches_on_next_2_days($league_id = '12', $season = '2025-2026') {
    $matches = [];

    // ✅ dépendances indispensables
    if (!class_exists('DateTime') || !class_exists('DateTimeZone')) return [];
    if (!function_exists('add_query_arg') || !function_exists('tp_http_json')) return [];

    // ✅ fenêtre horaire : si absente → fallback simple (maintenant → +48h en heure de Paris)
    $tzParis = new DateTimeZone('Europe/Paris');

    if (function_exists('tpb_get_time_window_for_league')) {
        [$startLocal, $cutoffLocal] = tpb_get_time_window_for_league((string)$league_id, $tzParis);

        // sécurité : si la fonction renvoie n'importe quoi
        if (!($startLocal instanceof DateTime) || !($cutoffLocal instanceof DateTime)) {
            $startLocal  = new DateTime('now', $tzParis);
            $cutoffLocal = (clone $startLocal)->modify('+2 days');
        }
    } else {
        $startLocal  = new DateTime('now', $tzParis);
        $cutoffLocal = (clone $startLocal)->modify('+2 days');
    }

    // Convertit en UTC (API dates sont UTC)
    $tzUTC    = new DateTimeZone('UTC');
    $startUTC = (clone $startLocal)->setTimezone($tzUTC);
    $cutoffUTC= (clone $cutoffLocal)->setTimezone($tzUTC);

    $startTs  = $startUTC->getTimestamp();
    $cutoffTs = $cutoffUTC->getTimestamp();

    // ✅ dates UTC à interroger (fenêtre peut chevaucher 2-3 jours)
    $days = [];
    $d = (clone $startUTC);  $d->setTime(0,0,0);
    $end = (clone $cutoffUTC); $end->setTime(0,0,0);

    while ($d <= $end) {
        $days[] = $d->format('Y-m-d'); // date en UTC
        $d->modify('+1 day');
    }

    foreach ($days as $dateUTC) {

        $api_url = add_query_arg(
            ['league' => $league_id, 'season' => $season, 'date' => $dateUTC],
            'https://v1.basketball.api-sports.io/games'
        );

        $ckey  = "tp_games_{$league_id}_{$season}_{$dateUTC}";
        $daily = tp_http_json($api_url, [], $ckey, 60);
        if (empty($daily['response'])) continue;

        foreach ((array)$daily['response'] as $match) {

            if (empty($match['date'])) continue;

            // ✅ DateTime peut throw si format invalide
            try {
                $utc_dt = new DateTime((string)$match['date'], $tzUTC);
            } catch (Exception $e) {
                continue;
            }

            $ts_utc = $utc_dt->getTimestamp();

            // Filtre strict : [start, cutoff)
            if ($ts_utc < $startTs || $ts_utc >= $cutoffTs) continue;

            // Affichage en heure de Paris
            $disp_dt = (clone $utc_dt)->setTimezone($tzParis);
            $display = $disp_dt->format('Y-m-d H:i');

            $matches[] = [
                'id'      => $match['id'] ?? ($match['game']['id'] ?? null),
                'date'    => $display,
                'date_ts' => $ts_utc,
                'league'  => $match['league']  ?? null,
                'country' => $match['country'] ?? null,
                'teams'   => $match['teams']   ?? null,
                'status'  => $match['status']  ?? null,
                'scores'  => $match['scores']  ?? null,
            ];
        }
    }

usort($matches, function($a, $b){
  return ($a['date_ts'] ?? 0) <=> ($b['date_ts'] ?? 0);
});

    return $matches;
}}


if (!function_exists('get_h2h_games')) {
function get_h2h_games($team1_id,$team2_id,$league_id='12',$season='2025-2026',$before_ts=null) {

    // ✅ dépendances indispensables
    if (!function_exists('tp_http_json') || !function_exists('add_query_arg')) {
        return [];
    }

    // ✅ tp_is_finished : si absent, on ne filtre pas (on garde tout ce qui a des scores)
    $is_finished = function($status){
        if (function_exists('tp_is_finished')) return tp_is_finished($status);
        // fallback minimal : on considère "fini" si status contient "FT" ou "Finished"
        $s = strtolower(is_array($status) ? ($status['short'] ?? ($status['long'] ?? '')) : (string)$status);
        return (strpos($s,'ft') !== false) || (strpos($s,'finished') !== false) || (strpos($s,'ended') !== false);
    };

    $api_url = add_query_arg(
        ['league'=>$league_id,'season'=>$season,'h2h'=>"$team1_id-$team2_id"],
        'https://v1.basketball.api-sports.io/games'
    );
    $ckey = "tp_h2h_{$league_id}_{$season}_{$team1_id}_{$team2_id}";
    $data = tp_http_json($api_url, [], $ckey, 300); // 5 min
    if (empty($data['response'])) return [];

    $finished = array_filter((array)$data['response'], fn($g)=>$is_finished($g['status'] ?? []));

    // strtotime fallback safe
    if ($before_ts) {
        $finished = array_filter($finished, function($g) use ($before_ts){
            $t = isset($g['date']) ? strtotime((string)$g['date']) : false;
            return $t ? ($t < $before_ts) : false;
        });
    }

    foreach ($finished as &$g){
        $home = $g['scores']['home'] ?? [];
        $away = $g['scores']['away'] ?? [];

        $g['total_points'] = (int)($home['total'] ?? 0) + (int)($away['total'] ?? 0);

        $g['periods'] = [
            'Q1'=>['home'=>(int)($home['quarter_1'] ?? 0),'away'=>(int)($away['quarter_1'] ?? 0)],
            'Q2'=>['home'=>(int)($home['quarter_2'] ?? 0),'away'=>(int)($away['quarter_2'] ?? 0)],
            'Q3'=>['home'=>(int)($home['quarter_3'] ?? 0),'away'=>(int)($away['quarter_3'] ?? 0)],
            'Q4'=>['home'=>(int)($home['quarter_4'] ?? 0),'away'=>(int)($away['quarter_4'] ?? 0)],
            'OT'=>['home'=>(int)($home['over_time'] ?? 0),'away'=>(int)($away['over_time'] ?? 0)],
        ];
    } unset($g);

    return array_values($finished);
}}
if (!function_exists('get_last_5_games_data_for_team')) {
function get_last_5_games_data_for_team($team_id,$league_id='12',$season='2025-2026',$before_ts=null) {

    // ✅ dépendances indispensables
    if (!function_exists('tp_http_json') || !function_exists('add_query_arg')) {
        return [];
    }

    // ✅ tp_is_finished : fallback minimal si absent
    $is_finished = function($status){
        if (function_exists('tp_is_finished')) return tp_is_finished($status);
        $s = strtolower(is_array($status) ? ($status['short'] ?? ($status['long'] ?? '')) : (string)$status);
        return (strpos($s,'ft') !== false) || (strpos($s,'finished') !== false) || (strpos($s,'ended') !== false);
    };

    $api_url = add_query_arg(
        ['team'=>$team_id,'league'=>$league_id,'season'=>$season],
        'https://v1.basketball.api-sports.io/games'
    );
    $ckey = "tp_teamgames_{$league_id}_{$season}_{$team_id}";
    $data = tp_http_json($api_url, [], $ckey, 300); // 5 min
    if (empty($data['response'])) return [];

    $games = array_filter((array)$data['response'], function($g) use ($team_id, $is_finished){
        if (!$is_finished($g['status'] ?? [])) return false;

        $home_total = $g['scores']['home']['total'] ?? null;
        $away_total = $g['scores']['away']['total'] ?? null;
        if ($home_total===null || $away_total===null) return false;

        $hid = $g['teams']['home']['id'] ?? null;
        $aid = $g['teams']['away']['id'] ?? null;
        return ($hid == $team_id) || ($aid == $team_id);
    });

    if ($before_ts) {
        $games = array_filter($games, function($g) use ($before_ts){
            $t = isset($g['date']) ? strtotime((string)$g['date']) : false;
            return $t ? ($t < $before_ts) : false;
        });
    }

    usort($games, function($a,$b){
        $ta = isset($a['date']) ? strtotime((string)$a['date']) : 0;
        $tb = isset($b['date']) ? strtotime((string)$b['date']) : 0;
        return $tb <=> $ta;
    });

    // (tu avais 6 dans ton code) → je conserve 6
    return array_slice(array_values($games), 0, 6);
}}


if (!function_exists('get_players_statistics_last_5_games_by_team')) {
function get_players_statistics_last_5_games_by_team($team_id,$league_id='12',$season='2025-2026',$before_ts=null){

    // ✅ dépendances indispensables
    if (!function_exists('get_last_5_games_data_for_team') || !function_exists('tp_http_json')) {
        return [];
    }
    if (!function_exists('add_query_arg')) {
        return [];
    }

    // ✅ minutes helper : si absent, fallback simple
    $minutes_to_decimal = function($mmss){
        if (function_exists('tp_minutes_to_decimal')) {
            return tp_minutes_to_decimal($mmss);
        }
        $s = trim((string)$mmss);
        if ($s === '' || strpos($s, ':') === false) return 0.0;
        $parts = array_map('intval', explode(':', $s, 2));
        $m = (int)($parts[0] ?? 0);
        $sec = (int)($parts[1] ?? 0);
        return $m + ($sec/60);
    };

    $players_totals = [];
    $last_5         = get_last_5_games_data_for_team($team_id,$league_id,$season,$before_ts);

    // array_column peut ne pas être dispo (très vieux PHP) → fallback
    if (function_exists('array_column')) {
        $game_ids = array_column((array)$last_5,'id');
    } else {
        $game_ids = [];
        foreach ((array)$last_5 as $g){
            if (!empty($g['id'])) $game_ids[] = $g['id'];
        }
    }

    foreach ((array)$game_ids as $game_id){
        if (!$game_id) continue;

        $api_url = add_query_arg(['id'=>$game_id], 'https://v1.basketball.api-sports.io/games/statistics/players');
        $ckey    = "tp_gstats_".$game_id;
        $data    = tp_http_json($api_url, [], $ckey, 600); // 10 min (boxscore figé)
        if (empty($data['response'])) continue;

        foreach ((array)$data['response'] as $entry){
            if (($entry['team']['id'] ?? null) != $team_id) continue;

            $player_id   = $entry['player']['id'] ?? null;
            $player_name = trim((string)($entry['player']['name'] ?? ''));
            $team_name   = (string)($entry['team']['name'] ?? '');

            if (!$player_id || $player_name === '') continue;

            if (!isset($players_totals[$player_id])){
                $players_totals[$player_id]=[
                    'name'            => $player_name,
                    'team_id'         => $team_id,
                    'team_name'       => $team_name,
                    'points'          => 0,
                    'assists'         => 0,
                    'rebounds'        => 0,
                    'threepoints_made'=> 0,
                    'minutes_total'   => 0.0,
                    'tp_total'        => 0,
                    'tp_attempts'     => 0,
                    'ft_total'        => 0,
                    'ft_attempts'     => 0,
                    'games'           => 0,
                ];
            } else {
                // complète le team_name si vide
                if (empty($players_totals[$player_id]['team_name']) && $team_name !== '') {
                    $players_totals[$player_id]['team_name'] = $team_name;
                }
            }

            $players_totals[$player_id]['points']           += (int)($entry['points'] ?? 0);
            $players_totals[$player_id]['assists']          += (int)($entry['assists'] ?? 0);
            $players_totals[$player_id]['rebounds']         += (int)($entry['rebounds']['total'] ?? 0);
            $players_totals[$player_id]['threepoints_made'] += (int)($entry['threepoint_goals']['total'] ?? 0);
            $players_totals[$player_id]['minutes_total']    += (float)$minutes_to_decimal($entry['minutes'] ?? '0:00');

            $players_totals[$player_id]['tp_total']         += (int)($entry['threepoint_goals']['total'] ?? 0);
            $players_totals[$player_id]['tp_attempts']      += (int)($entry['threepoint_goals']['attempts'] ?? 0);
            $players_totals[$player_id]['ft_total']         += (int)($entry['freethrows_goals']['total'] ?? 0);
            $players_totals[$player_id]['ft_attempts']      += (int)($entry['freethrows_goals']['attempts'] ?? 0);
            $players_totals[$player_id]['games']            += 1;
        }
    }

    $filtered=[];
    foreach ($players_totals as $p){
        $g           = max(1,(int)($p['games'] ?? 0));
        $avg_minutes = ((float)($p['minutes_total'] ?? 0)) / $g;
        if ($avg_minutes < 10) continue;

        $p['avg_points']      = round(((int)$p['points'])/$g,1);
        $p['avg_rebounds']    = round(((int)$p['rebounds'])/$g,1);
        $p['avg_assists']     = round(((int)$p['assists'])/$g,1);
        $p['avg_minutes']     = round($avg_minutes,1);
        $p['avg_3pts']        = round(((int)$p['threepoints_made'])/$g,1);
        $p['tp_pct']          = ((int)($p['tp_attempts'] ?? 0) > 0)
            ? round(((int)$p['tp_total'])/max(1,(int)$p['tp_attempts'])*100,1).'%' : '0%';
        $p['ft_pct']          = ((int)($p['ft_attempts'] ?? 0) > 0)
            ? round(((int)$p['ft_total'])/max(1,(int)$p['ft_attempts'])*100,1).'%' : '0%';

        $p['total_points']    = (int)$p['points'];
        $p['total_rebounds']  = (int)$p['rebounds'];
        $p['total_assists']   = (int)$p['assists'];

        $p['prediction_total']= (int)$p['points'] + (int)$p['rebounds'] + (int)$p['assists'];
        $p['prediction_avg']  = round($p['prediction_total']/$g,1);

        $filtered[] = $p;
    }

    usort($filtered, fn($a,$b)=>($b['prediction_avg'] ?? 0) <=> ($a['prediction_avg'] ?? 0));
    return $filtered;
}}


if (!function_exists('get_player_stats_for_h2h')) {
function get_player_stats_for_h2h($team1_id,$team2_id,$league_id='12',$season='2025-2026',$before_ts=null){

    // ✅ garde-fous : dépendances indispensables
    if (!function_exists('get_h2h_games') || !function_exists('tp_http_json')) {
        return [];
    }

    // ✅ garde-fous WP helpers
    if (!function_exists('add_query_arg')) {
        return [];
    }

    // ✅ minutes helper : si absent, fallback simple (0)
    $minutes_to_decimal = function($mmss){
        if (function_exists('tp_minutes_to_decimal')) {
            return tp_minutes_to_decimal($mmss);
        }
        // fallback "MM:SS" => minutes décimales
        $s = trim((string)$mmss);
        if ($s === '') return 0.0;
        if (!preg_match('/^(\d+)\s*:\s*(\d+)$/', $s, $m)) return 0.0;
        $min = (int)$m[1];
        $sec = (int)$m[2];
        return $min + ($sec/60);
    };

    $games         = get_h2h_games($team1_id,$team2_id,$league_id,$season,$before_ts);
    $players_stats = [];
    $team_names    = [];

    if (!empty($games)){
        $g0 = $games[array_key_first($games)];
        $team_names[$g0['teams']['home']['id']] = $g0['teams']['home']['name'] ?? '';
        $team_names[$g0['teams']['away']['id']] = $g0['teams']['away']['name'] ?? '';
    }

    foreach ((array)$games as $game){
        $gid = $game['id'] ?? null;
        if (!$gid) continue;

        $api_url = add_query_arg(['id'=>$gid], 'https://v1.basketball.api-sports.io/games/statistics/players');
        $ckey    = "tp_gstats_".$gid;

        $data = tp_http_json($api_url, [], $ckey, 600);
        if (empty($data['response'])) continue;

        foreach ((array)$data['response'] as $entry){
            $tid = $entry['team']['id'] ?? null;
            if ($tid != $team1_id && $tid != $team2_id) continue;

            $pid  = $entry['player']['id'] ?? null;
            $name = trim((string)($entry['player']['name'] ?? ''));
            if (!$pid || $name === '') continue;

            if (!isset($players_stats[$pid])){
                $players_stats[$pid]=[
                    'name'             => $name,
                    'team_id'          => $tid,
                    'team_name'        => $team_names[$tid] ?? ($entry['team']['name'] ?? ''),
                    'games'            => 0,
                    'points'           => 0,
                    'rebounds'         => 0,
                    'assists'          => 0,
                    'threepoints_made' => 0,
                    'tp_attempts'      => 0,
                    'ft_total'         => 0,
                    'ft_attempts'      => 0,
                    'minutes_total'    => 0.0,
                ];
            } else {
                // complète le team_name si vide
                if (empty($players_stats[$pid]['team_name'])) {
                    $players_stats[$pid]['team_name'] = $team_names[$tid] ?? ($entry['team']['name'] ?? '');
                }
            }

            $players_stats[$pid]['games']            += 1;
            $players_stats[$pid]['points']           += (int)($entry['points'] ?? 0);
            $players_stats[$pid]['rebounds']         += (int)($entry['rebounds']['total'] ?? 0);
            $players_stats[$pid]['assists']          += (int)($entry['assists'] ?? 0);
            $players_stats[$pid]['threepoints_made'] += (int)($entry['threepoint_goals']['total'] ?? 0);
            $players_stats[$pid]['tp_attempts']      += (int)($entry['threepoint_goals']['attempts'] ?? 0);
            $players_stats[$pid]['ft_total']         += (int)($entry['freethrows_goals']['total'] ?? 0);
            $players_stats[$pid]['ft_attempts']      += (int)($entry['freethrows_goals']['attempts'] ?? 0);
            $players_stats[$pid]['minutes_total']    += (float)$minutes_to_decimal($entry['minutes'] ?? '0:00');
        }
    }

    $out=[];
    foreach ($players_stats as $p){
        $g       = max(1,(int)($p['games'] ?? 0));
        $avg_min = ((float)($p['minutes_total'] ?? 0)) / $g;
        if ($avg_min < 10) continue;

        $prediction_total = (int)($p['points'] ?? 0) + (int)($p['rebounds'] ?? 0) + (int)($p['assists'] ?? 0);

        $out[] = [
            'name'             => (string)$p['name'],
            'team_id'          => $p['team_id'],
            'team_name'        => (string)($p['team_name'] ?? ''),
            'games'            => (int)$p['games'],
            'total_points'     => (int)$p['points'],
            'total_rebounds'   => (int)$p['rebounds'],
            'total_assists'    => (int)$p['assists'],
            'prediction_total' => $prediction_total,
            'prediction_avg'   => round($prediction_total/$g,1),
            'avg_points'       => round(((int)$p['points'])/$g,1),
            'avg_rebounds'     => round(((int)$p['rebounds'])/$g,1),
            'avg_assists'      => round(((int)$p['assists'])/$g,1),
            'avg_3pts'         => round(((int)$p['threepoints_made'])/$g,1),
            'tp_pct'           => ((int)($p['tp_attempts'] ?? 0) > 0)
                                   ? round(((int)$p['threepoints_made'])/max(1,(int)$p['tp_attempts'])*100,1)."%" : "0%",
            'ft_pct'           => ((int)($p['ft_attempts'] ?? 0) > 0)
                                   ? round(((int)$p['ft_total'])/max(1,(int)$p['ft_attempts'])*100,1)."%" : "0%",
            'avg_minutes'      => round($avg_min,1),
        ];
    }

    usort($out, fn($a,$b)=>($b['prediction_avg'] ?? 0) <=> ($a['prediction_avg'] ?? 0));
    return $out;
}}


/**
 * Combine H2H + L5 équipes, construit les prédictions joueur
 * et retourne ['combined'=>..., 'checks'=>...] tout en affichant la table principale.
 */
if (!function_exists('get_combined_player_predictions')) {
function get_combined_player_predictions($team1_id,$team2_id,$league_id='12',$season='2025-2026',$before_ts=null,$home_team=[],$away_team=[]){

    // ✅ garde-fous (fonctionnelles)
    if (!function_exists('get_player_stats_for_h2h')
     || !function_exists('get_players_statistics_last_5_games_by_team')
     || !function_exists('tp_nba_norm_name')) {
        return ['combined'=>[], 'checks'=>[
            'duos'=>[], 'points'=>[], 'rebounds'=>[], 'assists'=>[], 'threes'=>[], 'awards'=>[]
        ]];
    }

    // ✅ garde-fous (WP)
    if (!function_exists('esc_html')) {
        // si pas dans WP context, on évite de casser
        return ['combined'=>[], 'checks'=>[
            'duos'=>[], 'points'=>[], 'rebounds'=>[], 'assists'=>[], 'threes'=>[], 'awards'=>[]
        ]];
    }

    $has_stats_btn    = function_exists('tpb_player_stats_button');
    $has_conf_for     = function_exists('tp_nba_confidence_for');
    $has_conf_chip    = function_exists('tp_nba_conf_chip');
    $has_duo_conf     = function_exists('tp_nba_duo_confidence_pts');

    $h2h_players  = get_player_stats_for_h2h($team1_id,$team2_id,$league_id,$season,$before_ts);
    $team1_recent = get_players_statistics_last_5_games_by_team($team1_id,$league_id,$season,$before_ts);
    $team2_recent = get_players_statistics_last_5_games_by_team($team2_id,$league_id,$season,$before_ts);

    $all_players = array_merge((array)$h2h_players,(array)$team1_recent,(array)$team2_recent);

    $combined   = [];
    $team_names = [
        $team1_id => ($home_team['name'] ?? ''),
        $team2_id => ($away_team['name'] ?? ''),
    ];

    // Sélections à valider
    $checks = [
        'duos'     => [],
        'points'   => [],
        'rebounds' => [],
        'assists'  => [],
        'threes'   => [],
        'awards'   => [],
    ];

    foreach ($all_players as $p){
        if (!isset($p['games']) || (int)$p['games'] < 3) continue;

        $key = tp_nba_norm_name($p['name'] ?? '');
        if ($key === '') continue;

        // ---- Normalise l'équipe + fallback sur la map du match
        $tid   = $p['team_id']   ?? ($p['team']['id']   ?? null);
        $tname = $p['team_name'] ?? ($p['team']['name'] ?? '');
        if (!$tname && $tid && isset($team_names[$tid])) {
            $tname = $team_names[$tid];
        }

        if (!isset($combined[$key])){
            $combined[$key] = [
                'name'            => $p['name'],
                'team_id'         => $tid,
                'team_name'       => $tname,
                'total_points'    => 0,
                'total_rebounds'  => 0,
                'total_assists'   => 0,
                'total_avg_3pts'  => 0,
                'avg_3pts_count'  => 0,
                'games'           => 0,
            ];
        } else {
            // Complète une entrée existante si l'équipe manquait
            if (empty($combined[$key]['team_id'])   && $tid)   { $combined[$key]['team_id']   = $tid; }
            if (empty($combined[$key]['team_name']) && $tname) { $combined[$key]['team_name'] = $tname; }
        }

        $combined[$key]['total_points']   += (float)($p['total_points']   ?? 0);
        $combined[$key]['total_rebounds'] += (float)($p['total_rebounds'] ?? 0);
        $combined[$key]['total_assists']  += (float)($p['total_assists']  ?? 0);

        if (isset($p['avg_3pts'])){
            $combined[$key]['total_avg_3pts'] += (float)$p['avg_3pts'];
            $combined[$key]['avg_3pts_count'] += 1;
        }

        $combined[$key]['games'] += (int)($p['games'] ?? 0);
    }

    $sorted = array_values($combined);
    usort($sorted, function($a,$b){
        $avg_a = ($a['games'] ?? 0) > 0 ? floor(($a['total_points'] ?? 0)/$a['games']) : 0;
        $avg_b = ($b['games'] ?? 0) > 0 ? floor(($b['total_points'] ?? 0)/$b['games']) : 0;
        return ($b['games'] ?? 0)===0 ? -1 : (($a['games'] ?? 0)===0 ? 1 : $avg_b <=> $avg_a);
    });

    if (!empty($sorted)){
        echo "<h4 class='basket-title'>💥 Prédiction</h4>";
        echo "<div class='card'><table class='table table--pred compact'><thead><tr>
                <th>Joueur</th><th>Équipe</th><th>Pts</th><th>Reb</th><th>Pas</th><th>3Pts</th><th>PRP</th>
              </tr></thead><tbody>";

        $top_avg  = [];
        $assistL  = [];
        $rebL     = [];
        $shootL   = [];

        foreach ($sorted as $pl){
            $g  = max(1,(int)($pl['games'] ?? 0));
            $ap = (int)floor(((float)($pl['total_points'] ?? 0))/$g);
            $ar = (int)floor(((float)($pl['total_rebounds'] ?? 0))/$g);
            $aa = (int)floor(((float)($pl['total_assists'] ?? 0))/$g);
            $a3 = ((int)($pl['avg_3pts_count'] ?? 0))>0 ? round(((float)($pl['total_avg_3pts'] ?? 0))/max(1,(int)$pl['avg_3pts_count']),1) : 0;
            $prp= $ap+$ar+$aa;

            if ($prp>=14){
                $top_avg[] = ['name'=>$pl['name'], 'avg_points'=>$ap, 'avg_3pts'=>$a3];
                $assistL[] = ['name'=>$pl['name'], 'avg_assists'=>$aa];
                $rebL[]    = ['name'=>$pl['name'], 'avg_rebounds'=>$ar];
                $shootL[]  = ['name'=>$pl['name'], 'avg_points'=>$ap, 'avg_3pts'=>$a3];

                $team_label = $pl['team_name'] ?? '';
                if ($team_label === '' && !empty($pl['team_id']) && isset($team_names[$pl['team_id']])) {
                    $team_label = $team_names[$pl['team_id']];
                }

                $pname = (string)($pl['name'] ?? '');
                $btn   = ($has_stats_btn && $pname !== '') ? (" ".tpb_player_stats_button($pname)) : '';

                echo "<tr>
                        <td>".esc_html($pname).$btn."</td>
                        <td class='team-name'>".esc_html($team_label)."</td>
                        <td class='num'>{$ap}</td>
                        <td class='num'>{$ar}</td>
                        <td class='num'>{$aa}</td>
                        <td class='num'>{$a3}</td>
                        <td class='num prp'>{$prp}</td>
                      </tr>";
            }
        }

        // Tri pour différentes listes
        usort($top_avg, fn($a,$b)=>$b['avg_points']   <=> $a['avg_points']);
        usort($assistL, fn($a,$b)=>$b['avg_assists']  <=> $a['avg_assists']);
        usort($rebL,    fn($a,$b)=>$b['avg_rebounds'] <=> $a['avg_rebounds']);
        usort($shootL,  fn($a,$b)=>$b['avg_3pts']     <=> $a['avg_3pts']);

        $duo_results  = [];
        $bestScorer   = "";
        $bestAst      = "";
        $bestReb      = "";
        $quatuor      = "";
        $reb_label    = "";
        $ast_label    = "";
        $shoot_label  = "";

        // Duos marqueurs
        if (count($top_avg)>=3){
            $duos = [
                ['l'=>$top_avg[0]['name'].' + '.$top_avg[1]['name'], 's'=>$top_avg[0]['avg_points']+$top_avg[1]['avg_points']-3.5, 'r'=>'Ma sélection Duo Marqueurs 1er choix'],
                ['l'=>$top_avg[0]['name'].' + '.$top_avg[2]['name'], 's'=>$top_avg[0]['avg_points']+$top_avg[2]['avg_points']-3.5, 'r'=>'Ma sélection Duo Marqueurs 2e choix (si blessé au 1er choix)'],
                ['l'=>$top_avg[1]['name'].' + '.$top_avg[2]['name'], 's'=>$top_avg[1]['avg_points']+$top_avg[2]['avg_points']-3.5, 'r'=>'Ma sélection Duo Marqueurs 3e choix (si blessé au 2e choix)'],
            ];
            $paliers = [69.5,64.5,59.5,54.5,49.5,44.5,39.5,34.5,29.5,24.5];

            foreach($duos as $d){
                $pal='Pas de confiance';
                foreach($paliers as $p){
                    if($d['s'] >= $p){ $pal=$p; break; }
                }

                // 🔢 % de validation du duo (si la fonction existe)
                $conf = null;
                if ($has_duo_conf && is_numeric($pal)) {
                    $players = array_map('trim', explode('+', $d['l']));
                    if (count($players) === 2) {
                        $conf = tp_nba_duo_confidence_pts($players[0], $players[1], (float)$pal);
                    }
                }

                $duo_results[] = [
                    'label'      => $d['l'],
                    'palier'     => $pal,
                    'rank'       => $d['r'],
                    'confidence' => $conf,
                ];
            }

            if ($top_avg[0]['avg_points'] - $top_avg[1]['avg_points'] >= 7) {
                $bestScorer = $top_avg[0]['name'];
            }
        }

        // Alimenter checks[duos]
        foreach ($duo_results as $dr){
            if (is_numeric($dr['palier'])){
                $parts = array_map('trim', explode('+',$dr['label']));
                if (count($parts)===2 && $parts[0]!=='' && $parts[1]!==''){
                    $checks['duos'][]=[
                        'players'    => [$parts[0],$parts[1]],
                        'threshold'  => (float)$dr['palier'],
                        'rank'       => $dr['rank'] ?? '',
                        'confidence' => $dr['confidence'] ?? null,
                    ];
                }
            }
        }

        if (count($assistL)>=2 && ($assistL[0]['avg_assists']-$assistL[1]['avg_assists'])>=4) $bestAst = $assistL[0]['name'];
        if (count($rebL)>=2    && ($rebL[0]['avg_rebounds']-$rebL[1]['avg_rebounds'])>=4)     $bestReb = $rebL[0]['name'];

        // 🎯 Points (quatuor) + confiance (si fonctions dashboard dispo)
        if (count($top_avg)>=4){
            $p_pts = [29.5,24.5,19.5,14.5,9.5,4.5];
            foreach (array_slice($top_avg,0,4) as $pl){
                $adj = $pl['avg_points']-3.5;
                $pal = null;
                foreach($p_pts as $pt){ if($adj>=$pt){ $pal=$pt; break; } }
                if ($pal!==null){
                    $conf       = ($has_conf_for ? tp_nba_confidence_for($pl['name'], 'PTS', (float)$pal) : null);
                    $conf_label = ($has_conf_chip ? tp_nba_conf_chip($conf) : '');

                    $quatuor .= $pl['name']." (".$pal.")".$conf_label." ";

                    $checks['points'][] = [
                        'player'     => $pl['name'],
                        'threshold'  => (float)$pal,
                        'confidence' => $conf,
                    ];
                }
            }
        }

        // 🪣 Rebonds
        if (count($rebL)>=3){
            $p_reb=[12.5,9.5,6.5,4.5,2.5];
            foreach (array_slice($rebL,0,3) as $r){
                $adj = $r['avg_rebounds']-2;
                $pal = null;
                foreach($p_reb as $pr){ if($adj>=$pr){ $pal=$pr; break; } }
                if ($pal!==null){
                    $conf       = ($has_conf_for ? tp_nba_confidence_for($r['name'], 'REB', (float)$pal) : null);
                    $conf_label = ($has_conf_chip ? tp_nba_conf_chip($conf) : '');

                    $reb_label .= $r['name']." (".$pal.")".$conf_label." ";

                    $checks['rebounds'][] = [
                        'player'     => $r['name'],
                        'threshold'  => (float)$pal,
                        'confidence' => $conf,
                    ];
                }
            }
        }

        // 🎯 Passes
        if (count($assistL)>=3){
            $p_ast=[12.5,9.5,6.5,4.5,2.5];
            foreach (array_slice($assistL,0,3) as $a){
                $adj = $a['avg_assists']-2;
                $pal = null;
                foreach($p_ast as $pa){ if($adj>=$pa){ $pal=$pa; break; } }
                if ($pal!==null){
                    $conf       = ($has_conf_for ? tp_nba_confidence_for($a['name'], 'AST', (float)$pal) : null);
                    $conf_label = ($has_conf_chip ? tp_nba_conf_chip($conf) : '');

                    $ast_label .= $a['name']." (".$pal.")".$conf_label." ";

                    $checks['assists'][] = [
                        'player'     => $a['name'],
                        'threshold'  => (float)$pal,
                        'confidence' => $conf,
                    ];
                }
            }
        }

        // 🔥 3 pts
        usort($top_avg, fn($a,$b)=>$b['avg_points'] <=> $a['avg_points']);
        $top6 = array_slice($top_avg,0,6);
        usort($top6, fn($a,$b)=>$b['avg_3pts'] <=> $a['avg_3pts']);

        $p_3=[4.5,3.5,2.5,1.5,0.5];
        foreach (array_slice($top6,0,3) as $s){
            if ($s['avg_points']>=15){
                $adj = floor($s['avg_3pts'] - 1.5);
                foreach($p_3 as $v){
                    if($adj >= $v){
                        $conf       = ($has_conf_for ? tp_nba_confidence_for($s['name'], '3PM', (float)$v) : null);
                        $conf_label = ($has_conf_chip ? tp_nba_conf_chip($conf) : '');

                        $shoot_label .= $s['name'].
                            " (<span class=\"chip chip--neutral\">".$v."</span>)".
                            $conf_label.", ";

                        $checks['threes'][] = [
                            'player'     => $s['name'],
                            'threshold'  => (float)$v,
                            'confidence' => $conf,
                        ];
                        break;
                    }
                }
            }
        }

        // Ligne duos
        echo "<tr class='info-row info-row--duos'><td colspan='7'>";
        foreach ($duo_results as $result){
            $noPal = ($result['palier']==='Pas de confiance' || !is_numeric($result['palier']));

            $chipPal = $noPal
                ? "<span class='chip chip--muted'>Pas de confiance</span>"
                : "<span class='chip chip--accent'>".esc_html($result['palier'])." pts</span>";

            $chipConf = '';
            if (!$noPal && isset($result['confidence']) && $result['confidence'] !== null) {
                $chipConf = " <span class='chip chip--neutral'>".(int)$result['confidence']."%</span>";
            }

            echo "<div class='duo-line'>🔥 <strong>".esc_html($result['rank'])."</strong> — "
                .esc_html($result['label'])." ➡️ + {$chipPal}{$chipConf}</div>";
        }
        echo "</td></tr>";

        // Ligne FUN My Match
        $full = "";
        if ($quatuor!=="")     $full.="🧠 <strong>Points :</strong> ".trim($quatuor)."<br>";
        if ($reb_label!=="")   $full.="🪣 <strong>Rebonds :</strong> ".trim($reb_label)."<br>";
        if ($ast_label!=="")   $full.="🎯 <strong>Passes :</strong> ".trim($ast_label)."<br>";
        if ($shoot_label!=="") $full.="🔥 <strong>Tirs à 3 pts :</strong> ".rtrim(trim($shoot_label),',')."<br>";

        if ($full!==""){
            echo "<tr class='info-row info-row--fun'><td colspan='7'>🔮 <strong>Ma sélection FUN My match</strong><br>$full</td></tr>";
        }

        // Awards
        if ($bestScorer!=="") {
            echo "<tr class='info-row info-row--award'><td colspan='7'>🏀 <strong>Meilleur Marqueur :</strong> <span class='chip chip--brand'>".esc_html($bestScorer)."</span></td></tr>";
        }
        if ($bestAst!=="") {
            echo "<tr class='info-row info-row--award'><td colspan='7'>🎯 <strong>Meilleur Passeur :</strong> <span class='chip chip--brand'>".esc_html($bestAst)."</span></td></tr>";
        }
        if ($bestReb!=="") {
            echo "<tr class='info-row info-row--award'><td colspan='7'>🛡️ <strong>Meilleur Rebondeur :</strong> <span class='chip chip--brand'>".esc_html($bestReb)."</span></td></tr>";
        }

        if ($bestScorer)  { $checks['awards']['scorer']    = $bestScorer; }
        if ($bestAst)     { $checks['awards']['assister']  = $bestAst; }
        if ($bestReb)     { $checks['awards']['rebounder'] = $bestReb; }

        echo "</tbody></table></div>";
    } else {
        echo "<p style='color:#a16207;'>Aucune donnée combinée trouvée.</p>";
    }

    return ['combined'=>$combined,'checks'=>$checks];
}}


/** Renderer pour réafficher la table à partir du snapshot (cache) */
if (!function_exists('tp_render_prediction_table_from_snapshot')) {
function tp_render_prediction_table_from_snapshot(array $combined, array $checks = []){
    if (empty($combined)) return;

    // ✅ garde-fous WP / bouton stats
    if (!function_exists('esc_html')) return;
    $has_stats_btn = function_exists('tpb_player_stats_button');

    echo "<h4 class='basket-title'>💥 Prédiction</h4>";
    echo "<div class='card'><table class='table table--pred compact'><thead><tr>
            <th>Joueur</th><th>Équipe</th><th>Pts</th><th>Reb</th><th>Pas</th><th>3Pts</th><th>PRP</th>
          </tr></thead><tbody>";

    foreach ($combined as $pl){
        $g  = max(1, (int)($pl['games'] ?? 0));
        $ap = (int)floor(((float)($pl['total_points']   ?? 0))/$g);
        $ar = (int)floor(((float)($pl['total_rebounds'] ?? 0))/$g);
        $aa = (int)floor(((float)($pl['total_assists']  ?? 0))/$g);
        $a3 = ((int)($pl['avg_3pts_count'] ?? 0)) > 0
            ? round(((float)($pl['total_avg_3pts'] ?? 0))/max(1,(int)$pl['avg_3pts_count']), 1)
            : 0;

        $prp = $ap + $ar + $aa;
        if ($prp < 14) continue;

        $team_label = $pl['team_name'] ?? '';
        $pname      = (string)($pl['name'] ?? '');

        $btn = ($has_stats_btn && $pname !== '') ? (" ".tpb_player_stats_button($pname)) : '';

        echo "<tr>
                <td>".esc_html($pname).$btn."</td>
                <td class='team-name'>".esc_html($team_label)."</td>
                <td class='num'>{$ap}</td>
                <td class='num'>{$ar}</td>
                <td class='num'>{$aa}</td>
                <td class='num'>{$a3}</td>
                <td class='num prp'>{$prp}</td>
              </tr>";
    }
// ---------- LIGNES INFO (DUOS / FUN / AWARDS) depuis $checks ----------
$has_conf_chip = function_exists('tp_nba_conf_chip');

if (!empty($checks['duos']) && is_array($checks['duos'])) {
  echo "<tr class='info-row info-row--duos'><td colspan='7'>";
  foreach ($checks['duos'] as $d){
    $p1 = $d['players'][0] ?? '';
    $p2 = $d['players'][1] ?? '';
    if ($p1 === '' || $p2 === '') continue;

    $pal = isset($d['threshold']) ? (float)$d['threshold'] : null;
    $rank = (string)($d['rank'] ?? '');
    $conf = $d['confidence'] ?? null;

    $chipPal = ($pal === null)
      ? "<span class='chip chip--muted'>Pas de confiance</span>"
      : "<span class='chip chip--accent'>".esc_html($pal)." pts</span>";

    $chipConf = '';
    if ($pal !== null && $conf !== null) {
      $chipConf = " <span class='chip chip--neutral'>".(int)$conf."%</span>";
    }

    echo "<div class='duo-line'>🔥 <strong>".esc_html($rank)."</strong> — "
      .esc_html($p1." + ".$p2)." ➡️ + {$chipPal}{$chipConf}</div>";
  }
  echo "</td></tr>";
}

// FUN My match (texte)
$full = "";

$mkBlocks = [
  'points'   => ['emoji'=>'🧠', 'title'=>'Points'],
  'rebounds' => ['emoji'=>'🪣', 'title'=>'Rebonds'],
  'assists'  => ['emoji'=>'🎯', 'title'=>'Passes'],
  'threes'   => ['emoji'=>'🔥', 'title'=>'Tirs à 3 pts'],
];

foreach ($mkBlocks as $k=>$meta){
  if (empty($checks[$k]) || !is_array($checks[$k])) continue;

  $line = "";
  foreach ($checks[$k] as $c){
    $player = (string)($c['player'] ?? '');
    if ($player === '') continue;

    $thr = $c['threshold'] ?? null;
    $conf = $c['confidence'] ?? null;

    $chip = '';
    if ($has_conf_chip) $chip = tp_nba_conf_chip($conf);

    $line .= $player." (".(is_numeric($thr)?(float)$thr:$thr).")".$chip." ";
  }

  $line = trim($line);
  if ($line !== '') {
    $full .= $meta['emoji']." <strong>".$meta['title']." :</strong> ".$line."<br>";
  }
}

if ($full !== '') {
  echo "<tr class='info-row info-row--fun'><td colspan='7'>🔮 <strong>Ma sélection FUN My match</strong><br>{$full}</td></tr>";
}

// Awards
if (!empty($checks['awards']['scorer'])) {
  echo "<tr class='info-row info-row--award'><td colspan='7'>🏀 <strong>Meilleur Marqueur :</strong> <span class='chip chip--brand'>".esc_html($checks['awards']['scorer'])."</span></td></tr>";
}
if (!empty($checks['awards']['assister'])) {
  echo "<tr class='info-row info-row--award'><td colspan='7'>🎯 <strong>Meilleur Passeur :</strong> <span class='chip chip--brand'>".esc_html($checks['awards']['assister'])."</span></td></tr>";
}
if (!empty($checks['awards']['rebounder'])) {
  echo "<tr class='info-row info-row--award'><td colspan='7'>🛡️ <strong>Meilleur Rebondeur :</strong> <span class='chip chip--brand'>".esc_html($checks['awards']['rebounder'])."</span></td></tr>";
}

    echo "</tbody></table></div>";
}}
/** Boxscore pour validation */
if (!function_exists('tp_fetch_boxscore_for_game')) {
function tp_fetch_boxscore_for_game($game_id){
    // ✅ garde-fous
    if (!function_exists('add_query_arg') || !function_exists('tp_http_json')) return [];

    if (!function_exists('tp_nba_norm_name')) {
        // fallback minimal si le normalizer n'existe pas
        function tp_nba_norm_name($name){
            $name = strtolower(trim((string)$name));
            $name = str_replace('.', '', $name);
            return $name;
        }
    }

    $api_url = add_query_arg(['id'=>$game_id], 'https://v1.basketball.api-sports.io/games/statistics/players');
    $ckey    = "tp_gstats_".$game_id;
    $data    = tp_http_json($api_url, [], $ckey, 600);
    if (empty($data['response'])) return [];

    $box = [];
    foreach ($data['response'] as $e){
        $name = trim($e['player']['name'] ?? '');
        if ($name === '') continue;

        $k = tp_nba_norm_name($name);
        $box[$k] = [
            'name'   => $name,
            'team'   => $e['team']['name'] ?? '',
            'pts'    => (int)($e['points'] ?? 0),
            'reb'    => (int)($e['rebounds']['total'] ?? 0),
            'ast'    => (int)($e['assists'] ?? 0),
            'threes' => (int)($e['threepoint_goals']['total'] ?? 0),
        ];
    }
    return $box;
}}


/** Tableau Validation */
if (!function_exists('tp_render_validation_table')) {
function tp_render_validation_table(array $checks, array $box, string $home_name = '', string $away_name = ''){
    if (empty($checks)) return;

    // ✅ garde-fous (évite fatal si snippet appelé sans WP/normalizer)
    if (!function_exists('esc_html')) return;
    if (!function_exists('tp_nba_norm_name')) {
        // fallback minimal si jamais le normalizer n'existe pas
        function tp_nba_norm_name($name){
            $name = strtolower(trim((string)$name));
            $name = str_replace('.', '', $name);
            return $name;
        }
    }

    $title_teams = ($home_name || $away_name)
        ? " — <span class=\"teams-heading\">".esc_html($home_name)." vs ".esc_html($away_name)."</span>"
        : "";

    $tot = ['ok'=>0,'ko'=>0];

    $chip = function($ok){
        return $ok
            ? "<span class='chip chip--ok'>OK</span>"
            : "<span class='chip chip--ko'>KO</span>";
    };

    $stat = function($name,$field) use($box){
        $k = tp_nba_norm_name($name);
        return isset($box[$k][$field]) ? $box[$k][$field] : 0;
    };

    $maxBy = function($field) use($box){
        $bestVal = -INF;
        $best    = ['',0];
        foreach($box as $p){
            $v = $p[$field] ?? -INF;
            if($v > $bestVal){
                $bestVal = $v;
                $best    = [$p['name'] ?? '', $v];
            }
        }
        return $best;
    };

    echo "<h4 class='basket-title'>📊 Validation des Prédictions{$title_teams}</h4>";
    echo "<div class='card compact'><table class='table compact'><thead><tr>
            <th>Type</th><th>Sélection</th><th>Critère</th><th>Résultat</th><th>Statut</th>
          </tr></thead><tbody>";

    // Duos
    foreach (($checks['duos'] ?? []) as $d){
        $p1 = $d['players'][0] ?? '';
        $p2 = $d['players'][1] ?? '';
        if ($p1 === '' || $p2 === '') continue;

        $sum = (int)$stat($p1,'pts') + (int)$stat($p2,'pts');
        $ok  = ($sum >= (float)($d['threshold'] ?? 0));
        $tot[$ok?'ok':'ko']++;

        echo "<tr>
                <td>Duo marqueurs</td>
                <td>".esc_html("$p1 + $p2")."</td>
                <td class='num'>≥ ".esc_html($d['threshold'])." pts"
                    .(!empty($d['rank']) ? " <small>(".esc_html($d['rank']).")</small>" : "")."</td>
                <td class='num'>".$sum."</td>
                <td>".$chip($ok)."</td>
              </tr>";
    }

    // Points / Rebonds / Passes / 3pts
    $map    = ['points'=>'pts','rebounds'=>'reb','assists'=>'ast','threes'=>'threes'];
    $labels = ['points'=>'Points','rebounds'=>'Rebonds','assists'=>'Passes','threes'=>'3Pts'];

    foreach ($map as $key=>$field){
        foreach (($checks[$key] ?? []) as $c){
            if (empty($c['player'])) continue;

            $val = (float)$stat($c['player'],$field);
            $ok  = ($val >= (float)($c['threshold'] ?? 0));
            $tot[$ok?'ok':'ko']++;

            echo "<tr>
                    <td>{$labels[$key]}</td>
                    <td>".esc_html($c['player'])."</td>
                    <td class='num'>≥ ".esc_html($c['threshold'])."</td>
                    <td class='num'>".$val."</td>
                    <td>".$chip($ok)."</td>
                  </tr>";
        }
    }

    // Awards
    if (!empty($checks['awards'])){
        [$bestPtsName,$bestPts] = $maxBy('pts');
        [$bestRebName,$bestReb] = $maxBy('reb');
        [$bestAstName,$bestAst] = $maxBy('ast');

        if (!empty($checks['awards']['scorer'])){
            $sel = $checks['awards']['scorer'];
            $ok  = (tp_nba_norm_name($sel) === tp_nba_norm_name((string)$bestPtsName));
            $tot[$ok?'ok':'ko']++;

            echo "<tr>
                    <td>Meilleur marqueur</td>
                    <td>".esc_html($sel)."</td>
                    <td>#1 Pts (".esc_html($bestPts).")</td>
                    <td class='num'>".(float)$stat($sel,'pts')."</td>
                    <td>".$chip($ok)."</td>
                  </tr>";
        }

        if (!empty($checks['awards']['rebounder'])){
            $sel = $checks['awards']['rebounder'];
            $ok  = (tp_nba_norm_name($sel) === tp_nba_norm_name((string)$bestRebName));
            $tot[$ok?'ok':'ko']++;

            echo "<tr>
                    <td>Meilleur rebondeur</td>
                    <td>".esc_html($sel)."</td>
                    <td>#1 Reb (".esc_html($bestReb).")</td>
                    <td class='num'>".(float)$stat($sel,'reb')."</td>
                    <td>".$chip($ok)."</td>
                  </tr>";
        }

        if (!empty($checks['awards']['assister'])){
            $sel = $checks['awards']['assister'];
            $ok  = (tp_nba_norm_name($sel) === tp_nba_norm_name((string)$bestAstName));
            $tot[$ok?'ok':'ko']++;

            echo "<tr>
                    <td>Meilleur passeur</td>
                    <td>".esc_html($sel)."</td>
                    <td>#1 Pas (".esc_html($bestAst).")</td>
                    <td class='num'>".(float)$stat($sel,'ast')."</td>
                    <td>".$chip($ok)."</td>
                  </tr>";
        }
    }

    echo "</tbody><tfoot><tr><td colspan='5' style='text-align:right; padding:10px;'>
            <span class='chip' style='background:#16a34a;color:#fff;'>".$tot['ok']." OK</span>
            <span class='chip chip--muted' style='margin-left:8px;'>".$tot['ko']." KO</span>
          </td></tr></tfoot></table></div>";
}}


/** ODDS — Bet365 (cache court) */
if (!function_exists('get_player_points_and_assists_odds_bet365')) {
function get_player_points_and_assists_odds_bet365($game_id){

    // ✅ sécurités minimales
    if (empty($game_id)) return [];
    if (!function_exists('tp_http_json') || !function_exists('add_query_arg')) return [];

    $api_url = add_query_arg(['game'=>$game_id], 'https://v1.basketball.api-sports.io/odds');
    $ckey    = "tp_odds_".$game_id;

    $data = tp_http_json($api_url, ['timeout'=>20], $ckey, 60); // 60s : cotes fraîches
    if (empty($data) || empty($data['response']) || !is_array($data['response'])) return [];

    $rows = [];
    foreach ($data['response'] as $match){
        $mid = $match['game']['id'] ?? ($match['game_id'] ?? null);
        if ($mid != $game_id) continue;

        $bookmakers = $match['bookmakers'] ?? [];
        if (!is_array($bookmakers)) continue;

        foreach ($bookmakers as $book){
            if (($book['id'] ?? null) != 4) continue; // Bet365 only

            $bets = $book['bets'] ?? [];
            if (!is_array($bets)) continue;

            foreach ($bets as $bet){
                $bet_id   = $bet['id'] ?? null;
                $bet_name = $bet['name'] ?? '';

                $values = $bet['values'] ?? [];
                if (!is_array($values)) continue;

                foreach ($values as $val){
                    $rows[] = [
                        'bookmaker_id' => 4,
                        'bookmaker'    => 'Bet365',
                        'bet_id'       => $bet_id,
                        'bet'          => $bet_name,
                        'selection'    => $val['selection'] ?? ($val['value'] ?? ''),
                        'odd'          => isset($val['odd']) ? (string)$val['odd'] : null,
                        'handicap'     => $val['handicap'] ?? null,
                        'line'         => $val['line'] ?? ($val['selection'] ?? null),
                        'last_update'  => $val['last_update'] ?? null,
                    ];
                }
            }
        }
    }

    return $rows;
}}


/** FUN + cotes (tableau unique) */
if (!function_exists('tp_render_fun_with_odds_table')) {
function tp_render_fun_with_odds_table(array $checks, array $odds){

    if (empty($checks)) return;

    // Sécurité : si les helpers odds ne sont pas là, on sort sans planter
    if (!function_exists('tp_build_milestones_indexes') || !function_exists('tp_find_milestone_odd')) {
        echo "<!-- tp_render_fun_with_odds_table: helpers odds manquants -->";
        return;
    }

    $idxAll = tp_build_milestones_indexes($odds);
    $prod   = ['points'=>1.0,'rebounds'=>1.0,'assists'=>1.0];
    $cnt    = ['points'=>0,'rebounds'=>0,'assists'=>0];

    // Bouton stats (si dispo)
    $btnStats = function($player){
        return function_exists('tpb_player_stats_button')
            ? ' '.tpb_player_stats_button((string)$player)
            : '';
    };

    echo "<details class='tpb-collapse tpb-collapse--fun'>";
    echo "<summary><span class='basket-title'>🎯🔮 Ma sélection FUN + Cotes</span><span class='tpb-collapse-arrow'>▾</span></summary>";

    echo "<div class='card'><table class='table compact'><thead><tr>
            <th>Type</th><th>Sélection</th><th>Palier</th><th>Cote</th>
          </tr></thead><tbody>";

    $renderRow = function($label,$player,$threshold,$res) use (&$prod,&$cnt,$btnStats){
        $pal = $res ? (is_numeric($res['line']) ? (float)$res['line'] : $res['line']) : '—';
        $odd = $res ? $res['odd'] : '—';

        if ($res && in_array($label,['Points','Rebonds','Passes'],true)){
            $k = $label==='Points'?'points':($label==='Rebonds'?'rebounds':'assists');
            $prod[$k] *= (float)$res['odd'];
            $cnt[$k]++;
        }

        echo "<tr>
                <td>".esc_html($label)."</td>
                <td>".esc_html($player).$btnStats($player)." ≥ ".esc_html($threshold)."</td>
                <td class='num'>{$pal}</td>
                <td class='num'>{$odd}</td>
              </tr>";
    };

    foreach (($checks['points'] ?? []) as $c){
        $renderRow(
            'Points',
            $c['player'],
            $c['threshold'],
            tp_find_milestone_odd($idxAll['points'] ?? [], $c['player'], (float)$c['threshold'])
        );
    }
    foreach (($checks['rebounds'] ?? []) as $c){
        $renderRow(
            'Rebonds',
            $c['player'],
            $c['threshold'],
            tp_find_milestone_odd($idxAll['rebounds'] ?? [], $c['player'], (float)$c['threshold'])
        );
    }
    foreach (($checks['assists'] ?? []) as $c){
        $renderRow(
            'Passes',
            $c['player'],
            $c['threshold'],
            tp_find_milestone_odd($idxAll['assists'] ?? [], $c['player'], (float)$c['threshold'])
        );
    }

    // 3pts : affichage seulement (on ne multiplie pas)
    $findThreeOver = function(string $player, float $threshold) use($odds){
        $name  = strtolower($player);
        $parts = preg_split('/\s+/', $name);
        $first = $parts[0] ?? '';
        $last  = $parts[count($parts)-1] ?? '';
        $best  = null;

        foreach ($odds as $r){
            if (($r['bookmaker_id'] ?? null) != 4) continue;

            $bet = strtolower($r['bet'] ?? '');
            if (strpos($bet,'three')===false
             && strpos($bet,'3pt')===false
             && strpos($bet,'3-pointers')===false
             && strpos($bet,'3 pointers')===false
             && strpos($bet,'3 points')===false) continue;

            $sel  = strtolower($r['selection'] ?? '');
            $isOv = (strpos($sel,'over')!==false || strpos($sel,'plus')!==false || preg_match('/\bo\b/',$sel));
            if(!$isOv) continue;

            $match = (strpos($sel,$last)!==false)
                  || ($first && preg_match('/\b'.preg_quote(substr($first,0,1),'/').'\.\s*'.preg_quote($last,'/').'\b/i',$sel));
            if(!$match) continue;

            $line = null;
            if (isset($r['line']) && is_numeric($r['line'])) $line=(float)$r['line'];
            elseif (isset($r['handicap']) && is_numeric($r['handicap'])) $line=(float)$r['handicap'];
            elseif (preg_match('/-?\d+(?:\.\d+)?/',$sel,$m)) $line=(float)$m[0];
            if ($line===null) continue;

            $odd = isset($r['odd']) ? (float)$r['odd'] : null;
            if(!$odd) continue;

            $score = [abs($line-$threshold), -$odd];
            if($best===null || $score < $best['score']){
                $best=['line'=>$line,'odd'=>$odd,'score'=>$score];
            }
        }
        return $best;
    };

    foreach (($checks['threes'] ?? []) as $c){
        $res = $findThreeOver((string)$c['player'], (float)$c['threshold']);
        $pal = $res ? (is_numeric($res['line']) ? $res['line'] : '—') : '—';
        $odd = $res ? $res['odd'] : '—';

        echo "<tr>
                <td>3Pts</td>
                <td>".esc_html($c['player']).$btnStats($c['player'])." ≥ ".esc_html($c['threshold'])."</td>
                <td class='num'>{$pal}</td>
                <td class='num'>{$odd}</td>
              </tr>";
    }

    echo "</tbody>";

    // Cote globale FUN (Produit Points x Rebonds x Passes)
    $overall = ($cnt['points']+$cnt['rebounds']+$cnt['assists']>0)
        ? number_format(($prod['points'] ?: 1)*($prod['rebounds'] ?: 1)*($prod['assists'] ?: 1),2,'.','')
        : '—';

    echo "<tfoot><tr><td colspan='4' style='text-align:right; padding:10px;'>
            <span class='chip chip--brand'>Cote Globale FUN = {$overall}</span>
          </td></tr></tfoot>";
    echo "</table></div>";

    echo "</details>";
}}

/* ============================================================
   1) Helper : ligue sélectionnée (NBA par défaut)
   ============================================================ */

if (!function_exists('tpb_league_config')) {
  function tpb_league_config(){
    // ⚠️ Ajuste les saisons si besoin
    return [
      12  => ['name' => 'NBA',       'season' => '2025-2026'],
      13  => ['name' => 'WNBA',      'season' => '2025'],       // ex
      1   => ['name' => 'NBL',       'season' => '2025-2026'],  // ex
      120 => ['name' => 'EuroLeague','season' => '2025-2026'],  // ex
    ];
  }
}

if (!function_exists('tpb_get_selected_league_season')) {
  function tpb_get_selected_league_season(array $atts, array $cfg, int $defaultLeague = 12){
    // priorité : shortcode > URL > défaut
    $lid = ($atts['league'] ?? '') !== '' ? (int)$atts['league'] : (isset($_GET['tpb_league']) ? (int)$_GET['tpb_league'] : $defaultLeague);
    if (!isset($cfg[$lid])) $lid = $defaultLeague;

    $season = ($atts['season'] ?? '') !== '' ? (string)$atts['season'] : (isset($_GET['tpb_season']) ? (string)$_GET['tpb_season'] : (string)($cfg[$lid]['season'] ?? '2025-2026'));

    return [$lid, $season];
  }
}

if (!function_exists('tpb_render_spot_players')) {
  function tpb_render_spot_players($atts = []){
    $atts = shortcode_atts([
      'league' => '',   // ex: 12, 13, 1, 120
      'season' => '',   // ex: 2025-2026
    ], $atts, 'tp_basket_spot');

$cfg = tpb_league_config();
[$lid, $season] = tpb_get_selected_league_season($atts, $cfg, 12);
$league_id = (string)$lid;

    ob_start();

    // ✅ ton rendu normal
    tp_print_basket_css_once();
    tp_print_basket_js_once();

    $league_id = (string)$lid;

    $matches = get_nba_matches_on_next_2_days($league_id, $season);

    echo "<div class='tp-basket' data-league='".esc_attr($league_id)."'>";

    if (!empty($matches)) {

      // ✅ barre de filtres
      echo "<div class='tpb-filters'>";

      // filtre ligue (reload)
      echo "<label>Ligue :
              <select class='tpb-filter-league'>";
      foreach ($cfg as $id => $c){
        $isSel = ((string)$id === (string)$league_id) ? " selected" : "";
        echo "<option value='".esc_attr($id)."' data-season='".esc_attr($c['season'] ?? '')."'{$isSel}>"
          . esc_html($c['name'] ?? ('League '.$id)) . "</option>";
      }
      echo "  </select>
            </label>";

      echo "  <label>Match :
                <input type='text' class='tpb-filter-teams' placeholder='Ex : Rockets, Jazz…'>
              </label>
              <label>Statut :
                <select class='tpb-filter-status'>
                  <option value='all'>Tous</option>
                  <option value='upcoming'>À venir</option>
                  <option value='live'>Live</option>
                  <option value='finished'>Terminés</option>
                </select>
              </label>
            </div>";

    // Compteurs globaux NBA Spot – pour stats analyseurs (matches "À venir" uniquement)
    if (!defined('TP_NBA_SPOT_STATS_TTL')) {
        define('TP_NBA_SPOT_STATS_TTL', 30 * MINUTE_IN_SECONDS);
    }

    $tp_nba_nb_duo_1er_choix   = 0; // nombre de "Ma sélection Duo Marqueurs 1er choix"
    $tp_nba_nb_fun_my_match    = 0; // nombre de FUN My Match (volontairement = nb de Duo 1er choix)
    $tp_nba_nb_best_scorer     = 0; // nombre de "Meilleur Marqueur"
    $tp_nba_nb_best_rebounder  = 0; // nombre de "Meilleur Rebondeur"
    $tp_nba_nb_best_assister   = 0; // nombre de "Meilleur Passeur"

    $tp_nba_high_conf_global     = [];
    $tp_nba_high_conf_threshold  = 87; // seuil picks très confiants
    $tp_nba_match_fire_threshold = 87; // seuil pour feu vert
    $tp_nba_match_fire_min_spots = 3;  // nb minimum de spots

    foreach ($matches as $match) {

    $tz = new DateTimeZone('Europe/Paris');
$dt = (new DateTime("@".$match['date_ts']))->setTimezone($tz);
$date = $dt->format('d M Y - H:i');
        $league  = $match['league']['name'];
        $country = $match['country']['name'];

        $home  = $match['teams']['home'];
        $away  = $match['teams']['away'];
        $statusBad = tp_status_badge($match['status'] ?? []);

        // Tag de statut pour le filtre
        if (tp_is_not_started($match['status'] ?? [])) {
            $status_tag = 'upcoming';
        } elseif (tp_is_finished($match['status'] ?? [])) {
            $status_tag = 'finished';
        } else {
            $status_tag = 'live';
        }

        $teams_label = trim(($home['name'] ?? '').' vs '.($away['name'] ?? ''));

        echo "<div class='tpb-match-block' data-status='".esc_attr($status_tag)."' data-teams='".esc_attr($teams_label)."'>";

        [$hs,$as] = tp_extract_final_scores($match['scores'] ?? null);
        $hs_disp  = ($hs!==null)?intval($hs):'—';
        $as_disp  = ($as!==null)?intval($as):'—';

        echo "<div class='match'>";
        echo   "<div class='match__header'>
                  <div class='match__meta'>".esc_html($country)." • ".esc_html($league)." • ".esc_html($date)."</div>
                  <div>{$statusBad}</div>
                </div>";
        echo   "<div class='teamline'>";
        echo     "<div class='team'>
                    <img src='".esc_url($home['logo'])."' alt='".esc_attr($home['name'])."'>
                    <div class='team__name'>".esc_html($home['name'])."</div>
                  </div>";
        echo     "<div class='tpb-score'>
                    <span class='tpb-score__box'>{$hs_disp}</span>
                    <span class='tpb-score__sep'>—</span>
                    <span class='tpb-score__box'>{$as_disp}</span>
                  </div>";
        echo     "<div class='team'>
                    <img src='".esc_url($away['logo'])."' alt='".esc_attr($away['name'])."'>
                    <div class='team__name'>".esc_html($away['name'])."</div>
                  </div>";
        echo   "</div>";
        echo "</div>";

        $team1_id  = $home['id'];
        $team2_id  = $away['id'];
        $game_id   = $match['id'];
        $before_ts = isset($match['date_ts']) ? (int)$match['date_ts'] : (int)strtotime($match['date']);

$isNS = tp_is_not_started($match['status'] ?? []);
$isFT = tp_is_finished($match['status'] ?? []);

$box  = $isFT ? tp_fetch_boxscore_for_game($game_id) : [];
$odds = $isNS ? get_player_points_and_assists_odds_bet365($game_id) : [];


        $cached  = tp_pre_cache_get($game_id);
        $compute = function() use ($team1_id,$team2_id,$league_id,$season,$before_ts,$home,$away){
            return get_combined_player_predictions($team1_id,$team2_id,$league_id,$season,$before_ts,$home,$away);
        };

        /* ➜ TTL dynamique : garde le snapshot jusqu'à 24 h après le tip-off (min 24 h) */
        $ttl = max(DAY_IN_SECONDS, ($before_ts + 1*DAY_IN_SECONDS) - time());

        if (tp_is_not_started($match['status'] ?? [])) {
            // Avant match : calcule (imprime) + fige
            $player_predictions = $compute();
            tp_pre_cache_set($game_id, $player_predictions, $ttl);
        } else {
            // Live/Terminé : affiche le snapshot si présent, sinon calcule puis fige
            if (is_array($cached)) {
                $player_predictions = $cached;
             tp_render_prediction_table_from_snapshot(
  $player_predictions['combined'] ?? [],
  $player_predictions['checks'] ?? []
);
            } else {
                $player_predictions = $compute(); // imprime
                tp_pre_cache_set($game_id, $player_predictions, $ttl);
            }
        }

        $checks = $player_predictions['checks'] ?? [];

        // 💡 Match feu vert (juste après la table de prédiction)
        $high_conf_count = 0;
        foreach (['points','rebounds','assists','threes'] as $mk) {
            foreach (($checks[$mk] ?? []) as $c) {
                $conf = $c['confidence'] ?? null;
                if ($conf !== null && $conf >= $tp_nba_match_fire_threshold) {
                    $high_conf_count++;
                }
            }
        }

        if (tp_is_not_started($match['status'] ?? []) && $high_conf_count >= $tp_nba_match_fire_min_spots) {
            echo "<div class='tpb-match-flag'>💡 Match feu vert : "
               . esc_html($high_conf_count)." spots ≥ ".esc_html($tp_nba_match_fire_threshold)."%</div>";
        }

               // 🔥 Alimente le tableau global : uniquement les matchs "À venir"
        $match_label = ($home['name'] ?? 'Home')." vs ".($away['name'] ?? 'Away');
        $match_date  = $date;

        // On ne garde les snipers que pour les matchs non commencés
        if (tp_is_not_started($match['status'] ?? [])) {

            $markets_labels = [
                'points'   => 'Points',
                'rebounds' => 'Rebonds',
                'assists'  => 'Passes',
                'threes'   => '3Pts',
            ];

            foreach ($markets_labels as $key => $label_type) {
                foreach (($checks[$key] ?? []) as $c) {
                    $conf = $c['confidence'] ?? null;
                    if ($conf === null || $conf < $tp_nba_high_conf_threshold) {
                        continue;
                    }

                    $tp_nba_high_conf_global[] = [
                        'market'     => $label_type,
                        'player'     => $c['player'],
                        'threshold'  => (float)$c['threshold'],
                        'confidence' => (int)$conf,
                        'match'      => $match_label,
                        'date'       => $match_date,
                    ];
                }
            }
        }

        // 🔢 Comptage pour les matchs "À venir" uniquement
        if (tp_is_not_started($match['status'] ?? [])) {

            // 1) Ma sélection Duo Marqueurs 1er choix
            $has_duo_1er = false;
            if (!empty($checks['duos'])) {
                foreach ($checks['duos'] as $duo_sel) {
                    $rank = $duo_sel['rank'] ?? '';
                    if ($rank && stripos($rank, '1er choix') !== false) {
                        $has_duo_1er = true;
                        break;
                    }
                }
            }

            if ($has_duo_1er) {
                $tp_nba_nb_duo_1er_choix++;
                $tp_nba_nb_fun_my_match++;
            }

            // 2) Meilleur Marqueur / Rebondeur / Passeur
            if (!empty($checks['awards']['scorer'])) {
                $tp_nba_nb_best_scorer++;
            }
            if (!empty($checks['awards']['rebounder'])) {
                $tp_nba_nb_best_rebounder++;
            }
            if (!empty($checks['awards']['assister'])) {
                $tp_nba_nb_best_assister++;
            }
        }

      // FUN + cotes : uniquement avant match (sinon odds inutiles)
if ($isNS) {
  tp_render_fun_with_odds_table($checks, $odds);
}

// Validation : uniquement si terminé (sinon ça sort des KO absurdes)
if ($isFT) {
  tp_render_validation_table($checks, $box, $home['name'] ?? '', $away['name'] ?? '');
} elseif (!$isNS) {
  echo "<div class='card compact' style='margin-top:8px;'>
          <span class='chip chip--neutral'>Match en cours : validation à la fin</span>
        </div>";
}
        echo "</div>"; // .tpb-match-block
    } // fin foreach

    // 💣 Tableau global Gros FUN My match (tous les matchs à venir)
    if (!empty($tp_nba_high_conf_global)) {
        echo "<h4 class='basket-title'>💣 Gros FUN My match – Sélections ≥ {$tp_nba_high_conf_threshold}% (tous les matchs à venir)</h4>";
        echo "<div class='card'><table class='table compact'><thead><tr>
                <th>Match</th>
                <th>Date</th>
                <th>Type</th>
                <th>Joueur</th>
                <th>Palier</th>
                <th>Confiance</th>
              </tr></thead><tbody>";

        foreach ($tp_nba_high_conf_global as $row) {
            echo "<tr>
                    <td>".esc_html($row['match'])."</td>
                    <td>".esc_html($row['date'])."</td>
                    <td>".esc_html($row['market'])."</td>
                    <td>".esc_html($row['player'])."</td>
                    <td class='num'>≥ ".esc_html($row['threshold'])."</td>
                    <td class='num'>".esc_html($row['confidence'])."%</td>
                  </tr>";
        }

        echo "</tbody></table></div>";
    }

    // ⭐ Top snipers de la nuit
    if (!empty($tp_nba_high_conf_global)) {
        $by_market = [];
        foreach ($tp_nba_high_conf_global as $row) {
            $m = $row['market']; // 'Points','Rebonds','Passes','3Pts'
            if (!isset($by_market[$m])) $by_market[$m] = [];
            $by_market[$m][] = $row;
        }

        $labels_top = [
            'Points'  => 'Top Snipers Points',
            'Rebonds' => 'Top Snipers Rebonds',
            'Passes'  => 'Top Snipers Passes',
            '3Pts'    => 'Top Snipers 3 pts',
        ];

        echo "<h4 class='basket-title'>⭐ Top snipers de la nuit</h4>";
        echo "<div class='card'><div class='tpb-toplists'>";

        foreach ($labels_top as $market_key => $title) {
            if (empty($by_market[$market_key])) continue;

            $list = $by_market[$market_key];

            usort($list, function($a,$b){
                if ($b['confidence'] === $a['confidence']) {
                    return $b['threshold'] <=> $a['threshold'];
                }
                return $b['confidence'] <=> $a['confidence'];
            });

            $list = array_slice($list, 0, 5);

            echo "<h5>⭐ ".esc_html($title)."</h5><ul class='tpb-toplist'>";
            foreach ($list as $row) {
                $chip = tp_nba_conf_chip($row['confidence']);
                echo "<li>"
                    .esc_html($row['player'])." ≥ ".esc_html($row['threshold'])
                    ."{$chip}<span class='tpb-toplist-match'> (".esc_html($row['match']).")</span></li>";
            }
            echo "</ul>";
        }

        echo "</div></div>";
    }

  $stats_prefix = "tp_spot_basket_{$league_id}_";
$ttl_stats = defined('TP_NBA_SPOT_STATS_TTL') ? TP_NBA_SPOT_STATS_TTL : 30 * MINUTE_IN_SECONDS;

set_transient($stats_prefix.'duo_1er',        (int)$tp_nba_nb_duo_1er_choix,  $ttl_stats);
set_transient($stats_prefix.'fun_mymatch',    (int)$tp_nba_nb_fun_my_match,   $ttl_stats);
set_transient($stats_prefix.'best_scorer',    (int)$tp_nba_nb_best_scorer,    $ttl_stats);
set_transient($stats_prefix.'best_rebounder', (int)$tp_nba_nb_best_rebounder, $ttl_stats);
set_transient($stats_prefix.'best_assister',  (int)$tp_nba_nb_best_assister,  $ttl_stats);

// Compat NBA (si tu veux garder tes anciens dashboards inchangés)
if ($league_id === '12'){
  set_transient('tp_nb_nba_duo_marqueurs_1er',  (int)$tp_nba_nb_duo_1er_choix,  $ttl_stats);
  set_transient('tp_nb_nba_fun_mymatch',        (int)$tp_nba_nb_fun_my_match,   $ttl_stats);
  set_transient('tp_nb_nba_best_scorer',        (int)$tp_nba_nb_best_scorer,    $ttl_stats);
  set_transient('tp_nb_nba_best_rebounder',     (int)$tp_nba_nb_best_rebounder, $ttl_stats);
  set_transient('tp_nb_nba_best_assister',      (int)$tp_nba_nb_best_assister,  $ttl_stats);
}

    } else {
      echo "<p>Aucun match trouvé pour les 2 prochains jours.</p>";
    }

    echo "</div>"; // .tp-basket

    return ob_get_clean();
  }
}
if (!function_exists('tpb_register_spot_players_shortcode')) {
  function tpb_register_spot_players_shortcode(){
    add_shortcode('tp_basket_spot', function($atts){
      return tpb_render_spot_players($atts);
    });
  }
  add_action('init', 'tpb_register_spot_players_shortcode');
}

4ème Analyse

<?php


/**
 * ============================================================
 * ✅ TP MATCH ANALYZER — LITE + ODDS + HIGH CONF BADGES
 * Shortcode : [tp_match_analyzer_lite]
 * - Liste matchs + filtres
 * - ✅ BADGES "Confiances élevées" DIRECT sur la carte (style image)
 * - ✅ Bouton "Ouvrir l’analyse" (panel inline)
 * - ✅ ODDS via CORE (bookmaker primary + fallback)
 * ============================================================
 */
if (!defined('ABSPATH')) exit;

/* ✅ Toujours enregistrer le shortcode (ordre snippets variable) */
add_action('init', function(){
  add_shortcode('tp_match_analyzer_lite', 'tp_match_analyzer_lite_shortcode');
}, 20);

/* =========================
   Cache helpers (core cache si dispo, sinon transients)
   ========================= */
if (!function_exists('tp_ma_lite_ck')) {
  function tp_ma_lite_ck($raw){ return 'tp_ma_lite_' . md5($raw); }
}
if (!function_exists('tp_ma_lite_cache_get')) {
  function tp_ma_lite_cache_get($raw_key){
    if (function_exists('core_base_foot_cache_get')) {
      $v = core_base_foot_cache_get($raw_key);
      if ($v !== null && $v !== false) return $v;
    }
    return get_transient(tp_ma_lite_ck($raw_key));
  }
}
if (!function_exists('tp_ma_lite_cache_set')) {
  function tp_ma_lite_cache_set($raw_key, $value, $ttl){
    if (function_exists('core_base_foot_cache_set')) {
      core_base_foot_cache_set($raw_key, $value, (int)$ttl);
      return;
    }
    set_transient(tp_ma_lite_ck($raw_key), $value, (int)$ttl);
  }
}
if (!function_exists('tp_ma_lite_cache_del')) {
  function tp_ma_lite_cache_del($raw_key){
    // Si le CORE a un delete, on l'utilise
    if (function_exists('core_base_foot_cache_del')) {
      try { core_base_foot_cache_del($raw_key); } catch (Throwable $e) {}
    } elseif (function_exists('core_base_foot_cache_set')) {
      // Fallback : on écrase en "null" TTL 1s (cache_get renverra null => miss)
      try { core_base_foot_cache_set($raw_key, null, 1); } catch (Throwable $e) {}
    }
    // Toujours purger le transient fallback
    delete_transient(tp_ma_lite_ck($raw_key));
  }
}

/* =========================
   ✅ GARDE-FOU CORE pour AJAX (évite “Erreur critique” si CORE absent)
   ========================= */
if (!function_exists('tp_ma_lite_require_core')) {
  function tp_ma_lite_require_core(){
    $need = [
      'core_base_foot_build_team_match_stats',
      'core_base_foot_calc_potential_goals',
      'core_base_foot_predict_double_chance_ppm',
      'core_base_foot_predict_win_ppm',
      'core_base_foot_predict_total_goals',
      'core_base_foot_predict_btts_yesno',
      'core_base_foot_predict_draw_xg',
      'core_base_foot_predict_team_to_score',
      'core_base_foot_get_odds_map',
    ];
    foreach ($need as $fn){
      if (!function_exists($fn)) {
        wp_send_json_error(['message'=>"CORE BASE FOOT indisponible ($fn)"]);
      }
    }
    if (!defined('CORE_BASE_FOOT_SEASON')) {
      wp_send_json_error(['message'=>"CORE_BASE_FOOT_SEASON non défini"]);
    }
  }
}

/* =========================
   Translate status (mini)
   ========================= */
if (!function_exists('tp_ma_lite_translate_status_short')) {
  function tp_ma_lite_translate_status_short($short){
    $short = (string)$short;
    if (function_exists('core_base_foot_translate_status')) {
      try {
        $tr = core_base_foot_translate_status($short, '');
        if (is_array($tr) && !empty($tr['short_fr'])) return (string)$tr['short_fr'];
      } catch (Throwable $e) {}
    }
    $map = [
      'NS'=>'À venir','TBD'=>'À définir','PST'=>'Reporté','CANC'=>'Annulé','SUSP'=>'Suspendu',
      'INT'=>'Interrompu','LIVE'=>'En direct','1H'=>'1re MT','HT'=>'Mi-temps','2H'=>'2e MT',
      'FT'=>'Terminé','AET'=>'Terminé (a.p.)','PEN'=>'TAB'
    ];
    return $map[$short] ?? ($short ?: '—');
  }
}

/* =========================
   Exact score from xG (simple)
   ========================= */
if (!function_exists('tp_ma_lite_exact_score_from_xg')) {
  function tp_ma_lite_exact_score_from_xg($pot): array {
    if (!is_array($pot) || !isset($pot['home_xg'], $pot['away_xg'])) return ['ok'=>false];

    $hx = (float)$pot['home_xg'];
    $ax = (float)$pot['away_xg'];

    $draw_thr = 0.20;

    $hs = (int) round($hx, 0, PHP_ROUND_HALF_UP);
    $as = (int) round($ax, 0, PHP_ROUND_HALF_UP);

    if (abs($hx - $ax) <= $draw_thr) {
      $avg = ($hx + $ax) / 2;
      $hs  = (int) round($avg, 0, PHP_ROUND_HALF_UP);
      $as  = $hs;
    }

    $hs = max(0, min(7, $hs));
    $as = max(0, min(7, $as));

    $dist = abs($hx - $hs) + abs($ax - $as);
    $pct  = (int) round(max(45, min(85, 85 - ($dist * 20))));
    $lvl  = ($pct >= 80) ? 'Forte' : (($pct >= 61) ? 'Moyenne' : 'Faible');

    return [
      'ok' => true,
      'label' => $hs.' - '.$as,
      'home_score' => $hs,
      'away_score' => $as,
      'home_xg' => round($hx, 2),
      'away_xg' => round($ax, 2),
      'confidence_pct' => $pct,
      'confidence_level' => $lvl,
      'vals' => ['dist'=>round($dist,2), 'draw_thr'=>$draw_thr],
    ];
  }
}
/* =========================
   ✅ Ensure exact score is included in score_multichance (Top 3)
   ========================= */
if (!function_exists('tp_ma_lite_norm_score_label')) {
  function tp_ma_lite_norm_score_label($s): string {
    $s = strtolower(trim((string)$s));
    // garder chiffres + séparateurs, nettoyer
    $s = preg_replace('~[^0-9:\-\s]+~', '', $s);
    $s = str_replace([' ', ':'], ['', '-'], $s);      // "1 - 1" => "1-1", "1:1" => "1-1"
    $s = preg_replace('~-{2,}~', '-', $s);
    return trim($s, '-');
  }
}

if (!function_exists('tp_ma_lite_score_mc_ensure_exact')) {
  /**
   * Force l'inclusion du score exact dans les TOP3 multi-chances
   * - si déjà présent => inchangé
   * - sinon => exact + top2 autres
   */
  function tp_ma_lite_score_mc_ensure_exact($sm, $es) {
    if (!is_array($sm) || empty($sm['ok']) || empty($sm['scores']) || !is_array($sm['scores'])) return $sm;
    if (!is_array($es) || empty($es['ok']) || empty($es['label'])) return $sm;

    $exactNorm = tp_ma_lite_norm_score_label($es['label']);
    if (!$exactNorm) return $sm;

    // Vérifier si déjà présent (en comparant normalisé)
    foreach ($sm['scores'] as $row) {
      $sc = (string)($row['score'] ?? '');
      if ($sc && tp_ma_lite_norm_score_label($sc) === $exactNorm) {
        return $sm; // déjà dedans => rien à faire
      }
    }

    // Construire Top3 = exact + meilleurs autres (sans doublon)
    $new = [];
    $new[] = ['score' => $exactNorm]; // format "1-1" cohérent avec multi-chance

    foreach ($sm['scores'] as $row) {
      $sc = (string)($row['score'] ?? '');
      if (!$sc) continue;
      if (tp_ma_lite_norm_score_label($sc) === $exactNorm) continue; // éviter doublon
      $new[] = $row; // garder le row original (prob/weight si présent)
    }

    $sm['scores'] = array_slice($new, 0, 3);
    return $sm;
  }
}

/* =========================
   ODDS helpers (robustes)
   ========================= */
if (!function_exists('tp_ma_lite_odds_fmt_sel_for_total_goals')) {
  function tp_ma_lite_odds_fmt_sel_for_total_goals(string $pred, float $line): array {
    $line = (float)$line;
    $s = (stripos($pred, 'over') === 0) ? 'Over ' : 'Under ';
    return [
      $s . $line,
      $s . $line . ' Goals',
      $s . number_format($line, 1, '.', ''),
    ];
  }
}
if (!function_exists('tp_ma_lite_is_half_market')) {
  function tp_ma_lite_is_half_market(string $name): bool {
    $n = strtolower(trim($name));
    // Exclure HT / 1st half / mi-temps (mais PAS "First Team To Score")
    return (bool) preg_match('~\b(ht|half|halftime|1st)\b|mi-temps|1re\s*mt|1ère\s*mt~i', $n);
  }
}

if (!function_exists('tp_ma_lite_odds_get_any_strict_ft')) {
  /**
   * Matching strict (FT) sur une map d'odds.
   * Supporte 2 formats fréquents :
   *  A) $map["Total - Home"]["Over 0.5"] = "1.37"
   *     OU $map["Total - Home"] = [ ["value"=>"Over 0.5","odd"=>"1.37"], ... ]  ✅ FIX
   *  B) $map = [ ["name"=>"Total - Home","id"=>16,"values"=>[["value"=>"Over 0.5","odd"=>"1.37"],...]], ... ]
   */
  function tp_ma_lite_odds_get_any_strict_ft($map, array $marketNames, array $selNames) {
    if (!is_array($map)) return null;

    $norm = function($s){
      $s = strtolower(trim((string)$s));
      $s = preg_replace('~\s+~', ' ', $s);
      return $s;
    };

    $wantedMarkets = array_values(array_unique(array_filter(array_map($norm, $marketNames))));
    $wantedSels    = array_values(array_unique(array_filter(array_map($norm, $selNames))));

    // -------- Format A : associative (market => selections)
    $keysAreStrings = true;
    foreach (array_keys($map) as $k) { if (!is_string($k)) { $keysAreStrings = false; break; } }

    if ($keysAreStrings) {
      foreach ($map as $mName => $sels) {
        if (!is_array($sels)) continue;
        if (tp_ma_lite_is_half_market((string)$mName)) continue;

        $mn = $norm($mName);

        // match market (exact d'abord, puis contains)
        $okMarket = in_array($mn, $wantedMarkets, true);
        if (!$okMarket) {
          foreach ($wantedMarkets as $wm) {
            if ($wm && strpos($mn, $wm) !== false) { $okMarket = true; break; }
          }
        }
        if (!$okMarket) continue;

        // selections
        foreach ($sels as $sName => $odd) {

          // ✅ FIX : si $sels est une LISTE numérique d'objets (value/odd)
          if (is_int($sName) && is_array($odd)) {
            $sName2 = (string)($odd['value'] ?? $odd['name'] ?? '');
            $odd2   = $odd['odd'] ?? ($odd['price'] ?? null);
            $sn2    = $norm($sName2);

            if ($sName2 && in_array($sn2, $wantedSels, true)) {
              if (is_numeric($odd2)) return (float)$odd2;
              if (is_string($odd2)) {
                $x = str_replace(',', '.', preg_replace('~[^0-9,\.]~', '', $odd2));
                return is_numeric($x) ? (float)$x : null;
              }
            }
            continue;
          }

          // ✅ cas assoc classique
          $sn = $norm($sName);
          if (in_array($sn, $wantedSels, true)) {
            if (is_numeric($odd)) return (float)$odd;
            if (is_string($odd)) {
              $x = str_replace(',', '.', preg_replace('~[^0-9,\.]~', '', $odd));
              return is_numeric($x) ? (float)$x : null;
            }
            if (is_array($odd) && isset($odd['odd'])) {
              $x = str_replace(',', '.', preg_replace('~[^0-9,\.]~', '', (string)$odd['odd']));
              return is_numeric($x) ? (float)$x : null;
            }
          }
        }
      }
      return null;
    }

    // -------- Format B : liste de markets objects
    foreach ($map as $m) {
      if (!is_array($m)) continue;
      $mName = (string)($m['name'] ?? '');
      if (!$mName) continue;
      if (tp_ma_lite_is_half_market($mName)) continue;

      $mn = $norm($mName);

      $okMarket = in_array($mn, $wantedMarkets, true);
      if (!$okMarket) {
        foreach ($wantedMarkets as $wm) {
          if ($wm && strpos($mn, $wm) !== false) { $okMarket = true; break; }
        }
      }
      if (!$okMarket) continue;

      $vals = $m['values'] ?? null;
      if (!is_array($vals)) continue;

      foreach ($vals as $v) {
        if (!is_array($v)) continue;
        $sName = (string)($v['value'] ?? '');
        $odd   = $v['odd'] ?? null;
        $sn = $norm($sName);

        if (in_array($sn, $wantedSels, true)) {
          if (is_numeric($odd)) return (float)$odd;
          if (is_string($odd)) {
            $x = str_replace(',', '.', preg_replace('~[^0-9,\.]~', '', $odd));
            return is_numeric($x) ? (float)$x : null;
          }
        }
      }
    }

    return null;
  }
}

if (!function_exists('tp_ma_lite_get_needed_odds')) {
  function tp_ma_lite_get_needed_odds(int $fixture_id, array $preds, array $ctx = []): array {

    // ✅ helper local: normalise n'importe quelle cote (float|string|array) -> float|null
    $odd_value = function($o){
      if ($o === null) return null;

      if (is_numeric($o)) return (float)$o;

      if (is_string($o)) {
        $x = str_replace(',', '.', preg_replace('~[^0-9,\.]~', '', $o));
        return is_numeric($x) ? (float)$x : null;
      }

      if (is_array($o)) {
        foreach (['odd','value','price','o'] as $k) {
          if (!isset($o[$k])) continue;
          $x = $o[$k];
          if (is_numeric($x)) return (float)$x;
          if (is_string($x)) {
            $y = str_replace(',', '.', preg_replace('~[^0-9,\.]~', '', $x));
            return is_numeric($y) ? (float)$y : null;
          }
        }
      }
      return null;
    };

    $out = [
      'ok' => false,
      'bookmaker_id' => null,
      'odds' => [
        'double_chance'     => null,
        'win_1x2'           => null,
        'draw_1x2'          => null,
        'total_goals'       => null,
        'btts'              => null,
        'exact_score'       => null,
        'tts_home'          => null,
        'tts_away'          => null,
        'score_multichance' => null, // array aligné top3
      ],
      '_error' => null,
    ];

    if ($fixture_id <= 0) { $out['_error'] = 'fixture_id invalide'; return $out; }
    if (!function_exists('core_base_foot_get_odds_map')) { $out['_error'] = 'core_base_foot_get_odds_map indisponible'; return $out; }

    $force = !empty($ctx['force']);
    $ttl = $force ? 30 : (defined('CORE_BASE_FOOT_CACHE_15M') ? (int)CORE_BASE_FOOT_CACHE_15M : 900);

    $oddsRes = core_base_foot_get_odds_map($fixture_id, ['ttl' => $ttl]);

    if (!is_array($oddsRes) || empty($oddsRes['ok']) || empty($oddsRes['map']) || !is_array($oddsRes['map'])) {
      $out['_error'] = is_array($oddsRes) ? (string)($oddsRes['_error'] ?? 'odds map empty') : 'odds map invalid';
      return $out;
    }

    $map = $oddsRes['map'];
    $out['bookmaker_id'] = !empty($oddsRes['bookmaker_id']) ? (int)$oddsRes['bookmaker_id'] : null;

    // --------------------
    // DC
    // --------------------
    $dc = is_array($preds['double_chance_ppm'] ?? null) ? $preds['double_chance_ppm'] : null;
    if ($dc && !empty($dc['ok']) && !empty($dc['prediction']) && function_exists('core_base_foot_odds_for_double_chance')) {
      $out['odds']['double_chance'] = core_base_foot_odds_for_double_chance($map, (string)$dc['prediction']);
    }

    // --------------------
    // WIN 1X2
    // --------------------
    $win = is_array($preds['win_ppm'] ?? null) ? $preds['win_ppm'] : null;
    if ($win && !empty($win['ok']) && !empty($win['prediction']) && function_exists('core_base_foot_odds_for_match_winner')) {
      $out['odds']['win_1x2'] = core_base_foot_odds_for_match_winner($map, (string)$win['prediction']);
    }

    // --------------------
    // DRAW 1X2 (X)
    // --------------------
    $draw = is_array($preds['draw_xg'] ?? null) ? $preds['draw_xg'] : null;
    if ($draw && !empty($draw['ok']) && function_exists('core_base_foot_odds_for_match_winner')) {
      $out['odds']['draw_1x2'] = core_base_foot_odds_for_match_winner($map, 'X');
    }

    // --------------------
    // TOTAL GOALS
    // --------------------
    $gt = is_array($preds['goals_total'] ?? null) ? $preds['goals_total'] : null;
    if ($gt && !empty($gt['ok'])) {
      $pred = (string)($gt['prediction'] ?? '');
      $line = (float)($gt['line'] ?? 0);

      if ($pred && $line > 0 && function_exists('core_base_foot_odds_for_total_goals')) {
        $out['odds']['total_goals'] = core_base_foot_odds_for_total_goals($map, $pred, $line);
      } elseif ($pred && $line > 0) {
        $sels = tp_ma_lite_odds_fmt_sel_for_total_goals($pred, $line);
        $out['odds']['total_goals'] = tp_ma_lite_odds_get_any_strict_ft(
          $map,
          ['Goals Over/Under','Total Goals','Over/Under','Goal Line'],
          $sels
        );
      }
    }

    // --------------------
    // BTTS
    // --------------------
    $bt = is_array($preds['btts_yesno'] ?? null) ? $preds['btts_yesno'] : null;
    if ($bt && !empty($bt['ok']) && !empty($bt['prediction']) && function_exists('core_base_foot_odds_for_btts')) {
      $out['odds']['btts'] = core_base_foot_odds_for_btts($map, (string)$bt['prediction']);
    }

    // --------------------
    // Team To Score (HOME/AWAY)
    // --------------------
    $tts = is_array($preds['team_to_score'] ?? null) ? $preds['team_to_score'] : null;

    $is_yes = function($v){
      $s = strtoupper(trim((string)$v));
      return in_array($s, ['YES','OUI','TRUE','1'], true);
    };

    if ($tts && function_exists('core_base_foot_odds_get_any')) {

      $homeName = (string)($ctx['home_name'] ?? '');
      $awayName = (string)($ctx['away_name'] ?? '');

      $homePred = $tts['home']['prediction'] ?? '';
      $awayPred = $tts['away']['prediction'] ?? '';

      // HOME
      if ($is_yes($homePred)) {

        $out['odds']['tts_home'] = core_base_foot_odds_get_any(
          $map,
          [
            'Home Team To Score','Home Team To Score?','Team To Score - Home','Home To Score',
            'Team To Score','Team to Score'
          ],
          array_values(array_unique(array_filter([
            'Yes','YES','OUI',
            'Home','Home Yes','Home - Yes',
            $homeName, $homeName.' Yes', $homeName.' - Yes',
          ])))
        );

        if (!$out['odds']['tts_home']) {
          $out['odds']['tts_home'] = tp_ma_lite_odds_get_any_strict_ft(
            $map,
            [
              'Total - Home',
              'Home Total',
              'Home Team Total Goals',
              'Team Total Goals',
              'Team Goals Over/Under',
              'Home Team Goals Over/Under',
              'Total Goals - Home Team',
            ],
            ['Over 0.5','Over 0.5 Goals','Over0.5','0.5 Over']
          );
        }

      } else {
        $out['odds']['tts_home'] = null;
      }

      // AWAY
      if ($is_yes($awayPred)) {

        $out['odds']['tts_away'] = core_base_foot_odds_get_any(
          $map,
          [
            'Away Team To Score','Away Team To Score?','Team To Score - Away','Away To Score',
            'Team To Score','Team to Score'
          ],
          array_values(array_unique(array_filter([
            'Yes','YES','OUI',
            'Away','Away Yes','Away - Yes',
            $awayName, $awayName.' Yes', $awayName.' - Yes',
          ])))
        );

        if (!$out['odds']['tts_away']) {
          $out['odds']['tts_away'] = tp_ma_lite_odds_get_any_strict_ft(
            $map,
            [
              'Total - Away',
              'Away Total',
              'Away Team Total Goals',
              'Team Total Goals',
              'Team Goals Over/Under',
              'Away Team Goals Over/Under',
              'Total Goals - Away Team',
            ],
            ['Over 0.5','Over 0.5 Goals','Over0.5','0.5 Over']
          );
        }

      } else {
        $out['odds']['tts_away'] = null;
      }
    }

    // --------------------
    // Exact score
    // --------------------
    $es = is_array($preds['exact_score_xg'] ?? null) ? $preds['exact_score_xg'] : null;
    if ($es && !empty($es['ok']) && !empty($es['label'])) {
      $lab  = (string)$es['label'];
      $sel1 = preg_replace('~\s+~','', str_replace(':','-',$lab));

      $out['odds']['exact_score'] = tp_ma_lite_odds_get_any_strict_ft(
        $map,
        ['Exact Score','Correct Score','Final Score'],
        [$lab, $sel1]
      );
    }

    // --------------------
    // Score multi-chance (TOP 3 exact scores)
    // --------------------
    $sm = is_array($preds['score_multichance_xg'] ?? null) ? $preds['score_multichance_xg'] : null;
    if ($sm && !empty($sm['ok']) && !empty($sm['scores']) && is_array($sm['scores'])) {

      $arr = [];
      $top = array_slice($sm['scores'], 0, 3);

      foreach ($top as $row) {
        $sc = trim((string)($row['score'] ?? ''));
        if (!$sc) { $arr[] = null; continue; }

        $sel_raw   = $sc;
        $sel_ns    = preg_replace('~\s+~','', $sc);
        $sel_dash  = str_replace(':','-', $sel_ns);
        $sel_colon = str_replace('-',':', $sel_ns);

        $sels = array_values(array_unique(array_filter([$sel_raw, $sel_ns, $sel_dash, $sel_colon])));

        $arr[] = tp_ma_lite_odds_get_any_strict_ft(
          $map,
          ['Exact Score','Correct Score','Final Score'],
          $sels
        );
      }

      $out['odds']['score_multichance'] = $arr;
    }

    // ✅ NORMALISATION (float OU array)
    foreach ($out['odds'] as $k => $v) {
      if (is_array($v)) {
        $out['odds'][$k] = array_map($odd_value, $v);
      } else {
        $out['odds'][$k] = $odd_value($v);
      }
    }

    $out['ok'] = true;
    return $out;
  }
}

/* ============================================================
   ✅ AJAX — BADGES ONLY (léger) + FORCE REFRESH  (UPDATED)
   - WIN: threshold 1.15 + guard L5 PPM diff 1.10
   - Cache keys bumped (v13 / v12)
   ============================================================ */
if (!function_exists('tp_ma_lite_ajax_badges')) {
  function tp_ma_lite_ajax_badges(){

    if (!check_ajax_referer('tp_ma_lite_nonce', 'nonce', false)) {
      wp_send_json_error(['message'=>'Nonce invalide']);
    }

    // ✅ GARDE-FOU CORE
    tp_ma_lite_require_core();

    $t0 = microtime(true);

    $fixture_id = (int)($_POST['fixture_id'] ?? 0);
    $league_id  = (int)($_POST['league_id'] ?? 0);
    $home_id    = (int)($_POST['home_id'] ?? 0);
    $away_id    = (int)($_POST['away_id'] ?? 0);

    $home_name  = sanitize_text_field($_POST['home_name'] ?? '');
    $away_name  = sanitize_text_field($_POST['away_name'] ?? '');
    $home_logo  = esc_url_raw($_POST['home_logo'] ?? '');
    $away_logo  = esc_url_raw($_POST['away_logo'] ?? '');

    $force = !empty($_POST['force']); // ✅ FORCE

    $season = defined('CORE_BASE_FOOT_SEASON') ? (int)CORE_BASE_FOOT_SEASON : 0;
    if ($fixture_id<=0 || $league_id<=0 || $home_id<=0 || $away_id<=0 || $season<=0) {
      wp_send_json_error(['message'=>'Paramètres manquants (fixture/league/home/away/season)']);
    }

    $pot_opts = ['mix_attack'=>0.50,'cap_min'=>0.2,'cap_max'=>3.8];

    // ✅ Options WIN (nouveau modèle)
    $win_opts = [
      'threshold'     => 1.15, // ✅ ancien 1.30 -> 1.15
      'l5_ppm_thr'    => 1.10, // ✅ guard L5
      'l5_min_played' => 4,    // ✅ min matchs (L5)
      // 'strict'      => false,
    ];

    // ✅ clés cache (bump versions)
    $cache_key   = "tp_ma_lite:badges:v13:fx={$fixture_id}:lg={$league_id}:season={$season}";
    $details_key = "tp_ma_lite:details:v12:fx={$fixture_id}:lg={$league_id}:season={$season}";

    // ✅ si force => purge badges + details
    if ($force && function_exists('tp_ma_lite_cache_del')) {
      tp_ma_lite_cache_del($cache_key);
      tp_ma_lite_cache_del($details_key);
    }

    // ✅ si PAS force => on sert le cache
    if (!$force) {
      $cached = tp_ma_lite_cache_get($cache_key);
      if (is_array($cached) && isset($cached['meta'])) wp_send_json_success($cached);
    }

    // ---- REBUILD ----
    try {
      $home_pack = core_base_foot_build_team_match_stats($home_id, $league_id, $season);
      $away_pack = core_base_foot_build_team_match_stats($away_id, $league_id, $season);
    } catch (Throwable $e) {
      wp_send_json_error(['message'=>'Erreur build_team_match_stats: '.$e->getMessage()]);
    }

    $pot = function_exists('core_base_foot_calc_potential_goals')
      ? core_base_foot_calc_potential_goals($home_pack, $away_pack, $pot_opts)
      : null;

    $pred_dc = function_exists('core_base_foot_predict_double_chance_ppm')
      ? core_base_foot_predict_double_chance_ppm($home_pack, $away_pack, ['threshold'=>0.30])
      : null;

    // ✅ UPDATED: WIN uses new opts (1.15 + L5 guard)
    $pred_win = function_exists('core_base_foot_predict_win_ppm')
      ? core_base_foot_predict_win_ppm($home_pack, $away_pack, $win_opts)
      : null;

    $pred_goals = function_exists('core_base_foot_predict_total_goals')
      ? core_base_foot_predict_total_goals($home_pack, $away_pack, $pot)
      : null;

    $pred_btts = function_exists('core_base_foot_predict_btts_yesno')
      ? core_base_foot_predict_btts_yesno($home_pack, $away_pack, $pot, ['xg_opts'=>$pot_opts])
      : null;

    $pred_draw = function_exists('core_base_foot_predict_draw_xg')
      ? core_base_foot_predict_draw_xg($home_pack, $away_pack, $pot, [
          'diff_xg_thr'   => 0.20,
          'diff_ppm_thr'  => 0.40,
          'draw_prob_thr' => 0.00,
        ])
      : null;

    $pred_tts = function_exists('core_base_foot_predict_team_to_score')
      ? core_base_foot_predict_team_to_score($home_pack, $away_pack, $pot, ['xg_opts'=>$pot_opts])
      : null;

$exact_score = tp_ma_lite_exact_score_from_xg($pot);
$score_mc = function_exists('core_base_foot_predict_score_multichance_xg')
  ? core_base_foot_predict_score_multichance_xg($home_pack, $away_pack, $pot, ['maxG'=>6])
  : null;

// ✅ Inject exact score into Top3 multi-chance if missing
$score_mc = tp_ma_lite_score_mc_ensure_exact($score_mc, $exact_score);


    $preds = [
      'double_chance_ppm' => $pred_dc,
      'win_ppm'           => $pred_win,
      'draw_xg'           => $pred_draw,
      'goals_total'       => $pred_goals,
      'btts_yesno'        => $pred_btts,
      'team_to_score'     => $pred_tts,
      'exact_score_xg'    => $exact_score,
		'score_multichance_xg' => $score_mc,
    ];

    $oddsPack = tp_ma_lite_get_needed_odds($fixture_id, $preds, [
      'home_name' => $home_name,
      'away_name' => $away_name,
      'league_id' => $league_id,
      'season'    => $season,
      'force'     => $force,
    ]);

    $payload = [
      'teams' => [
        'home' => [
          'id'   => $home_id,
          'name' => $home_name ?: (string)($home_pack['team_name'] ?? 'Domicile'),
          'logo' => $home_logo ?: (string)($home_pack['team_logo'] ?? ''),
        ],
        'away' => [
          'id'   => $away_id,
          'name' => $away_name ?: (string)($away_pack['team_name'] ?? 'Extérieur'),
          'logo' => $away_logo ?: (string)($away_pack['team_logo'] ?? ''),
        ],
      ],
      'predictions' => $preds,
      'odds' => $oddsPack,
      'meta' => [
        'time_used_sec' => round(microtime(true) - $t0, 2),
        'season'        => $season,
        'league_id'     => $league_id,
        'fixture_id'    => $fixture_id,
        'force'         => $force ? 1 : 0,
      ],
    ];

    tp_ma_lite_cache_set($cache_key, $payload, 15 * 60);
    wp_send_json_success($payload);
  }
}
add_action('wp_ajax_tp_ma_lite_badges', 'tp_ma_lite_ajax_badges');
add_action('wp_ajax_nopriv_tp_ma_lite_badges', 'tp_ma_lite_ajax_badges');

/* ============================================================
   ✅ AJAX — DETAILS (panel "ouvrir analyse") + FORCE REFRESH (UPDATED)
   - WIN: threshold 1.15 + guard L5 PPM diff 1.10
   - Cache keys bumped (v12 / v13)
   ============================================================ */
if (!function_exists('tp_ma_lite_ajax_details')) {
  function tp_ma_lite_ajax_details(){

    if (!check_ajax_referer('tp_ma_lite_nonce', 'nonce', false)) {
      wp_send_json_error(['message'=>'Nonce invalide']);
    }

    // ✅ GARDE-FOU CORE
    tp_ma_lite_require_core();

    $t0 = microtime(true);

    $fixture_id = (int)($_POST['fixture_id'] ?? 0);
    $league_id  = (int)($_POST['league_id'] ?? 0);
    $home_id    = (int)($_POST['home_id'] ?? 0);
    $away_id    = (int)($_POST['away_id'] ?? 0);

    $home_name  = sanitize_text_field($_POST['home_name'] ?? '');
    $away_name  = sanitize_text_field($_POST['away_name'] ?? '');
    $home_logo  = esc_url_raw($_POST['home_logo'] ?? '');
    $away_logo  = esc_url_raw($_POST['away_logo'] ?? '');

    $force = !empty($_POST['force']); // ✅ FORCE

    $season = defined('CORE_BASE_FOOT_SEASON') ? (int)CORE_BASE_FOOT_SEASON : 0;
    if ($fixture_id<=0 || $league_id<=0 || $home_id<=0 || $away_id<=0 || $season<=0) {
      wp_send_json_error(['message'=>'Paramètres manquants (fixture/league/home/away/season)']);
    }

    $pot_opts = ['mix_attack'=>0.50,'cap_min'=>0.2,'cap_max'=>3.8];

    // ✅ Options WIN (nouveau modèle)
    $win_opts = [
      'threshold'     => 1.15, // ✅ ancien 1.30 -> 1.15
      'l5_ppm_thr'    => 1.10, // ✅ guard L5
      'l5_min_played' => 4,    // ✅ min matchs (L5)
      // 'strict'      => false,
    ];

    // ✅ clés cache (bump versions)
    $cache_key  = "tp_ma_lite:details:v12:fx={$fixture_id}:lg={$league_id}:season={$season}";
    $badges_key = "tp_ma_lite:badges:v13:fx={$fixture_id}:lg={$league_id}:season={$season}";

    // ✅ si force => purge details + badges (cohérence)
    if ($force && function_exists('tp_ma_lite_cache_del')) {
      tp_ma_lite_cache_del($cache_key);
      tp_ma_lite_cache_del($badges_key);
    }

    // ✅ si PAS force => on sert le cache
    if (!$force) {
      $cached = tp_ma_lite_cache_get($cache_key);
      if (is_array($cached) && isset($cached['meta'])) wp_send_json_success($cached);
    }

    // ---- REBUILD ----
    try {
      $home_pack = core_base_foot_build_team_match_stats($home_id, $league_id, $season);
      $away_pack = core_base_foot_build_team_match_stats($away_id, $league_id, $season);
    } catch (Throwable $e) {
      wp_send_json_error(['message'=>'Erreur build_team_match_stats: '.$e->getMessage()]);
    }

    $pot = function_exists('core_base_foot_calc_potential_goals')
      ? core_base_foot_calc_potential_goals($home_pack, $away_pack, $pot_opts)
      : null;

    $pred_dc = function_exists('core_base_foot_predict_double_chance_ppm')
      ? core_base_foot_predict_double_chance_ppm($home_pack, $away_pack, ['threshold'=>0.30])
      : null;

    // ✅ UPDATED: WIN uses new opts (1.15 + L5 guard)
    $pred_win = function_exists('core_base_foot_predict_win_ppm')
      ? core_base_foot_predict_win_ppm($home_pack, $away_pack, $win_opts)
      : null;

    $pred_goals = function_exists('core_base_foot_predict_total_goals')
      ? core_base_foot_predict_total_goals($home_pack, $away_pack, $pot)
      : null;

    $pred_btts = function_exists('core_base_foot_predict_btts_yesno')
      ? core_base_foot_predict_btts_yesno($home_pack, $away_pack, $pot, ['xg_opts'=>$pot_opts])
      : null;

    $pred_draw = function_exists('core_base_foot_predict_draw_xg')
      ? core_base_foot_predict_draw_xg($home_pack, $away_pack, $pot, [
          'diff_xg_thr'   => 0.20,
          'diff_ppm_thr'  => 0.40,
          'draw_prob_thr' => 0.00,
        ])
      : null;

    $pred_tts = function_exists('core_base_foot_predict_team_to_score')
      ? core_base_foot_predict_team_to_score($home_pack, $away_pack, $pot, ['xg_opts'=>$pot_opts])
      : null;

$exact_score = tp_ma_lite_exact_score_from_xg($pot);
$score_mc = function_exists('core_base_foot_predict_score_multichance_xg')
  ? core_base_foot_predict_score_multichance_xg($home_pack, $away_pack, $pot, ['maxG'=>6])
  : null;

// ✅ Inject exact score into Top3 multi-chance if missing
$score_mc = tp_ma_lite_score_mc_ensure_exact($score_mc, $exact_score);

    $preds = [
      'double_chance_ppm' => $pred_dc,
      'win_ppm'           => $pred_win,
      'draw_xg'           => $pred_draw,
      'goals_total'       => $pred_goals,
      'btts_yesno'        => $pred_btts,
      'team_to_score'     => $pred_tts,
      'exact_score_xg'    => $exact_score,
		'score_multichance_xg' => $score_mc,
    ];

    $oddsPack = tp_ma_lite_get_needed_odds($fixture_id, $preds, [
      'home_name' => $home_name,
      'away_name' => $away_name,
      'league_id' => $league_id,
      'season'    => $season,
      'force'     => $force,
    ]);

    $payload = [
      'match_stats' => [
        'home' => [
          'id'    => $home_id,
          'name'  => $home_name ?: (string)($home_pack['team_name'] ?? 'Domicile'),
          'logo'  => $home_logo ?: (string)($home_pack['team_logo'] ?? ''),
          'stats' => $home_pack,
        ],
        'away' => [
          'id'    => $away_id,
          'name'  => $away_name ?: (string)($away_pack['team_name'] ?? 'Extérieur'),
          'logo'  => $away_logo ?: (string)($away_pack['team_logo'] ?? ''),
          'stats' => $away_pack,
        ],
      ],
      'potential_goals' => $pot,
      'predictions'     => $preds,
      'odds'            => $oddsPack,
      'meta' => [
        'time_used_sec' => round(microtime(true) - $t0, 2),
        'season'        => $season,
        'league_id'     => $league_id,
        'fixture_id'    => $fixture_id,
        'force'         => $force ? 1 : 0,
      ],
    ];

    tp_ma_lite_cache_set($cache_key, $payload, 15 * 60);
    wp_send_json_success($payload);
  }
}
add_action('wp_ajax_tp_ma_lite_details', 'tp_ma_lite_ajax_details');
add_action('wp_ajax_nopriv_tp_ma_lite_details', 'tp_ma_lite_ajax_details');

/* =========================
   Shortcode
   ========================= */
if (!function_exists('tp_match_analyzer_lite_shortcode')) {
  function tp_match_analyzer_lite_shortcode($atts = []){
    $atts = shortcode_atts([
      'high_thr' => '80',
    ], (array)$atts);

    $HIGH_THR = max(50, min(99, (int)$atts['high_thr']));

    // ✅ garde le snippet activable même si CORE absent (affiche un message)
    if (!function_exists('core_base_foot_get_fixtures_window') || !defined('CORE_BASE_FOOT_SEASON')) {
      return '<div style="padding:12px;border:1px solid #ddd;border-radius:12px;background:#fff">
        <b>Erreur :</b> le snippet <i>CORE BASE FOOT</i> n’est pas actif.
      </div>';
    }

    $fixtures = core_base_foot_get_fixtures_window();
    $nonce = wp_create_nonce('tp_ma_lite_nonce');
    $ajax_url = admin_url('admin-ajax.php');

    $leagueMap = [];
    foreach ((array)$fixtures as $fx) {
      if (!is_array($fx) || isset($fx['_error'])) continue;
      $lid = (int)($fx['league']['id'] ?? 0);
      $lname = (string)($fx['league']['name'] ?? '');
      if ($lid && $lname) $leagueMap[$lid] = $lname;
    }
    ksort($leagueMap);

    ob_start();
?>
<style>
.tpmL-wrap{
  --bg:#070b14; --card:#0b1220; --txt:#e5e7eb; --muted:#9ca3af; --line:rgba(255,255,255,.10);
  max-width:1120px;margin:0 auto;padding:14px;
  font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Arial;
  color:var(--txt);
}
.tpmL-title{font-size:18px;font-weight:1000;margin:4px 0 10px}
.tpmL-filters{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin:10px 0 14px}
.tpmL-field{display:flex;flex-direction:column;gap:6px}
.tpmL-label{font-size:11px;color:var(--muted);font-weight:900}
.tpmL-select,.tpmL-input{
  min-width:220px;border-radius:14px;padding:10px 12px;border:1px solid var(--line);
  background:rgba(11,18,32,.92);color:var(--txt);outline:none;
}
.tpmL-reset{
  border:0;border-radius:14px;padding:10px 12px;font-weight:900;cursor:pointer;color:#07140f;
  background:linear-gradient(135deg,#a7f3d0,#60a5fa);
}
.tpmL-count{font-size:12px;color:var(--muted);margin-left:auto}

.tpmL-grid{display:grid;grid-template-columns:1fr;gap:12px}
.tpmL-card{
  background:linear-gradient(180deg,#0b1220,#070b14);
  border:1px solid rgba(255,255,255,.08);
  border-radius:18px;
  padding:12px;
  overflow:hidden;
}
.tpmL-top{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}
.tpmL-league{font-weight:1000;font-size:13px}
.tpmL-when{font-weight:1000;font-size:13px;text-align:right}
.tpmL-pill{display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:rgba(255,255,255,.10);border:1px solid rgba(255,255,255,.12);font-weight:900;margin-left:6px}
.tpmL-mid{display:grid;grid-template-columns:1fr 150px 1fr;gap:10px;align-items:center;margin:10px 0 6px}
.tpmL-team{display:flex;gap:10px;align-items:center;min-width:0}
.tpmL-team.right{justify-content:flex-end}
.tpmL-logo{width:30px;height:30px;object-fit:contain}
.tpmL-name{font-weight:1000;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tpmL-score{text-align:center}
.tpmL-score-big{font-weight:1000;font-size:20px}
.tpmL-empty{font-size:12px;color:var(--muted)}
.tpmL-muted{color:var(--muted)}

.tpmL-btn{
  border:0;border-radius:14px;padding:10px 12px;
  font-weight:1000;cursor:pointer;color:#07140f;
  background:linear-gradient(135deg,#a7f3d0,#60a5fa);
  margin-top:10px;
}
.tpmL-panel{
  margin-top:10px;padding:12px;border-radius:16px;background:rgba(255,255,255,.04);
  border:1px solid rgba(255,255,255,.08);display:none
}
.tpmL-loading{font-weight:1000}

/* Panel tables */
.tpmL-box{border:1px solid rgba(255,255,255,.10);border-radius:16px;overflow:hidden;background:rgba(0,0,0,.12);margin-top:10px}
.tpmL-boxhead{padding:10px 12px;font-weight:1000;background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.08);display:flex;justify-content:space-between;gap:10px}
.tpmL-actions{display:flex;align-items:center;gap:10px}
.tpmL-refresh{
  border:1px solid rgba(255,255,255,.16);
  background:rgba(255,255,255,.06);
  color:var(--txt);
  border-radius:12px;
  padding:7px 10px;
  font-weight:1000;
  cursor:pointer;
  line-height:1;
}
.tpmL-refresh[disabled]{opacity:.55;cursor:not-allowed}

.tpmL-meta{font-size:11px;color:rgba(255,255,255,.65);font-weight:900}
.tpmL-table{width:100%;border-collapse:collapse}
.tpmL-table th,.tpmL-table td{padding:10px 12px;border-top:1px solid rgba(255,255,255,.08);font-size:12px;vertical-align:middle}
.tpmL-table th{width:240px;text-align:left;color:#d1d5db;background:rgba(255,255,255,.03)}
.tpmL-chip{display:inline-flex;align-items:center;justify-content:center;min-width:58px;padding:6px 10px;border-radius:999px;border:1px solid rgba(148,163,184,.26);background:rgba(148,163,184,.12);font-weight:1000;line-height:1}
.tpmL-green{border-color:rgba(34,197,94,.40);background:rgba(34,197,94,.18)}
.tpmL-red{border-color:rgba(239,68,68,.36);background:rgba(239,68,68,.14)}
.tpmL-amber{border-color:rgba(251,191,36,.40);background:rgba(251,191,36,.16)}
.tpmL-odd{border-color:rgba(96,165,250,.45);background:rgba(96,165,250,.18)}

/* ✅ BADGES (style image) */
.tpmL-badges{
  display:flex;flex-wrap:wrap;gap:8px;
  padding:10px 12px;
  margin-top:10px;
  border-top:1px solid rgba(255,255,255,.08);
  background:rgba(0,0,0,.10);
  border-radius:14px;
}
.tpmL-badge{
  display:inline-flex;align-items:center;gap:8px;
  padding:6px 10px;
  border-radius:999px;
  border:1px solid rgba(251,191,36,.55);
  background:rgba(17,24,39,.65);
  box-shadow:0 0 0 1px rgba(251,191,36,.16) inset;
  font-weight:1000;
  font-size:12px;
  line-height:1;
  white-space:nowrap;
}
.tpmL-badge-odd{color:rgba(251,191,36,.95);font-weight:1000}

@media (max-width:860px){
  .tpmL-mid{grid-template-columns:1fr 110px 1fr}
  .tpmL-select,.tpmL-input{min-width:180px}
  .tpmL-count{width:100%;margin-left:0}
}
</style>

<div class="tpmL-wrap"
  data-ajax="<?php echo esc_url($ajax_url); ?>"
  data-nonce="<?php echo esc_attr($nonce); ?>"
  data-highthr="<?php echo (int)$HIGH_THR; ?>"
>
  <div class="tpmL-title">TP Match Analyzer — LITE + ODDS (Saison <?php echo (int)CORE_BASE_FOOT_SEASON; ?>)</div>

  <div class="tpmL-filters">
    <div class="tpmL-field">
      <div class="tpmL-label">Filtrer par ligue</div>
      <select class="tpmL-select" id="tpmL-filter-league">
        <option value="">Toutes les ligues</option>
        <?php foreach ($leagueMap as $lid => $lname): ?>
          <option value="<?php echo (int)$lid; ?>"><?php echo esc_html($lname); ?> (<?php echo (int)$lid; ?>)</option>
        <?php endforeach; ?>
      </select>
    </div>

    <div class="tpmL-field">
      <div class="tpmL-label">Recherche équipe</div>
      <input class="tpmL-input" id="tpmL-filter-team" type="text" placeholder="Ex: PSG, Bologna, Fiorentina...">
    </div>

    <button class="tpmL-reset" id="tpmL-filter-reset" type="button">Réinitialiser</button>
    <div class="tpmL-count" id="tpmL-count"></div>
  </div>

  <div class="tpmL-grid" id="tpmL-grid">
    <?php
    if (!$fixtures) {
      echo '<div class="tpmL-card"><div class="tpmL-empty">Aucun match dans la fenêtre.</div></div>';
    } else {
      foreach ($fixtures as $fx) {
        if (!is_array($fx)) continue;

        if (isset($fx['_error'])) {
          echo '<div class="tpmL-card"><div class="tpmL-empty">Erreur ligue '.(int)$fx['_league'].' : '.esc_html($fx['_error']).'</div></div>';
          continue;
        }

        $fixture_id = (int)($fx['fixture']['id'] ?? 0);
        $league_id  = (int)($fx['league']['id'] ?? 0);

        $league = (string)($fx['league']['name'] ?? '');
        $home = (string)($fx['teams']['home']['name'] ?? 'Domicile');
        $away = (string)($fx['teams']['away']['name'] ?? 'Extérieur');
        $home_id = (int)($fx['teams']['home']['id'] ?? 0);
        $away_id = (int)($fx['teams']['away']['id'] ?? 0);

        $hlogo = (string)($fx['teams']['home']['logo'] ?? '');
        $alogo = (string)($fx['teams']['away']['logo'] ?? '');

        $status = (string)($fx['fixture']['status']['short'] ?? '');
        $statusFr = tp_ma_lite_translate_status_short($status);

        $dateStr = (string)($fx['fixture']['date'] ?? '');
        $when = $dateStr ? (function_exists('core_base_foot_format_match_datetime')
          ? core_base_foot_format_match_datetime($dateStr, CORE_BASE_FOOT_TZ)
          : $dateStr
        ) : '';

        $score = '—';
        if (function_exists('core_base_foot_get_ft_goals')) {
          [$sH,$sA] = core_base_foot_get_ft_goals($fx);
          if (is_int($sH) && is_int($sA)) $score = esc_html($sH.' - '.$sA);
        } else {
          $scoreH = $fx['goals']['home'] ?? null;
          $scoreA = $fx['goals']['away'] ?? null;
          if ($scoreH !== null || $scoreA !== null) $score = esc_html((string)$scoreH.' - '.(string)$scoreA);
        }

        $panelId = 'tpmL-panel-' . $fixture_id;

        $teamSearch = function_exists('core_base_foot_strtolower')
          ? core_base_foot_strtolower($home.' '.$away)
          : strtolower($home.' '.$away);
    ?>
      <div class="tpmL-card"
        data-league="<?php echo (int)$league_id; ?>"
        data-teams="<?php echo esc_attr($teamSearch); ?>"
        data-fixture="<?php echo (int)$fixture_id; ?>"
        data-league-id="<?php echo (int)$league_id; ?>"
        data-home-id="<?php echo (int)$home_id; ?>"
        data-away-id="<?php echo (int)$away_id; ?>"
        data-home-name="<?php echo esc_attr($home); ?>"
        data-away-name="<?php echo esc_attr($away); ?>"
        data-home-logo="<?php echo esc_url($hlogo); ?>"
        data-away-logo="<?php echo esc_url($alogo); ?>"
      >
        <div class="tpmL-top">
          <div>
            <div class="tpmL-league"><?php echo esc_html($league); ?></div>
            <div class="tpmL-empty"><?php echo (int)$league_id; ?></div>
          </div>
          <div>
            <div class="tpmL-when">
              <?php echo esc_html($when); ?>
              <span class="tpmL-pill"><?php echo esc_html($statusFr); ?></span>
            </div>
          </div>
        </div>

        <div class="tpmL-mid">
          <div class="tpmL-team">
            <?php if ($hlogo): ?><img class="tpmL-logo" src="<?php echo esc_url($hlogo); ?>" alt=""><?php endif; ?>
            <div class="tpmL-name"><?php echo esc_html($home); ?></div>
          </div>

          <div class="tpmL-score">
            <div class="tpmL-score-big"><?php echo $score; ?></div>
            <div class="tpmL-empty">Saison <?php echo (int)CORE_BASE_FOOT_SEASON; ?></div>
          </div>

          <div class="tpmL-team right">
            <div class="tpmL-name"><?php echo esc_html($away); ?></div>
            <?php if ($alogo): ?><img class="tpmL-logo" src="<?php echo esc_url($alogo); ?>" alt=""><?php endif; ?>
          </div>
        </div>

        <!-- ✅ BADGES placeholder (rempli automatiquement) -->
        <div class="tpmL-badges" id="tpmL-badges-<?php echo (int)$fixture_id; ?>">
          <span class="tpmL-muted">Chargement des confiances…</span>
        </div>

        <!-- ✅ Bouton + panel -->
        <button class="tpmL-btn"
          data-target="<?php echo esc_attr($panelId); ?>"
          type="button">Ouvrir l’analyse</button>

        <div class="tpmL-panel" id="<?php echo esc_attr($panelId); ?>">
          <div class="tpmL-loading">Chargement…</div>
        </div>
      </div>
    <?php } } ?>
  </div>
</div>

<script>
(function(){

  const root = document.querySelector('.tpmL-wrap');
  if(!root) return;

  const ajaxUrl = root.getAttribute('data-ajax');
  const nonce = root.getAttribute('data-nonce');
  const highThr = Number(root.getAttribute('data-highthr') || 80);

  const comboThr = 75; // ✅ seuil d'affichage du COMBO SAFE
  const funPenalty = 3; // ✅ FUN = moyenne - funPenalty

  const store = new Map();

  const leagueSel = document.getElementById('tpmL-filter-league');
  const teamInp = document.getElementById('tpmL-filter-team');
  const resetBtn = document.getElementById('tpmL-filter-reset');
  const countEl = document.getElementById('tpmL-count');

  const norm = s => (s ?? '').toString().toLowerCase().trim();
  const esc = s => (s ?? '').toString()
    .replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
    .replace(/"/g,'"').replace(/'/g,''');

  function applyFilters(){
    const leagueVal = leagueSel ? leagueSel.value : '';
    const teamVal = norm(teamInp ? teamInp.value : '');
    const cards = Array.from(document.querySelectorAll('.tpmL-card'));
    let shown = 0;

    for(const c of cards){
      const cl = c.getAttribute('data-league') || '';
      const ct = c.getAttribute('data-teams') || '';
      let ok = true;
      if(leagueVal && cl !== leagueVal) ok = false;
      if(ok && teamVal && !ct.includes(teamVal)) ok = false;
      c.style.display = ok ? '' : 'none';
      if(ok) shown++;
    }
    if(countEl) countEl.textContent = shown + ' match(s) affiché(s)';
  }

  function resetFilters(){
    if(leagueSel) leagueSel.value = '';
    if(teamInp) teamInp.value = '';
    applyFilters();
  }

  leagueSel && leagueSel.addEventListener('change', applyFilters);
  teamInp && teamInp.addEventListener('input', applyFilters);
  resetBtn && resetBtn.addEventListener('click', resetFilters);
  applyFilters();

  function chip(val, cls=''){
    return `<span class="tpmL-chip ${cls}">${esc(val)}</span>`;
  }

  function confChip(obj){
    const pct = Number(obj?.confidence_pct ?? NaN);
    const lvl = String(obj?.confidence_level ?? '');
    if(!Number.isFinite(pct)) return chip('—');
    const p = Math.max(0, Math.min(100, Math.round(pct)));
    const low = lvl.toLowerCase();
    const cls = low.includes('fort') ? 'tpmL-green' : low.includes('moy') ? 'tpmL-amber' : 'tpmL-red';
    return chip(p+'%', cls) + (lvl ? ` <span class="tpmL-muted">(${esc(lvl)})</span>` : '');
  }

  function yn(v){
    const s = String(v||'').toUpperCase().trim();
    if(s === 'OUI' || s === 'YES') return chip('✅ OUI','tpmL-green');
    if(s === 'NON' || s === 'NO') return chip('❌ NON','tpmL-red');
    if(s.includes('IND')) return chip('🤷 Indécis','tpmL-amber');
    return chip(v || '—');
  }

  function oddChip(odd){
    const o = Number(odd ?? NaN);
    if(!Number.isFinite(o) || o <= 1.0001) return '';
    return ' ' + chip('@' + o.toFixed(2), 'tpmL-odd');
  }
function multiScoresOdds(sm, oddsArr){
  if(!sm || !Array.isArray(sm.scores) || !sm.scores.length) return '';
  const arr = Array.isArray(oddsArr) ? oddsArr : [];
  const parts = sm.scores.slice(0,3).map((row,i)=>{
    const sc = String(row?.score ?? '').trim();
    if(!sc) return '';
    const o = Number(arr[i] ?? NaN);
    if(Number.isFinite(o) && o > 1.0001) return `${sc} @${o.toFixed(2)}`;
    return sc;
  }).filter(Boolean);

  return parts.length ? ` <span class="tpmL-muted">(${esc(parts.join(' · '))})</span>` : '';
}

// ===== BADGES builder =====
function isHigh(obj, thr){
  const pct = Number(obj?.confidence_pct ?? NaN);
  return (Number.isFinite(pct) && pct >= thr);
}

  function badgePill(txt, odd, icon='🔥'){
    const o = Number(odd ?? NaN);
    const oddHtml = (Number.isFinite(o) && o > 1.0001) ? ` <span class="tpmL-badge-odd">@${o.toFixed(2)}</span>` : '';
    return `<span class="tpmL-badge"><span>${esc(icon)}</span><span>${esc(txt)}</span>${oddHtml}</span>`;
  }

  function dcPretty(pred, hName, aName){
    const p = String(pred || '').toUpperCase().trim();
    if(p === '1X' || p.includes('1X')) return `${hName} ou nul`;
    if(p === 'X2' || p.includes('X2')) return `${aName} ou nul`;
    if(p === '12' || p.includes('12')) return `${hName} ou ${aName}`;
    return p || '—';
  }

  function buildBadges(ctx, thr){
    const list = [];

    if(ctx.draw?.ok && ctx.draw?.prediction && isHigh(ctx.draw, thr)){
      const p = String(ctx.draw.prediction).toUpperCase().trim();
      if(p === 'OUI' || p === 'YES'){
        list.push({txt:`MATCH NUL`, odd: ctx.odds?.draw_1x2, icon:'🤝', kind:'safe'});
      }
    }

    if(ctx.dc?.ok && ctx.dc?.prediction && isHigh(ctx.dc, thr)){
      const p = String(ctx.dc.prediction).toUpperCase().trim();
      const dcTxt = dcPretty(p, ctx.hName, ctx.aName);
      list.push({txt:`DC ${dcTxt}`, odd: ctx.odds?.double_chance, icon:'🔥', kind:'safe'});
    }

    if(ctx.win?.ok && ctx.win?.prediction && isHigh(ctx.win, thr)){
      const p = String(ctx.win.prediction).trim();
      const lab = (p==='1') ? `WIN ${ctx.hName}` : (p==='2') ? `WIN ${ctx.aName}` : (p==='X') ? `WIN NUL` : `WIN ${p}`;
      list.push({txt:lab, odd: ctx.odds?.win_1x2, icon:'🔥', kind:'safe'});
    }

    const hPred = String(ctx.tts?.home?.prediction ?? '').toUpperCase().trim();
    if((hPred === 'YES' || hPred === 'OUI') && isHigh(ctx.tts?.home, thr)){
      list.push({txt:`${ctx.hName} marque`, odd: ctx.odds?.tts_home, icon:'🔥', kind:'safe'});
    }

    const aPred = String(ctx.tts?.away?.prediction ?? '').toUpperCase().trim();
    if((aPred === 'YES' || aPred === 'OUI') && isHigh(ctx.tts?.away, thr)){
      list.push({txt:`${ctx.aName} marque`, odd: ctx.odds?.tts_away, icon:'🔥', kind:'safe'});
    }

    if(ctx.gt?.ok && isHigh(ctx.gt, thr)){
      const lab = String(ctx.gt.label || ctx.gt.pick || ctx.gt.prediction || '').trim();
      if(lab) list.push({txt:lab, odd: ctx.odds?.total_goals, icon:'🔥', kind:'safe'});
    }

    if(ctx.bt?.ok && ctx.bt?.prediction && isHigh(ctx.bt, thr)){
      const p = String(ctx.bt.prediction).toUpperCase().trim();
      list.push({txt:`BTTS ${p === 'YES' ? 'OUI' : p}`, odd: ctx.odds?.btts, icon:'🔥', kind:'safe'});
    }

    if(ctx.es?.ok && ctx.es?.label && isHigh(ctx.es, thr)){
      list.push({txt:`SCORE ${String(ctx.es.label)}`, odd: ctx.odds?.exact_score, icon:'🎯', kind:'fun'});
    }
	
if (
  ctx.sm?.ok &&
  Array.isArray(ctx.sm.scores) &&
  ctx.sm.scores.length &&
  isHigh(ctx.sm, thr)          // ✅ AJOUT DU FILTRE
){
  const lbl = ctx.sm.scores.slice(0,3)
    .map(r => String(r?.score ?? '').trim())
    .filter(Boolean)
    .join(' · ');
  if(lbl) list.push({ txt:`SCORES ${lbl}`, odd:null, icon:'🎯', kind:'fun' });
}


    // ✅ COMBO SAFE = DC + Nombre de buts
    let comboPill = '';

    const dcPred = (ctx.dc?.ok && ctx.dc?.prediction) ? String(ctx.dc.prediction).toUpperCase().trim() : '';
    const gtLab  = (ctx.gt?.ok) ? String(ctx.gt.label || ctx.gt.pick || ctx.gt.prediction || '').trim() : '';

    const dcPct = Number(ctx.dc?.confidence_pct ?? NaN);
    const gtPct = Number(ctx.gt?.confidence_pct ?? NaN);
    const avgPct = (Number.isFinite(dcPct) && Number.isFinite(gtPct)) ? ((dcPct + gtPct) / 2) : NaN;

    if (dcPred && gtLab && Number.isFinite(avgPct) && avgPct >= comboThr) {

      const o1 = Number(ctx.odds?.double_chance ?? NaN);
      const o2 = Number(ctx.odds?.total_goals ?? NaN);

      const comboOdd = (Number.isFinite(o1) && o1 > 1.0001 && Number.isFinite(o2) && o2 > 1.0001)
        ? (o1 * o2)
        : null;

      const dcText  = dcPretty(dcPred, ctx.hName, ctx.aName);
      const shortGt = gtLab.length > 22 ? (gtLab.slice(0, 22) + '…') : gtLab;
      comboPill = badgePill(`COMBO SAFE: ${dcText} + ${shortGt}`, comboOdd, '🧩');
    }

    // ✅ FUN = meilleur combo parmi :
    // (A) Victoire + Buts
    // (B) Double chance + BTTS OUI
    // Confiance FUN = moyenne - funPenalty
    let funPill = '';

    const winPredRaw = (ctx.win?.ok && ctx.win?.prediction) ? String(ctx.win.prediction).trim() : '';
    const btPredRaw  = (ctx.bt?.ok && ctx.bt?.prediction) ? String(ctx.bt.prediction).toUpperCase().trim() : '';

    const winPct = Number(ctx.win?.confidence_pct ?? NaN);
    const btPct  = Number(ctx.bt?.confidence_pct ?? NaN);

    const funScore = (a, b) => (Number.isFinite(a) && Number.isFinite(b)) ? (((a + b) / 2) - funPenalty) : NaN;

    const candidates = [];

    // (A) WIN (1/2 uniquement) + Goals
    if ((winPredRaw === '1' || winPredRaw === '2') && gtLab && Number.isFinite(winPct) && Number.isFinite(gtPct)) {
      const pct = funScore(winPct, gtPct);
      const oW  = Number(ctx.odds?.win_1x2 ?? NaN);
      const oG  = Number(ctx.odds?.total_goals ?? NaN);
      const odd = (Number.isFinite(oW) && oW > 1.0001 && Number.isFinite(oG) && oG > 1.0001) ? (oW * oG) : null;

      const winTxt = (winPredRaw === '1') ? `${ctx.hName} gagne` : `${ctx.aName} gagne`;
      const shortGt = gtLab.length > 22 ? (gtLab.slice(0, 22) + '…') : gtLab;

      candidates.push({
        pct,
        txt: `FUN ${Math.round(pct)}%: ${winTxt} + ${shortGt}`,
        odd
      });
    }

    // (B) DC + BTTS OUI
    const btYes = (btPredRaw === 'YES' || btPredRaw === 'OUI');
    if (dcPred && btYes && Number.isFinite(dcPct) && Number.isFinite(btPct)) {
      const pct = funScore(dcPct, btPct);
      const oD  = Number(ctx.odds?.double_chance ?? NaN);
      const oB  = Number(ctx.odds?.btts ?? NaN);
      const odd = (Number.isFinite(oD) && oD > 1.0001 && Number.isFinite(oB) && oB > 1.0001) ? (oD * oB) : null;

      const dcTxt = dcPretty(dcPred, ctx.hName, ctx.aName);
      candidates.push({
        pct,
        txt: `FUN ${Math.round(pct)}%: ${dcTxt} + BTTS OUI`,
        odd
      });
    }

    // best candidate
    candidates.sort((a,b) => (b.pct ?? -999) - (a.pct ?? -999));
    const best = candidates[0] || null;

    if (best && Number.isFinite(best.pct) && best.pct >= comboThr) {
      funPill = badgePill(best.txt, best.odd, '🎲');
    }

    const pills = list.map(x => badgePill(x.txt, x.odd, x.icon)).join('');
    return { html: (pills + comboPill + funPill), count: list.length + (comboPill ? 1 : 0) + (funPill ? 1 : 0) };
  }

  // ===== API helper =====
  async function postAjax(fd){
    const res = await fetch(ajaxUrl, { method:'POST', body: fd, credentials:'same-origin' });
    return await res.json();
  }

  function buildFormForCard(card, action, fxid, force){
    const fd = new FormData();
    fd.append('action', action);
    fd.append('nonce', nonce);
    fd.append('fixture_id', fxid);
    fd.append('league_id', card.getAttribute('data-league-id'));
    fd.append('home_id', card.getAttribute('data-home-id'));
    fd.append('away_id', card.getAttribute('data-away-id'));
    fd.append('home_name', card.getAttribute('data-home-name')||'');
    fd.append('away_name', card.getAttribute('data-away-name')||'');
    fd.append('home_logo', card.getAttribute('data-home-logo')||'');
    fd.append('away_logo', card.getAttribute('data-away-logo')||'');
    if(force) fd.append('force', '1'); // ✅ IMPORTANT
    return fd;
  }

  // ===== RENDER PANEL =====
  function renderPanel(d){
    const ms = d?.match_stats || {};
    const hName = ms?.home?.name || 'Domicile';
    const aName = ms?.away?.name || 'Extérieur';

    const pot = d?.potential_goals || null;
    const hx = Number(pot?.home_xg ?? NaN);
    const ax = Number(pot?.away_xg ?? NaN);
    const xgH = Number.isFinite(hx) ? hx.toFixed(2) : '—';
    const xgA = Number.isFinite(ax) ? ax.toFixed(2) : '—';
    const xgT = (Number.isFinite(hx) && Number.isFinite(ax)) ? (hx+ax).toFixed(2) : '—';

    const p = d?.predictions || {};
    const dc  = p?.double_chance_ppm || null;
    const win = p?.win_ppm || null;
    const gt  = p?.goals_total || null;
    const bt  = p?.btts_yesno || null;
    const tts = p?.team_to_score || null;
    const es  = p?.exact_score_xg || null;
const sm  = p?.score_multichance_xg || null; // ✅ NEW

    const oddsPack = d?.odds || {};
    const odds = oddsPack?.odds || {};
    const bk = oddsPack?.bookmaker_id ? String(oddsPack.bookmaker_id) : '';

    const dcPick = (dc && dc.ok && dc.prediction)
      ? (String(dc.prediction).toUpperCase().includes('1X') ? `<b>${esc(hName)} ou nul</b>` :
         String(dc.prediction).toUpperCase().includes('X2') ? `<b>${esc(aName)} ou nul</b>` :
         `<b>${esc(dc.prediction)}</b>`)
      : `<span class="tpmL-muted">—</span>`;

    const winPick = (win && win.ok && win.prediction)
      ? (String(win.prediction)==='1' ? `<b>${esc(hName)} gagne</b>` :
         String(win.prediction)==='2' ? `<b>${esc(aName)} gagne</b>` :
         String(win.prediction)==='X' ? `<b>Match nul</b>` :
         `<b>${esc(win.prediction)}</b>`)
      : `<span class="tpmL-muted">—</span>`;

    const gtPick = (gt && gt.ok)
      ? `<b>${esc(gt.label || gt.pick || gt.prediction || '—')}</b>`
      : `<span class="tpmL-muted">—</span>`;

    // ✅ COMBO SAFE (DC + Buts) pour la table
    const dcPredRaw = (dc && dc.ok && dc.prediction) ? String(dc.prediction).toUpperCase().trim() : '';
    const gtLabRaw  = (gt && gt.ok) ? String(gt.label || gt.pick || gt.prediction || '').trim() : '';

    const dcPct = Number(dc?.confidence_pct ?? NaN);
    const gtPct = Number(gt?.confidence_pct ?? NaN);
    const avgPct = (Number.isFinite(dcPct) && Number.isFinite(gtPct)) ? ((dcPct + gtPct) / 2) : NaN;

    const comboOk = (dcPredRaw && gtLabRaw && Number.isFinite(avgPct) && avgPct >= comboThr);

    const oDC = Number(odds?.double_chance ?? NaN);
    const oGT = Number(odds?.total_goals ?? NaN);

    const comboOddNum = (Number.isFinite(oDC) && oDC > 1.0001 && Number.isFinite(oGT) && oGT > 1.0001)
      ? (oDC * oGT)
      : NaN;

    const dcText = dcPretty(dcPredRaw, hName, aName);
// ✅ COMBO SAFE row (UI clean) : on n'affiche que la confiance cumulée (moyenne)
let comboConfHtml = '';
if (Number.isFinite(avgPct)) {
  const p = Math.round(avgPct);
  const lvl = (p >= 80) ? 'Forte' : (p >= 61) ? 'Moyenne' : 'Faible';
  const cls = (p >= 80) ? 'tpmL-green' : (p >= 61) ? 'tpmL-amber' : 'tpmL-red';
  comboConfHtml = chip(p + '%', cls) + ` <span class="tpmL-muted">(${esc(lvl)})</span>`;
}

const comboRow = comboOk ? `
  <tr>
    <th>Combiné SAFE 2% Bk (≥ ${comboThr}%)</th>
    <td>
      <b>${esc(dcText + ' + ' + gtLabRaw)}</b>${
        Number.isFinite(comboOddNum)
          ? oddChip(comboOddNum)
          : ` <span class="tpmL-muted">(cote indisponible)</span>`
      }
        <span class="tpmL-muted"> </span> ${comboConfHtml}
    </td>
  </tr>
` : '';

    // ✅ FUN ROW : meilleur combo parmi (WIN + Buts) ou (DC + BTTS OUI)
    // ✅ FIX : label cohérent -> Moyenne-${funPenalty}
    const funCandidates = [];

    const winPredRaw2 = (win && win.ok && win.prediction) ? String(win.prediction).trim() : '';
    const btPredRaw2  = (bt && bt.prediction) ? String(bt.prediction).toUpperCase().trim() : '';

    const winPct2 = Number(win?.confidence_pct ?? NaN);
    const btPct2  = Number(bt?.confidence_pct ?? NaN);

    const funScore2 = (a, b) => (Number.isFinite(a) && Number.isFinite(b)) ? (((a + b) / 2) - funPenalty) : NaN;

    // (A) WIN (1/2) + Buts
    if ((winPredRaw2 === '1' || winPredRaw2 === '2') && gtLabRaw && Number.isFinite(winPct2) && Number.isFinite(gtPct)) {
      const pct = funScore2(winPct2, gtPct);
      const oW  = Number(odds?.win_1x2 ?? NaN);
      const oG  = Number(odds?.total_goals ?? NaN);
      const odd = (Number.isFinite(oW) && oW > 1.0001 && Number.isFinite(oG) && oG > 1.0001) ? (oW * oG) : NaN;

      const winTxt = (winPredRaw2 === '1') ? `${hName} gagne` : `${aName} gagne`;

      funCandidates.push({
        pct,
        label: `${winTxt} + ${gtLabRaw}`,
        odd,
        legA: confChip(win),
        legB: confChip(gt),
        legs: 'WIN + Buts'
      });
    }

    // (B) DC + BTTS OUI
    const btYes2 = (btPredRaw2 === 'YES' || btPredRaw2 === 'OUI');
    if (dcPredRaw && btYes2 && Number.isFinite(dcPct) && Number.isFinite(btPct2)) {
      const pct = funScore2(dcPct, btPct2);
      const oD  = Number(odds?.double_chance ?? NaN);
      const oB  = Number(odds?.btts ?? NaN);
      const odd = (Number.isFinite(oD) && oD > 1.0001 && Number.isFinite(oB) && oB > 1.0001) ? (oD * oB) : NaN;

      funCandidates.push({
        pct,
        label: `${dcText} + BTTS OUI`,
        odd,
        legA: confChip(dc),
        legB: confChip(bt),
        legs: 'DC + BTTS'
      });
    }

    // best
    funCandidates.sort((a,b) => (b.pct ?? -999) - (a.pct ?? -999));
    const funBest = funCandidates[0] || null;

    const funRow = (funBest && Number.isFinite(funBest.pct) && funBest.pct >= comboThr) ? `
      <tr>
        <th>FUN 0.5% Bk(≥ ${comboThr}%)</th>
        <td>
          <b>${esc(funBest.label)}</b>${
            Number.isFinite(funBest.odd) ? oddChip(funBest.odd) : ` <span class="tpmL-muted">(cote indisponible)</span>`
          }
            <span class="tpmL-muted">${funBest.legs} · Moyenne-${funPenalty} = ${Math.round(funBest.pct)}% (A ${funBest.legA} · B ${funBest.legB})</span>
        </td>
      </tr>
    ` : '';

    const btPick = bt ? yn(bt.prediction) : chip('—');
    const esPick = (es && es.ok) ? `<b>${esc(es.label || '—')}</b>` : `<span class="tpmL-muted">—</span>`;
const smLabel = (sm && Array.isArray(sm.scores))
  ? sm.scores.slice(0,3).map(r => String(r?.score ?? '').trim()).filter(Boolean).join(' · ')
  : '';

const smPick = smLabel
  ? `<b>${esc(smLabel)}</b>`
  : `<span class="tpmL-muted">—</span>`;


    const ttsHome = tts?.home ? yn(tts.home.prediction) : chip('—');
    const ttsAway = tts?.away ? yn(tts.away.prediction) : chip('—');

    const get3 = (teamStats, key, ctx) => {
      const s = teamStats || {};
      const a = (s?.season?.[key] ?? '—');
      const b = (s?.[ctx]?.[key] ?? '—');
      const c = (s?.last5?.[key] ?? '—');
      const fmt = v => (key.endsWith('pct') ? (String(v) + '%') : String(v));
      return `${chip('S '+fmt(a))} ${chip(ctx==='home'?'D '+fmt(b):'E '+fmt(b))} ${chip('L5 '+fmt(c))}`;
    };

    const hStats = ms?.home?.stats || {};
    const aStats = ms?.away?.stats || {};

    const meta = d?.meta || {};
const metaLine = ''; // ✅ on n'affiche plus la meta (Saison/Ligue/temps/BK/High)

    const dr = p?.draw_xg || null;
    const drPick = dr ? yn(dr.prediction) : chip('—');
    const dxg  = dr?.vals?.abs_diff_xg;
    const txg  = dr?.vals?.diff_xg_thr;
    const dppm = dr?.vals?.abs_diff_ppm;
    const tppm = dr?.vals?.diff_ppm_thr;
const detailDraw = ''; // ✅ on enlève les détails (|xG| / |PPM|)

 const r = buildBadges({ dc, win, draw: dr, gt, bt, tts, es, sm, odds, hName, aName }, highThr);
    const badgesHtml = r.count ? `<div class="tpmL-badges">${r.html}</div>` : '';

    return `
      <div class="tpmL-box">
        <div class="tpmL-boxhead">
          <div>PRÉDICTIONS</div>
          <div class="tpmL-actions">
            <button class="tpmL-refresh" type="button" data-fx="${esc(meta.fixture_id||'')}">↻ Recharger stats</button>
          ${metaLine ? `<div class="tpmL-meta">${metaLine}</div>` : ''}
          </div>
        </div>

        ${badgesHtml}

        <table class="tpmL-table">
          <tbody>
            <tr>
              <th>Buts potentiels (xG)</th>
              <td>Total: <b>${esc(xgT)}</b> · ${esc(hName)}: <b>${esc(xgH)}</b> · ${esc(aName)}: <b>${esc(xgA)}</b></td>
            </tr>
            <tr>
              <th>Double chance 5% Bk</th>
              <td>${dcPick}${oddChip(odds?.double_chance)}   ${confChip(dc)}</td>
            </tr>
            <tr>
              <th>Victoire 1.5% Bk</th>
              <td>${winPick}${oddChip(odds?.win_1x2)}   ${confChip(win)}</td>
            </tr>
            <tr>
              <th>Match nul 1% Bk</th>
              <td>${drPick}${oddChip(odds?.draw_1x2)} ${detailDraw}   ${confChip(dr)}</td>
            </tr>
            <tr>
              <th>Nombre de buts 4% Bk</th>
              <td>${gtPick}${oddChip(odds?.total_goals)}   ${confChip(gt)}</td>
            </tr>
            ${comboRow}
            ${funRow}
            <tr>
              <th>LDEM / BTTS 3% Bk</th>
              <td>${btPick}${oddChip(odds?.btts)}   ${confChip(bt)}</td>
            </tr>
            <tr>
              <th>${esc(hName)} marque ? 3% Bk</th>
              <td>${ttsHome}${oddChip(odds?.tts_home)}   ${confChip(tts?.home)}</td>
            </tr>
            <tr>
              <th>${esc(aName)} marque ? 3% Bk</th>
              <td>${ttsAway}${oddChip(odds?.tts_away)}   ${confChip(tts?.away)}</td>
            </tr>
            <tr>
              <th>Score exact 0.1% Bk</th>
              <td>${esPick}${oddChip(odds?.exact_score)}   ${confChip(es)}</td>
            </tr>
            ${oddsPack && oddsPack._error ? `
            <tr>
              <th>Cotes</th>
              <td><span class="tpmL-muted">Info : ${esc(oddsPack._error)}</span></td>
            </tr>` : ''}
			<tr>
  <th>Scores multi-chances (Top 3) 0.2% Bk</th>
<td>${smPick}   ${confChip(sm)}</td>
</tr>
          </tbody>
        </table>
      </div>

      <div class="tpmL-box">
        <div class="tpmL-boxhead">
          <div>STATS (Saison / Dom-Ext / L5)</div>
   <div class="tpmL-meta"></div>
        </div>
        <table class="tpmL-table">
          <tbody>
            <tr><th>Points / match</th><td>${get3(hStats,'ppm','home')}<br>${get3(aStats,'ppm','away')}</td></tr>
            <tr><th>Buts marqués / match</th><td>${get3(hStats,'gfpm','home')}<br>${get3(aStats,'gfpm','away')}</td></tr>
            <tr><th>Buts encaissés / match</th><td>${get3(hStats,'gapm','home')}<br>${get3(aStats,'gapm','away')}</td></tr>
            <tr><th>Marque ≥ 1 but</th><td>${get3(hStats,'score1pct','home')}<br>${get3(aStats,'score1pct','away')}</td></tr>
            <tr><th>Encaisse ≥ 1 but</th><td>${get3(hStats,'concede1pct','home')}<br>${get3(aStats,'concede1pct','away')}</td></tr>
          </tbody>
        </table>
      </div>
    `;
  }

  // ===== BADGES AUTO SUR CARTES =====
  async function loadBadgesForCard(card, force=false){
    const fxid = card.getAttribute('data-fixture');
    const box = document.getElementById('tpmL-badges-' + fxid);
    if(!box) return;

    if(force){
      box.style.display = '';
      box.innerHTML = `<span class="tpmL-muted">Rechargement…</span>`;
    }

    const fd = buildFormForCard(card, 'tp_ma_lite_badges', fxid, force);

    try{
      const json = await postAjax(fd);
      if(!json || !json.success){
        box.style.display = 'none';
        return;
      }

      const d = json.data || {};
      const teams = d.teams || {};
      const hName = teams?.home?.name || (card.getAttribute('data-home-name')||'Domicile');
      const aName = teams?.away?.name || (card.getAttribute('data-away-name')||'Extérieur');

      const p = d.predictions || {};
      const oddsPack = d.odds || {};
      const odds = oddsPack.odds || {};

const r = buildBadges({
  hName, aName,
  dc: p.double_chance_ppm,
  win: p.win_ppm,
  draw: p.draw_xg,
  gt: p.goals_total,
  bt: p.btts_yesno,
  tts: p.team_to_score,
  es: p.exact_score_xg,
  sm: p.score_multichance_xg, // ✅ AJOUT ICI
  odds
}, highThr);


      if(!r.count){
        box.style.display = 'none';
        return;
      }
      box.style.display = '';
      box.innerHTML = r.html;

    } catch(e){
      box.style.display = 'none';
    }
  }

  // limite de concurrence
  const cards = Array.from(document.querySelectorAll('.tpmL-card'));
  const queue = cards.slice();
  const MAX = 4;
  let active = 0;

  function pump(){
    while(active < MAX && queue.length){
      const c = queue.shift();
      active++;
      loadBadgesForCard(c, false).finally(() => { active--; pump(); });
    }
  }
  pump();

  // ===== DETAILS fetcher (utilisé par open + refresh) =====
  async function fetchDetailsForCard(card, force=false){
    const fxid = card.getAttribute('data-fixture');
    const fd = buildFormForCard(card, 'tp_ma_lite_details', fxid, force);

    const json = await postAjax(fd);
    if(!json || !json.success) {
      return { ok:false, error: (json?.data?.message ?? 'inconnue') };
    }
    const d = json.data || {};
    const key = String(d?.meta?.fixture_id ?? fxid);
    store.set(key, d);
    return { ok:true, data:d, key };
  }

  // ===== OUVRIR ANALYSE (DETAILS) =====
  async function loadDetails(btn){
    const card = btn.closest('.tpmL-card');
    if(!card) return;

    const targetId = btn.getAttribute('data-target');
    const panel = document.getElementById(targetId);
    if(!panel) return;

    const open = panel.style.display === 'block';
    panel.style.display = open ? 'none' : 'block';
    btn.textContent = open ? 'Ouvrir l’analyse' : 'Fermer l’analyse';
    if(open) return;

    const fxid = String(card.getAttribute('data-fixture'));

    if(panel.getAttribute('data-loaded') === '1' && store.has(fxid)){
      panel.innerHTML = renderPanel(store.get(fxid));
      return;
    }

    panel.innerHTML = '<div class="tpmL-loading">Chargement…</div>';

    try{
      const r = await fetchDetailsForCard(card, false);
      if(!r.ok){
        panel.innerHTML = '<div class="tpmL-empty">Erreur : '+esc(r.error)+'</div>';
        return;
      }
      panel.innerHTML = renderPanel(r.data);
      panel.setAttribute('data-loaded','1');
	  // ✅ synchro badges du haut avec les data "details"
const fxid2 = String(card.getAttribute('data-fixture'));
const boxTop = document.getElementById('tpmL-badges-' + fxid2);

if (boxTop) {
  const d = r.data || {};
  const ms = d.match_stats || {};
  const hName = ms?.home?.name || (card.getAttribute('data-home-name')||'Domicile');
  const aName = ms?.away?.name || (card.getAttribute('data-away-name')||'Extérieur');

  const p = d.predictions || {};
  const odds = d?.odds?.odds || {};

  const rr = buildBadges({
    hName, aName,
    dc: p.double_chance_ppm,
    win: p.win_ppm,
    draw: p.draw_xg,
    gt: p.goals_total,
    bt: p.btts_yesno,
    tts: p.team_to_score,
    es: p.exact_score_xg,
    sm: p.score_multichance_xg,
    odds
  }, highThr);

  if(!rr.count){
    boxTop.style.display = 'none';
  } else {
    boxTop.style.display = '';
    boxTop.innerHTML = rr.html;
  }
}

    } catch(e){
      panel.innerHTML = '<div class="tpmL-empty">Erreur réseau : '+esc(e?.message ?? e)+'</div>';
    }
  }

  // ===== REFRESH (force=1) : refresh panel + badges =====
  async function refreshMatch(btnRefresh){
    const panel = btnRefresh.closest('.tpmL-panel');
    const card = btnRefresh.closest('.tpmL-card');
    if(!panel || !card) return;

    const oldTxt = btnRefresh.textContent;
    btnRefresh.disabled = true;
    btnRefresh.textContent = 'Rechargement…';

    const fxid = String(btnRefresh.getAttribute('data-fx') || card.getAttribute('data-fixture') || '');
    if(fxid) store.delete(fxid);

    panel.innerHTML = '<div class="tpmL-loading">Rechargement des stats…</div>';

    try{
      const r = await fetchDetailsForCard(card, true);
      if(!r.ok){
        panel.innerHTML = '<div class="tpmL-empty">Erreur : '+esc(r.error)+'</div>';
        return;
      }
      panel.innerHTML = renderPanel(r.data);
      panel.setAttribute('data-loaded','1');

      await loadBadgesForCard(card, true);

    } catch(e){
      panel.innerHTML = '<div class="tpmL-empty">Erreur réseau : '+esc(e?.message ?? e)+'</div>';
    } finally {
      btnRefresh.disabled = false;
      btnRefresh.textContent = oldTxt || '↻ Recharger stats';
    }
  }

  // ===== CLICK HANDLERS =====
  root.addEventListener('click', async (e) => {
    const btn = e.target.closest('.tpmL-btn');
    if(btn){
      e.preventDefault();
      await loadDetails(btn);
      return;
    }

    const rbtn = e.target.closest('.tpmL-refresh');
    if(rbtn){
      e.preventDefault();
      await refreshMatch(rbtn);
      return;
    }
  });

})();
</script>

<?php
    return ob_get_clean();
  }
}

5ème Analyse

<?php


/**
 * ============================================================
 * ✅ CORE BASE FOOT — Football only (API-SPORTS / API-Football v3)
 * Prefix : core_base_foot_
 *
 * ✅ Fix importants :
 * - Statuts terminés inclus : FT + AET + PEN
 * - Goals (FT/HT) : fallback sur score.fulltime / score.halftime si goals=null
 * - Last5 : fixtures?last=XX (SANS league) + filtre local FT/AET/PEN + filtre ligue local
 * - Goal Matrix :
 *   - n=0 => "saison complète" SAFE (1 call last=250) + filtre local (évite pagination/rate limit)
 *   - n>0 => N derniers matchs terminés via fixtures?last=... (SANS league) + filtre local + tri timestamp + filtre ligue local
 * - teams/statistics : totaux safe (évite float(array))
 * - API : gestion 429 + HTTP>=400 lisible + cache anti-spam
 *
 * ✅ AJOUT :
 * - Sidelined endpoint : /sidelined?player=ID
 * - Suspensions (type=Suspended) injectées dans le bouton Blessés
 *   => core_base_foot_get_injuries_with_suspensions()
 * ============================================================
 */

if (!defined('ABSPATH')) exit;

/* =========================
   CONFIG
   ========================= */
if (!defined('CORE_BASE_FOOT_APISPORTS_KEY'))  define('CORE_BASE_FOOT_APISPORTS_KEY', '54064c5d72f6a5f59487a5309de88efe');
if (!defined('CORE_BASE_FOOT_APISPORTS_HOST')) define('CORE_BASE_FOOT_APISPORTS_HOST', 'v3.football.api-sports.io');
if (!defined('CORE_BASE_FOOT_TZ'))             define('CORE_BASE_FOOT_TZ', 'Europe/Paris');
if (!defined('CORE_BASE_FOOT_CACHE_VER'))      define('CORE_BASE_FOOT_CACHE_VER', 'v30'); // ✅ bump (purge cache) car logique changed

if (!defined('CORE_BASE_FOOT_SEASON'))         define('CORE_BASE_FOOT_SEASON', 2025);
if (!defined('CORE_BASE_FOOT_LEAGUE_IDS'))     define('CORE_BASE_FOOT_LEAGUE_IDS', '2, 3, 39, 40, 61, 62, 78, 79,
      88, 94, 95, 106, 119, 135, 136, 140, 141, 144, 172, 179, 188, 197, 203, 207, 210, 218, 271, 283, 286, 307, 318, 332, 345, 373, 848');
if (!defined('CORE_BASE_FOOT_BOOKMAKER_ID'))   define('CORE_BASE_FOOT_BOOKMAKER_ID', 11);

/* TTL */
if (!defined('CORE_BASE_FOOT_CACHE_15M')) define('CORE_BASE_FOOT_CACHE_15M', 15 * 60);
if (!defined('CORE_BASE_FOOT_CACHE_1H'))  define('CORE_BASE_FOOT_CACHE_1H', 60 * 60);
if (!defined('CORE_BASE_FOOT_CACHE_12H')) define('CORE_BASE_FOOT_CACHE_12H', 12 * 60 * 60);
// =========================
// ✅ POIDS MODELE (Saison / Dom-Ext / L5)
// =========================
if (!defined('CORE_BASE_FOOT_W_SEASON')) define('CORE_BASE_FOOT_W_SEASON', 0.20);
if (!defined('CORE_BASE_FOOT_W_HA'))     define('CORE_BASE_FOOT_W_HA',     0.35);
if (!defined('CORE_BASE_FOOT_W_L5'))     define('CORE_BASE_FOOT_W_L5',     0.45);
/* =========================
   CACHE
   ========================= */
if (!function_exists('core_base_foot_cache_key')) {
  function core_base_foot_cache_key(string $key): string {
    return 'core_base_foot_' . md5(CORE_BASE_FOOT_CACHE_VER . '|' . $key);
  }
}
if (!function_exists('core_base_foot_cache_get')) {
  function core_base_foot_cache_get(string $key) {
    return get_transient(core_base_foot_cache_key($key));
  }
}
if (!function_exists('core_base_foot_cache_set')) {
  function core_base_foot_cache_set(string $key, $value, int $ttl = 900) {
    set_transient(core_base_foot_cache_key($key), $value, $ttl);
    return $value;
  }
}

/* =========================
   HELPERS
   ========================= */
if (!function_exists('core_base_foot_now')) {
  function core_base_foot_now(): DateTime {
    return new DateTime('now', new DateTimeZone(CORE_BASE_FOOT_TZ));
  }
}
if (!function_exists('core_base_foot_dt')) {
  function core_base_foot_dt(string $s, string $tz = CORE_BASE_FOOT_TZ): DateTime {
    $dt = new DateTime($s); // respecte le timezone du string s’il existe
    try {
      $dt->setTimezone(new DateTimeZone($tz));
    } catch (Throwable $e) {}
    return $dt;
  }
}

if (!function_exists('core_base_foot_strtolower')) {
  function core_base_foot_strtolower(string $s): string {
    return function_exists('mb_strtolower') ? mb_strtolower($s, 'UTF-8') : strtolower($s);
  }
}
if (!function_exists('core_base_foot_finished_statuses')) {
  function core_base_foot_finished_statuses(): array { return ['FT','AET','PEN']; }
}
if (!function_exists('core_base_foot_is_finished_status')) {
  function core_base_foot_is_finished_status(string $st): bool {
    static $set = null;
    if ($set === null) $set = array_flip(core_base_foot_finished_statuses());
    return isset($set[$st]);
  }
}
if (!function_exists('core_base_foot_fixture_ts')) {
  function core_base_foot_fixture_ts(array $fx): int {
    $ts = (int)($fx['fixture']['timestamp'] ?? 0);
    if ($ts > 0) return $ts;
    $dateStr = (string)($fx['fixture']['date'] ?? '');
    if ($dateStr) { try { return core_base_foot_dt($dateStr, CORE_BASE_FOOT_TZ)->getTimestamp(); } catch (Throwable $e) {} }
    return 0;
  }
}
if (!function_exists('tp_match_analyzer_translate_status')) {
  function tp_match_analyzer_translate_status($short, $long = ''){
    $short = (string)$short; 
    $long  = (string)$long;

    // ✅ si CORE dispo, on l'utilise
    if (function_exists('core_base_foot_translate_status')) {
      try {
        $tr = core_base_foot_translate_status($short, $long);
        if (is_array($tr)) {
          return array_merge(
            ['short_fr' => ($short ?: '—'), 'long_fr' => ($long ?: '')],
            $tr
          );
        }
      } catch (Throwable $e) {}
    }

    // fallback local
    $mapShort = [
      'NS'=>'À venir','TBD'=>'À définir','PST'=>'Reporté','CANC'=>'Annulé','SUSP'=>'Suspendu',
      'INT'=>'Interrompu','LIVE'=>'En direct','1H'=>'1re mi-temps','HT'=>'Mi-temps','2H'=>'2e mi-temps','ET'=>'Prolongation',
      'PEN'=>'Tirs au but','BT'=>'Pause','FT'=>'Terminé','AET'=>'Terminé (a.p.)',
      'AWD'=>'Victoire sur tapis vert','WO'=>'Forfait'
    ];
    $mapLong = [
      'Not Started'=>'Match à venir',
      'Time to be defined'=>'Horaire à définir',
      'Match Finished'=>'Match terminé',
      'Match Finished After Extra Time'=>'Terminé après prolongation',
      'Match Finished After Penalty'=>'Terminé après tirs au but',
      'Postponed'=>'Reporté',
      'Cancelled'=>'Annulé',
      'Suspended'=>'Suspendu',
      'Interrupted'=>'Interrompu',
      'In Progress'=>'En cours',
      'Halftime'=>'Mi-temps',
      'Extra Time'=>'Prolongation',
      'Penalty In Progress'=>'Tirs au but',
    ];

    return [
      'short_fr' => $mapShort[$short] ?? ($short ?: '—'),
      'long_fr'  => $mapLong[$long]  ?? ($long ?: ''),
    ];
  }
}
if (!function_exists('tp_match_analyzer_translate_injury_text')) {
  function tp_match_analyzer_translate_injury_text($text){
    $text = (string)$text;
    $key = trim($text);
    if ($key === '') return $text;

    // ✅ si CORE dispo, on l'utilise
    if (function_exists('core_base_foot_translate_injury_text')) {
      try { 
        return (string) core_base_foot_translate_injury_text($key); 
      } catch (Throwable $e) {}
    }

    static $translations = [
      'Broken ankle'=>'Cheville cassée','Illness'=>'Maladie','Knee Injury'=>'Blessure au genou','Knock'=>'Coup',
      'Muscle Injury'=>'Blessure musculaire','Hamstring Injury'=>'Blessure aux ischio-jambiers',
      'Foot Injury'=>'Blessure au pied','Groin Injury'=>'Blessure à l’aine','Head Injury'=>'Blessure à la tête',
      'Calf Injury'=>'Blessure au mollet','Thigh Injury'=>'Blessure à la cuisse','Back Injury'=>'Blessure au dos',
      'Red Card'=>'Carton rouge','Ankle Injury'=>'Blessure à la cheville','Toe Injury'=>'Blessure à l’orteil',
      'Achilles Tendon Injury'=>'Blessure au tendon d’Achille','Finger Injury'=>'Blessure au doigt',
      'Leg Injury'=>'Blessure à la jambe','Injury'=>'Blessure','Unknown'=>'Inconnu',
      "Jumper's knee"=>'Tendinite rotulienne','Jumpers knee'=>'Tendinite rotulienne',
      'Muscle bruise'=>'Contusion musculaire','Wound'=>'Plaie','Sprained ankle'=>'Entorse de la cheville',
      'Thigh problems'=>'Problèmes à la cuisse','Foot injury'=>'Blessure au pied','Fitness'=>'Condition physique',
      'Heel pain'=>'Douleur au talon',
      'Groin'=>'Aine','Ankle'=>'Cheville','Knee'=>'Genou','Hip'=>'Hanche','Shoulder'=>'Épaule','Wrist'=>'Poignet',
      'Elbow'=>'Coude','Hand'=>'Main','Neck'=>'Cou','Concussion'=>'Commotion','Suspension'=>'Suspension',
      'Doubtful'=>'Incertain','Questionable'=>'Incertain','Out'=>'Absent','Injured'=>'Blessé',
      'Rest'=>'Repos','Personal reasons'=>'Raisons personnelles',
    ];

    // match direct
    if (isset($translations[$key])) return $translations[$key];

    // match insensible à la casse
    foreach ($translations as $en => $fr) {
      if (strcasecmp($en, $key) === 0) return $fr;
    }

    return $text;
  }
}
if (!function_exists('tp_match_analyzer_translate_round')) {
  function tp_match_analyzer_translate_round($round){
    $round = (string)$round;

    // ✅ si CORE dispo, on l'utilise
    if (function_exists('core_base_foot_translate_round')) {
      try { 
        return (string) core_base_foot_translate_round($round); 
      } catch (Throwable $e) {}
    }

    // fallback local
    $r = $round;
    $r = str_ireplace(
      ['Regular Season','League','Round','Stage','Group'],
      ['Saison régulière','Ligue','Journée','Phase','Groupe'],
      $r
    );
    return $r;
  }
}

/* ============================================================
 * ✅ FINISHED FIXTURES FOR TEAM (FT + AET + PEN) — sans last=
 * ============================================================ */
if (!function_exists('core_base_foot_get_finished_matches_for_team')) {
  function core_base_foot_get_finished_matches_for_team(
    int $team_id,
    int $league_id,
    int $season = 0,
    array $statuses = ['FT','AET','PEN']
  ): array {
    if (!$season) $season = (int)CORE_BASE_FOOT_SEASON;

    $statuses = array_values(array_unique(array_filter($statuses)));
    if (!$statuses) $statuses = ['FT'];

    $ck = "finished:last250:team=$team_id:league=$league_id:season=$season:st=" . implode(',', $statuses);
    $cached = core_base_foot_cache_get($ck);
    if (is_array($cached)) return $cached;

    // 1 call safe
    $r = core_base_foot_api('fixtures', [
      'team' => $team_id,
      'season' => $season,
      'last' => 250,
      'timezone' => CORE_BASE_FOOT_TZ
    ], CORE_BASE_FOOT_CACHE_12H);

    if (!empty($r['_error'])) {
      // TTL court si erreur
      return core_base_foot_cache_set($ck, [], 120);
    }

    $fx = $r['response'] ?? [];
    if (!is_array($fx)) $fx = [];

    // filtre finis + ligue + status subset
    $setSt = array_flip($statuses);

    $fx = core_base_foot_filter_finished_fixtures($fx);
    $fx = array_values(array_filter($fx, function($m) use ($league_id, $setSt){
      $lid = (int)($m['league']['id'] ?? 0);
      if ($league_id > 0 && $lid !== (int)$league_id) return false;
      $st = (string)($m['fixture']['status']['short'] ?? '');
      return isset($setSt[$st]);
    }));

    core_base_foot_sort_fixtures_desc($fx);

    // TTL normal si on a de la data, sinon TTL court
    $ttl = count($fx) ? CORE_BASE_FOOT_CACHE_12H : 180;
    return core_base_foot_cache_set($ck, $fx, $ttl);
  }
}

if (!function_exists('core_base_foot_get_ft_goals')) {
  function core_base_foot_get_ft_goals(array $fx): array {

    $h = $fx['goals']['home'] ?? null;
    $a = $fx['goals']['away'] ?? null;
    if (is_numeric($h) && is_numeric($a)) return [(int)$h, (int)$a];

    // ✅ fallback AET: extratime
    $hX = $fx['score']['extratime']['home'] ?? null;
    $aX = $fx['score']['extratime']['away'] ?? null;
    if (is_numeric($hX) && is_numeric($aX)) return [(int)$hX, (int)$aX];

    // ✅ fallback standard: fulltime
    $h2 = $fx['score']['fulltime']['home'] ?? null;
    $a2 = $fx['score']['fulltime']['away'] ?? null;
    if (is_numeric($h2) && is_numeric($a2)) return [(int)$h2, (int)$a2];

    return [null, null];
  }
}

/**
 * ✅ HT goals robuste
 * return [int|null, int|null]
 */
if (!function_exists('core_base_foot_get_ht_goals')) {
  function core_base_foot_get_ht_goals(array $fx): array {
    $h = $fx['score']['halftime']['home'] ?? null;
    $a = $fx['score']['halftime']['away'] ?? null;
    if (is_numeric($h) && is_numeric($a)) return [(int)$h, (int)$a];
    return [null, null];
  }
}

if (!function_exists('core_base_foot_is_finished_fixture')) {
  function core_base_foot_is_finished_fixture(array $fx): bool {
    $st = (string)($fx['fixture']['status']['short'] ?? '');
    if (!$st || !core_base_foot_is_finished_status($st)) return false;
    [$h,$a] = core_base_foot_get_ft_goals($fx);
    return (is_int($h) && is_int($a));
  }
}
if (!function_exists('core_base_foot_filter_finished_fixtures')) {
  function core_base_foot_filter_finished_fixtures(array $fixtures): array {
    $out = [];
    foreach ($fixtures as $fx) {
      if (is_array($fx) && core_base_foot_is_finished_fixture($fx)) $out[] = $fx;
    }
    return $out;
  }
}
if (!function_exists('core_base_foot_sort_fixtures_desc')) {
  function core_base_foot_sort_fixtures_desc(array &$fixtures): void {
    usort($fixtures, fn($a,$b) => core_base_foot_fixture_ts($b) <=> core_base_foot_fixture_ts($a));
  }
}
if (!function_exists('core_base_foot_season_date_range')) {
  function core_base_foot_season_date_range(int $season): array {
    $from = sprintf('%04d-07-01', (int)$season);
    $to   = core_base_foot_now()->format('Y-m-d');
    return [$from, $to];
  }
}
if (!function_exists('core_base_foot_format_match_datetime')) {
  function core_base_foot_format_match_datetime(string $iso, string $tz = CORE_BASE_FOOT_TZ): string {
    try {
      $dt = core_base_foot_dt($iso, $tz);
      $fr = $dt->format('D d/m H:i');
      $map = ['Mon'=>'lun.','Tue'=>'mar.','Wed'=>'mer.','Thu'=>'jeu.','Fri'=>'ven.','Sat'=>'sam.','Sun'=>'dim.'];
      $p = substr($fr, 0, 3);
      return (isset($map[$p]) ? $map[$p] : $p) . substr($fr, 3);
    } catch (Throwable $e) { return $iso; }
  }
}

/* (OPTION) Traductions */
if (!function_exists('core_base_foot_translate_round')) {
  function core_base_foot_translate_round(string $round): string {
    $r = $round;
    $r = str_ireplace(['Regular Season','League','Round','Stage','Group'], ['Saison régulière','Ligue','Journée','Phase','Groupe'], $r);
    return $r;
  }
}
if (!function_exists('core_base_foot_translate_status')) {
  function core_base_foot_translate_status(string $short, string $long = ''): array {
    $mapShort = [
      'NS'=>'À venir','TBD'=>'À définir','PST'=>'Reporté','CANC'=>'Annulé','SUSP'=>'Suspendu','INT'=>'Interrompu',
      'LIVE'=>'En direct','1H'=>'1re mi-temps','HT'=>'Mi-temps','2H'=>'2e mi-temps','ET'=>'Prolongation','PEN'=>'Tirs au but',
      'BT'=>'Pause','FT'=>'Terminé','AET'=>'Terminé (a.p.)','AWD'=>'Victoire sur tapis vert','WO'=>'Forfait'
    ];
    $mapLong = [
      'Not Started'=>'Match à venir','Time to be defined'=>'Horaire à définir','Match Finished'=>'Match terminé',
      'Match Finished After Extra Time'=>'Terminé après prolongation','Match Finished After Penalty'=>'Terminé après tirs au but',
      'Postponed'=>'Reporté','Cancelled'=>'Annulé','Suspended'=>'Suspendu','Interrupted'=>'Interrompu','In Progress'=>'En cours',
      'Halftime'=>'Mi-temps','Extra Time'=>'Prolongation','Penalty In Progress'=>'Tirs au but',
    ];
    return [
      'short_fr' => $mapShort[$short] ?? ($short ?: '—'),
      'long_fr'  => $mapLong[$long] ?? ($long ?: ''),
    ];
  }
}
if (!function_exists('core_base_foot_translate_injury_text')) {
  function core_base_foot_translate_injury_text(string $text): string {
    $t = trim($text);
    if ($t === '') return $t;
    static $tr = [
      'Broken ankle'=>'Cheville cassée','Illness'=>'Maladie','Knee Injury'=>'Blessure au genou','Knock'=>'Coup',
      'Muscle Injury'=>'Blessure musculaire','Hamstring Injury'=>'Blessure aux ischio-jambiers',
      'Foot Injury'=>'Blessure au pied','Groin Injury'=>'Blessure à l’aine','Head Injury'=>'Blessure à la tête',
      'Calf Injury'=>'Blessure au mollet','Thigh Injury'=>'Blessure à la cuisse','Back Injury'=>'Blessure au dos',
      'Red Card'=>'Carton rouge','Ankle Injury'=>'Blessure à la cheville','Toe Injury'=>'Blessure à l’orteil',
      'Achilles Tendon Injury'=>'Blessure au tendon d’Achille','Finger Injury'=>'Blessure au doigt','Leg Injury'=>'Blessure à la jambe',
      'Injury'=>'Blessure','Unknown'=>'Inconnu',"Jumper's knee"=>'Tendinite rotulienne','Jumpers knee'=>'Tendinite rotulienne',
      'Suspension'=>'Suspension','Doubtful'=>'Incertain','Questionable'=>'Incertain','Out'=>'Absent','Injured'=>'Blessé',
      'Personal reasons'=>'Raisons personnelles',
    ];
    if (isset($tr[$t])) return $tr[$t];
    foreach ($tr as $en=>$fr) if (strcasecmp($en, $t) === 0) return $fr;
    return $text;
  }
}

/* =========================
   API (cache + erreurs lisibles)
   ✅ Optimisations :
   - ksort($params) => URL stable => cache stable
   - cache aussi WP_Error / JSON invalide (TTL court)
   - 429 => TTL basé sur Retry-After (clamp 30..600)
   - HTTP>=400 => cache 90s
   - code=200 + errors => cache 90s (anti-spam)
   ========================= */
if (!function_exists('core_base_foot_api')) {
  function core_base_foot_api(string $path, array $params = [], int $ttl = 900): array {

    // ---- build url (stable)
    $path = '/' . ltrim($path, '/');
    $url  = 'https://' . CORE_BASE_FOOT_APISPORTS_HOST . $path;

    if (!empty($params)) {
      // ✅ IMPORTANT: ordre stable => cache stable
      ksort($params);
      $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($params);
    }

    $ck = 'api:' . $url;

    // cache hit
    $cached = core_base_foot_cache_get($ck);
    if (is_array($cached)) return $cached;

    // call
    $res = wp_remote_get($url, [
      'timeout' => 25,
      'headers' => [
        'x-apisports-key' => CORE_BASE_FOOT_APISPORTS_KEY,
        // 'x-rapidapi-host' => CORE_BASE_FOOT_APISPORTS_HOST, // (optionnel) si tu passes par RapidAPI
      ],
    ]);

    // ✅ cache aussi WP_Error (anti spam)
    if (is_wp_error($res)) {
      $out = ['_error' => $res->get_error_message(), '_url' => $url];
      core_base_foot_cache_set($ck, $out, 60);
      return $out;
    }

    $code = (int) wp_remote_retrieve_response_code($res);
    $body = (string) wp_remote_retrieve_body($res);
    $json = json_decode($body, true);

    // ✅ 429 : TTL adaptatif si Retry-After est fourni
    if ($code === 429) {
      $retry = wp_remote_retrieve_header($res, 'retry-after');
      $ttl429 = 60;

      if (is_array($retry)) $retry = reset($retry);
      if (is_numeric($retry)) {
        // clamp 30s..10min
        $ttl429 = max(30, min(600, (int)$retry));
      }

      $out = [
        '_error' => 'Rate limit (429)',
        '_code'  => 429,
        '_url'   => $url,
        '_retry_after' => (is_scalar($retry) && $retry !== '') ? (string)$retry : null,
      ];
      core_base_foot_cache_set($ck, $out, $ttl429);
      return $out;
    }

    // HTTP error
    if ($code >= 400) {
      $out = ['_error' => "HTTP $code", '_code' => $code, '_url' => $url];
      if (is_array($json) && !empty($json['errors'])) $out['_errors'] = $json['errors'];
      else $out['_raw'] = substr($body, 0, 220);
      core_base_foot_cache_set($ck, $out, 90);
      return $out;
    }

    // ✅ cache JSON invalide (anti spam)
    if (!is_array($json)) {
      $out = [
        '_error' => 'JSON invalide',
        '_code'  => $code,
        '_url'   => $url,
        '_raw'   => substr($body, 0, 220)
      ];
      core_base_foot_cache_set($ck, $out, 90);
      return $out;
    }

    // ✅ PATCH D : API-FOOTBALL peut répondre code=200 + errors -> cache TTL court anti-spam
    if (!empty($json['errors']) && is_array($json['errors'])) {
      $out = [
        '_error'  => 'API_ERRORS',
        '_code'   => $code,
        '_url'    => $url,
        '_errors' => $json['errors'],
        '_raw'    => null,
      ];
      core_base_foot_cache_set($ck, $out, 90);
      return $out;
    }

    // OK
    $json['_http_code'] = $code;
    $json['_url'] = $url;
    core_base_foot_cache_set($ck, $json, $ttl);
    return $json;
  }
}

if (!function_exists('core_base_foot_get_fixtures_window')) {
  function core_base_foot_get_fixtures_window(array $league_ids = [], int $season = 0, string $tz = CORE_BASE_FOOT_TZ): array {
    if (!$league_ids) $league_ids = array_values(array_filter(array_map('intval', explode(',', CORE_BASE_FOOT_LEAGUE_IDS))));
    if (!$season) $season = (int)CORE_BASE_FOOT_SEASON;

    $today = core_base_foot_now(); $today->setTime(0,0,0);
    $cut   = (clone $today); $cut->modify('+1 day')->setTime(10,0,0);

    $from = $today->format('Y-m-d');
    $to   = (clone $today)->modify('+1 day')->format('Y-m-d');

    $all = [];
    $dbg_errors = [];

    foreach ($league_ids as $league) {
      $r = core_base_foot_api('fixtures', [
        'league' => (int)$league,
        'season' => $season,
        'from' => $from,
        'to' => $to,
        'timezone' => $tz
      ], 120);

      if (!empty($r['_error'])) {
        // ✅ on ne pollue pas $all
        $dbg_errors[] = [
          'league' => (int)$league,
          'error'  => $r['_error'],
          'code'   => $r['_code'] ?? null,
          'url'    => $r['_url'] ?? '',
          'errors' => $r['_errors'] ?? null,
        ];
        continue;
      }

      foreach (($r['response'] ?? []) as $fx) {
        $dateStr = $fx['fixture']['date'] ?? null;
        if (!$dateStr) continue;

        try { $dt = core_base_foot_dt($dateStr, $tz); }
        catch (Throwable $e) { continue; }

        if ($dt < $today || $dt > $cut) continue;
        $all[] = $fx;
      }
    }

    usort($all, fn($a,$b) => core_base_foot_fixture_ts($a) <=> core_base_foot_fixture_ts($b));

    // ✅ option debug (ne casse pas le return)
    if (!empty($dbg_errors)) {
      core_base_foot_cache_set('fixtures_window:_debug_errors', $dbg_errors, 180);
    }

    return $all;
  }
}


/* ============================================================
 * ✅ TEAM STATS (teams/statistics) — Saison + Dom/Ext
 * ============================================================ */
if (!function_exists('core_base_foot_get_team_stats')) {
  function core_base_foot_get_team_stats(int $team_id, int $league_id, int $season = 0): array {
    if (!$season) $season = (int)CORE_BASE_FOOT_SEASON;

    $ck = "teamstats:league=$league_id:season=$season:team=$team_id";
    $cached = core_base_foot_cache_get($ck);
    if (is_array($cached)) return $cached;

    $data = core_base_foot_api('teams/statistics', ['season'=>$season,'team'=>$team_id,'league'=>$league_id], CORE_BASE_FOOT_CACHE_12H);
    if (!empty($data['_error'])) return core_base_foot_cache_set($ck, $data, 300);

    $data['team_logo'] = $data['response']['team']['logo'] ?? '';
    return core_base_foot_cache_set($ck, $data, CORE_BASE_FOOT_CACHE_12H);
  }
}
if (!function_exists('core_base_foot_extract_bucket_from_teamstats')) {
  function core_base_foot_extract_bucket_from_teamstats(array $teamStatsApi, string $scope): array {
    $resp = $teamStatsApi['response'] ?? [];

    $played = (int)($resp['fixtures']['played'][$scope] ?? 0);
    $wins   = (int)($resp['fixtures']['wins'][$scope] ?? 0);
    $draws  = (int)($resp['fixtures']['draws'][$scope] ?? 0);
    $losses = (int)($resp['fixtures']['loses'][$scope] ?? 0);

    $points = ($wins * 3) + $draws;
    $ppm = $played ? round($points / $played, 2) : 0.0;

    $gfAvg = $resp['goals']['for']['average'][$scope] ?? null;
    $gaAvg = $resp['goals']['against']['average'][$scope] ?? null;

    $gfTotalRaw = $resp['goals']['for']['total'][$scope] ?? 0;
    $gaTotalRaw = $resp['goals']['against']['total'][$scope] ?? 0;
    $gfTotal = is_numeric($gfTotalRaw) ? (float)$gfTotalRaw : 0.0;
    $gaTotal = is_numeric($gaTotalRaw) ? (float)$gaTotalRaw : 0.0;

    $gfpm = is_numeric($gfAvg) ? (float)$gfAvg : ($played ? round($gfTotal / $played, 2) : 0.0);
    $gapm = is_numeric($gaAvg) ? (float)$gaAvg : ($played ? round($gaTotal / $played, 2) : 0.0);

    $cleanSheet    = (int)($resp['clean_sheet'][$scope] ?? 0);
    $failedToScore = (int)($resp['failed_to_score'][$scope] ?? 0);

    $score1pct   = $played ? (int)round((($played - $failedToScore) / $played) * 100) : 0;
    $concede1pct = $played ? (int)round((($played - $cleanSheet) / $played) * 100) : 0;

    return [
      'played'=>$played,'ppm'=>$ppm,'gfpm'=>round((float)$gfpm,2),'gapm'=>round((float)$gapm,2),
      'score1pct'=>$score1pct,'concede1pct'=>$concede1pct,'wins'=>$wins,'draws'=>$draws,'losses'=>$losses,
    ];
  }
}

/* ============================================================
 * ✅ LAST 5 — fixtures?last=XX (SANS league) puis filtre local FT/AET/PEN + filtre ligue local
 * ============================================================ */
if (!function_exists('core_base_foot_get_last5_fixtures')) {
  function core_base_foot_get_last5_fixtures(int $team_id, int $league_id, int $season = 0): array {
    if (!$season) $season = (int)CORE_BASE_FOOT_SEASON;

    $ck = "last5:league=$league_id:season=$season:team=$team_id";
    $cached = core_base_foot_cache_get($ck);
    if (is_array($cached)) return $cached;

    $dbg = [];

    $pick_lastN = function(array $response, int $want, int $league_id) {
      $fx = core_base_foot_filter_finished_fixtures($response);
      if ($league_id > 0) {
        $fx = array_values(array_filter($fx, fn($x)=> (int)($x['league']['id'] ?? 0) === (int)$league_id));
      }
      core_base_foot_sort_fixtures_desc($fx);
      return array_slice($fx, 0, $want);
    };

    // ✅ call unique sans league (évite erreurs API avec last+league)
    $r1 = core_base_foot_api('fixtures', ['team'=>$team_id,'season'=>$season,'last'=>80,'timezone'=>CORE_BASE_FOOT_TZ], CORE_BASE_FOOT_CACHE_12H);
    $dbg[] = ['mode'=>'last80_no_league','url'=>$r1['_url'] ?? '','err'=>$r1['_error'] ?? '','errors'=>$r1['_errors'] ?? null];
    $fx = empty($r1['_error']) ? $pick_lastN($r1['response'] ?? [], 5, $league_id) : [];

    // fallback si pas assez (rare)
    if (count($fx) < 5) {
      $r2 = core_base_foot_api('fixtures', ['team'=>$team_id,'season'=>$season,'last'=>250,'timezone'=>CORE_BASE_FOOT_TZ], CORE_BASE_FOOT_CACHE_12H);
      $dbg[] = ['mode'=>'last250_no_league','url'=>$r2['_url'] ?? '','err'=>$r2['_error'] ?? '','errors'=>$r2['_errors'] ?? null];
      if (empty($r2['_error'])) $fx = $pick_lastN($r2['response'] ?? [], 5, $league_id);
    }

  $out = ['response'=>$fx,'_debug'=>$dbg];

// s'il y a eu une erreur API (429 / API_ERRORS / HTTP...), on ne fige pas 12H
$hadErr = false;
foreach ($dbg as $row) {
  if (!empty($row['err']) || !empty($row['errors'])) { $hadErr = true; break; }
}

// cache long uniquement si on a vraiment du L5 exploitable
$ttlCache = (count($fx) >= 3 && !$hadErr) ? CORE_BASE_FOOT_CACHE_12H : 180; 
// (>=3 parce que parfois tu peux avoir 3-4 matchs : ça reste utile, et ton modèle sait pondérer via played/5)

return core_base_foot_cache_set($ck, $out, $ttlCache);
  }
}

if (!function_exists('core_base_foot_compute_last5_bucket')) {
  function core_base_foot_compute_last5_bucket(array $fixtures, int $team_id): array {
    $played=0; $points=0; $gf=0; $ga=0; $score1=0; $concede1=0;

    foreach ($fixtures as $m) {
      $homeId = (int)($m['teams']['home']['id'] ?? 0);
      $awayId = (int)($m['teams']['away']['id'] ?? 0);
      if ($homeId !== $team_id && $awayId !== $team_id) continue;

      [$hs,$as] = core_base_foot_get_ft_goals($m);
      if (!is_int($hs) || !is_int($as)) continue;

      $played++;
      $isHome = ($homeId === $team_id);
      $my = $isHome ? $hs : $as;
      $op = $isHome ? $as : $hs;

      $gf += $my; $ga += $op;
      if ($my > 0) $score1++;
      if ($op > 0) $concede1++;

      if ($my > $op) $points += 3;
      elseif ($my === $op) $points += 1;
    }

    return [
      'played'=>$played,
      'ppm'=>$played ? round($points/$played,2) : 0.0,
      'gfpm'=>$played ? round($gf/$played,2) : 0.0,
      'gapm'=>$played ? round($ga/$played,2) : 0.0,
      'score1pct'=>$played ? (int)round(($score1/$played)*100) : 0,
      'concede1pct'=>$played ? (int)round(($concede1/$played)*100) : 0,
      'points'=>$points,
    ];
  }
}

/* ============================================================
 * ✅ PACK STATS (saison/dom-ext/last5)
 * ============================================================ */
if (!function_exists('core_base_foot_build_team_match_stats')) {
  function core_base_foot_build_team_match_stats(int $team_id, int $league_id, int $season = 0): array {
    if (!$season) $season = (int)CORE_BASE_FOOT_SEASON;

    $teamStats = core_base_foot_get_team_stats($team_id, $league_id, $season);
    $last5Res  = core_base_foot_get_last5_fixtures($team_id, $league_id, $season);

    $out = [
      'team_logo' => $teamStats['team_logo'] ?? '',
      'season' => ['played'=>0,'ppm'=>0,'gfpm'=>0,'gapm'=>0,'score1pct'=>0,'concede1pct'=>0],
      'home'   => ['played'=>0,'ppm'=>0,'gfpm'=>0,'gapm'=>0,'score1pct'=>0,'concede1pct'=>0],
      'away'   => ['played'=>0,'ppm'=>0,'gfpm'=>0,'gapm'=>0,'score1pct'=>0,'concede1pct'=>0],
      'last5'  => ['played'=>0,'ppm'=>0,'gfpm'=>0,'gapm'=>0,'score1pct'=>0,'concede1pct'=>0],
      'debug'  => [
        'teamstats_error' => $teamStats['_error'] ?? '',
        'last5_debug' => $last5Res['_debug'] ?? [],
      ],
    ];

    if (empty($teamStats['_error'])) {
      $out['season'] = core_base_foot_extract_bucket_from_teamstats($teamStats, 'total');
      $out['home']   = core_base_foot_extract_bucket_from_teamstats($teamStats, 'home');
      $out['away']   = core_base_foot_extract_bucket_from_teamstats($teamStats, 'away');
    } else {
      $out['debug']['teamstats_error'] = $teamStats['_error'] ?? 'unknown';
    }

    $out['last5'] = core_base_foot_compute_last5_bucket($last5Res['response'] ?? [], $team_id);
    return $out;
  }
}

/* ============================================================
 * ✅ GOALS MATRIX (FIX IMPORTANT)
 * - IMPORTANT : API-FOOTBALL renvoie parfois une erreur avec last+league.
 *   => on appelle SANS league et on filtre la ligue localement.
 * ============================================================ */
if (!function_exists('core_base_foot_goal_matrix_init')) {
  function core_base_foot_goal_matrix_init(): array {
    return [
      'played'=>0,
      'c_over05'=>0,'c_over15'=>0,'c_over25'=>0,
      'c_under35'=>0,'c_under45'=>0,'c_under55'=>0,
      'c_btts'=>0,
      'c_ht_over15'=>0,'c_ht_under15'=>0,
      'c_score_ht'=>0,'c_concede_ht'=>0,
      'c_score_2h'=>0,'c_concede_2h'=>0,
    ];
  }
}
if (!function_exists('core_base_foot_goal_matrix_push_fixture')) {
  function core_base_foot_goal_matrix_push_fixture(array &$C, array $m, int $team_id, string $scope): void {
    $homeId = (int)($m['teams']['home']['id'] ?? 0);
    $awayId = (int)($m['teams']['away']['id'] ?? 0);
    if ($homeId !== $team_id && $awayId !== $team_id) return;

    $isHome = ($homeId === $team_id);
    if ($scope === 'home' && !$isHome) return;
    if ($scope === 'away' &&  $isHome) return;

    [$ftH,$ftA] = core_base_foot_get_ft_goals($m);
    if (!is_int($ftH) || !is_int($ftA)) return;

    $total = $ftH + $ftA;
    $C['played']++;

    if ($total >= 1) $C['c_over05']++;
    if ($total >= 2) $C['c_over15']++;
    if ($total >= 3) $C['c_over25']++;
    if ($total <= 3) $C['c_under35']++;
    if ($total <= 4) $C['c_under45']++;
    if ($total <= 5) $C['c_under55']++;
    if ($ftH > 0 && $ftA > 0) $C['c_btts']++;

[$htH,$htA] = core_base_foot_get_ht_goals($m);
if (is_int($htH) && is_int($htA)) {

  $htTotal = $htH + $htA;
  if ($htTotal >= 2) $C['c_ht_over15']++;
  if ($htTotal <= 1) $C['c_ht_under15']++;

  $myHT = $isHome ? $htH : $htA;
  $opHT = $isHome ? $htA : $htH;
  if ($myHT > 0) $C['c_score_ht']++;
  if ($opHT > 0) $C['c_concede_ht']++;

  $myFT = $isHome ? $ftH : $ftA;
  $opFT = $isHome ? $ftA : $ftH;
  $my2H = max(0, $myFT - $myHT);
  $op2H = max(0, $opFT - $opHT);
  if ($my2H > 0) $C['c_score_2h']++;
  if ($op2H > 0) $C['c_concede_2h']++;
}
}
if (!function_exists('core_base_foot_goal_matrix_finalize')) {
  function core_base_foot_goal_matrix_finalize(array $C): array {
    $played = (int)($C['played'] ?? 0);
    $pct = fn(int $c) => $played ? (int)round(($c/$played)*100) : 0;

    return [
      'played'=>$played,
      'over_0_5'=>$pct((int)$C['c_over05']),
      'over_1_5'=>$pct((int)$C['c_over15']),
      'over_2_5'=>$pct((int)$C['c_over25']),
      'under_3_5'=>$pct((int)$C['c_under35']),
      'under_4_5'=>$pct((int)$C['c_under45']),
      'under_5_5'=>$pct((int)$C['c_under55']),
      'btts'=>$pct((int)$C['c_btts']),
      'ht_over_1_5'=>$pct((int)$C['c_ht_over15']),
      'ht_under_1_5'=>$pct((int)$C['c_ht_under15']),
      'score_ht'=>$pct((int)$C['c_score_ht']),
      'concede_ht'=>$pct((int)$C['c_concede_ht']),
      'score_2h'=>$pct((int)$C['c_score_2h']),
      'concede_2h'=>$pct((int)$C['c_concede_2h']),
    ];
  }
}
/* ============================================================
 * ✅ GOALS MATRIX (NEW) — sans last=
 * - n>0 : prend les N derniers matchs terminés (FT/AET/PEN)
 * - n=0 : “saison complète” (tous les terminés)
 *
 * ✅ PATCH C :
 * - Réutilise core_base_foot_get_finished_matches_for_team()
 *   -> cohérence cache + tri + filtre + zéro doublon
 * ============================================================ */
if (!function_exists('core_base_foot_build_team_goal_matrix')) {
  function core_base_foot_build_team_goal_matrix(
    int $team_id,
    int $league_id,
    int $season = 0,
    int $n = 20,
    string $scope = 'all'
  ): array {
    if (!$season) $season = (int) CORE_BASE_FOOT_SEASON;
    $scope = in_array($scope, ['all','home','away'], true) ? $scope : 'all';
    $n = max(0, (int)$n);

    $ck = "goalmatrix2:team=$team_id:league=$league_id:season=$season:n=$n:scope=$scope";
    $cached = core_base_foot_cache_get($ck);
    if (is_array($cached)) return $cached;

    $C = core_base_foot_goal_matrix_init();

    // debug (on garde la structure, mais plus besoin des urls/errors status)
    $dbg = [
      'mode'  => ($n > 0 ? 'lastN_helper' : 'season_helper'),
      'urls'  => [],     // gardé pour compat éventuelle
      'errors'=> [],     // gardé pour compat éventuelle
      'taken' => 0
    ];

    // ✅ PATCH C : une seule source cohérente (cache + filtre finis + ligue + tri desc)
    $fx = core_base_foot_get_finished_matches_for_team($team_id, $league_id, $season, ['FT','AET','PEN']);

    // slice N si demandé
    $pick = ($n > 0) ? array_slice($fx, 0, $n) : $fx;
    $dbg['taken'] = count($pick);

    foreach ($pick as $m) {
      core_base_foot_goal_matrix_push_fixture($C, $m, $team_id, $scope);
    }

    $out = core_base_foot_goal_matrix_finalize($C);
    $out['_debug'] = $dbg;

    // cache plus long si on a des données
    core_base_foot_cache_set($ck, $out, ($out['played'] > 0 ? CORE_BASE_FOOT_CACHE_12H : 120));
    return $out;
  }
}

/* ============================================================
 * ✅ SIDELINED / SUSPENSIONS
 * ============================================================ */
if (!function_exists('core_base_foot_get_sidelined_by_player')) {
  function core_base_foot_get_sidelined_by_player(int $player_id, int $ttl = CORE_BASE_FOOT_CACHE_12H): array {
    if ($player_id <= 0) return [];
    $ck = "sidelined:player=$player_id";
    $cached = core_base_foot_cache_get($ck);
    if (is_array($cached)) return $cached;

    $r = core_base_foot_api('sidelined', ['player'=>$player_id], $ttl);
    if (!empty($r['_error'])) return core_base_foot_cache_set($ck, ['_error'=>$r['_error'],'_url'=>$r['_url'] ?? '','_errors'=>$r['_errors'] ?? null], 300);

    $out = [];
    foreach (($r['response'] ?? []) as $row) {
      if (!is_array($row)) continue;
      $type = (string)($row['type'] ?? '');
      if ($type === '') continue;
      $out[] = ['type'=>$type,'start'=>($row['start'] ?? null) ?: null,'end'=>($row['end'] ?? null) ?: null];
    }
    return core_base_foot_cache_set($ck, $out, $ttl);
  }
}
if (!function_exists('core_base_foot_extract_player_suspensions')) {
  function core_base_foot_extract_player_suspensions(array $rows, bool $only_active = true, string $tz = CORE_BASE_FOOT_TZ): array {
    try { $now = new DateTime('now', new DateTimeZone($tz)); } catch (Throwable $e) { $now = new DateTime('now'); }

    $out = [];
    foreach ($rows as $r) {
      if (!is_array($r)) continue;
      $type = (string)($r['type'] ?? '');
      if ($type === '' || stripos($type, 'suspend') === false) continue;

      $startS = $r['start'] ?? null;
      $endS   = $r['end'] ?? null;

      $start = null; $end = null;
      try { if ($startS) $start = new DateTime((string)$startS, new DateTimeZone($tz)); } catch (Throwable $e) {}
      try { if ($endS)   $end   = new DateTime((string)$endS,   new DateTimeZone($tz)); } catch (Throwable $e) {}

      if ($only_active) {
        $okStart = $start ? ($start <= $now) : true;
        $okEnd   = $end   ? ($end >= $now)   : true;
        if (!($okStart && $okEnd)) continue;
      }

      $out[] = ['type'=>$type,'start'=>$startS ?: null,'end'=>$endS ?: null];
    }

    usort($out, fn($a,$b) => strcmp((string)($b['start'] ?? ''), (string)($a['start'] ?? '')));
    return $out;
  }
}
if (!function_exists('core_base_foot_enrich_injuries_with_suspensions')) {
  function core_base_foot_enrich_injuries_with_suspensions(array $injuries, bool $only_active = true): array {
    if (!$injuries) return $injuries;

    foreach ($injuries as &$inj) {
      if (!is_array($inj)) continue;
      $pid = (int)($inj['player']['id'] ?? $inj['player']['player_id'] ?? $inj['player_id'] ?? 0);
      if ($pid <= 0) continue;

      $sid = core_base_foot_get_sidelined_by_player($pid);
      if (!is_array($sid) || !empty($sid['_error'])) continue;

      $susp = core_base_foot_extract_player_suspensions($sid, $only_active, CORE_BASE_FOOT_TZ);
      $inj['suspension_active'] = !empty($susp);
      $inj['suspension'] = $susp[0] ?? null;
      $inj['suspensions_all'] = $susp;
    }
    unset($inj);

    return $injuries;
  }
}

/* ============================================================
 * AUTRES FETCHERS
 * ============================================================ */
if (!function_exists('core_base_foot_get_standings')) {
  function core_base_foot_get_standings(int $league_id, int $season = 0): array {
    if (!$season) $season = (int)CORE_BASE_FOOT_SEASON;
    $r = core_base_foot_api('standings', ['league'=>$league_id,'season'=>$season], CORE_BASE_FOOT_CACHE_12H);
    if (!empty($r['_error'])) return ['_error'=>$r['_error'],'_url'=>$r['_url'] ?? '','_errors'=>$r['_errors'] ?? null];
    return $r['response'][0]['league']['standings'][0] ?? [];
  }
}
if (!function_exists('core_base_foot_get_injuries')) {
  function core_base_foot_get_injuries(int $fixture_id, string $tz = CORE_BASE_FOOT_TZ): array {
    $r = core_base_foot_api('injuries', ['fixture'=>$fixture_id,'timezone'=>$tz], CORE_BASE_FOOT_CACHE_15M);
    if (!empty($r['_error'])) return ['_error'=>$r['_error'],'_url'=>$r['_url'] ?? '','_errors'=>$r['_errors'] ?? null];
    return $r['response'] ?? [];
  }
}
if (!function_exists('core_base_foot_get_injuries_with_suspensions')) {
  function core_base_foot_get_injuries_with_suspensions(int $fixture_id, string $tz = CORE_BASE_FOOT_TZ, bool $only_active = true): array {
    $inj = core_base_foot_get_injuries($fixture_id, $tz);
    if (!is_array($inj) || !empty($inj['_error'])) return $inj;
    return core_base_foot_enrich_injuries_with_suspensions($inj, $only_active);
  }
}
if (!function_exists('core_base_foot_get_lineups')) {
  function core_base_foot_get_lineups(int $fixture_id): array {
    $r = core_base_foot_api('fixtures/lineups', ['fixture'=>$fixture_id], CORE_BASE_FOOT_CACHE_15M);
    if (!empty($r['_error'])) return ['_error'=>$r['_error'],'_url'=>$r['_url'] ?? '','_errors'=>$r['_errors'] ?? null];
    return $r['response'] ?? [];
  }
}
if (!function_exists('core_base_foot_get_h2h_seasons')) {
  function core_base_foot_get_h2h_seasons(int $home_id, int $away_id, array $seasons = [], string $tz = CORE_BASE_FOOT_TZ): array {
    $r = core_base_foot_api('fixtures/headtohead', ['h2h'=>"$home_id-$away_id",'last'=>50,'timezone'=>$tz], CORE_BASE_FOOT_CACHE_12H);
    if (!empty($r['_error'])) return ['_error'=>$r['_error'],'_url'=>$r['_url'] ?? '','_errors'=>$r['_errors'] ?? null];

    $resp = $r['response'] ?? [];
    if (!$resp) return [];

    core_base_foot_sort_fixtures_desc($resp);

    $seasons = array_values(array_filter(array_map('intval', $seasons)));
    if (!$seasons) return $resp;

    $set = array_flip($seasons);
    $keep = [];
    foreach ($resp as $fx) {
      $s = (int)($fx['league']['season'] ?? 0);
      if ($s && isset($set[$s])) $keep[] = $fx;
    }
    return $keep ?: array_slice($resp, 0, 10);
  }
}
/* ============================================================
 * ✅ EXPECTED GOALS / BUTS POTENTIELS (pondérés)
 * - respecte ton point de vue :
 *   HOME xG = attaque(Home: saison+dom+L5) vs défense Away adverse (saison+away+L5)
 *   AWAY xG = attaque(Away: saison+away+L5) vs défense Home adverse (saison+dom+L5)
 *
 * Dépend de core_base_foot_build_team_match_stats() :
 *  $pack['season']['gfpm'], $pack['home']['gfpm'], $pack['away']['gfpm'], $pack['last5']['gfpm']
 *  $pack['season']['gapm'], ...
 *  $pack['season']['played'], $pack['home']['played'], $pack['away']['played'], $pack['last5']['played']
 * ============================================================ */

/** Normalise des poids après "fiabilité" (ex: last5.played<5 => L5 réduit) */
if (!function_exists('core_base_foot_effective_weights')) {
  function core_base_foot_effective_weights(int $playedSeason, int $playedHA, int $playedL5): array {
    $wS  = (float) CORE_BASE_FOOT_W_SEASON;
    $wHA = (float) CORE_BASE_FOOT_W_HA;
    $wL5 = (float) CORE_BASE_FOOT_W_L5;

    // Fiabilité
    $fS  = ($playedSeason > 0) ? 1.0 : 0.0;
    $fHA = ($playedHA     > 0) ? 1.0 : 0.0;

    // L5: si seulement 2 matchs => 2/5 de fiabilité
    $fL5 = 0.0;
    if ($playedL5 > 0) $fL5 = min(1.0, max(0.0, $playedL5 / 5.0));

    // Poids effectifs
    $eS  = $wS  * $fS;
    $eHA = $wHA * $fHA;
    $eL5 = $wL5 * $fL5;

    $sum = $eS + $eHA + $eL5;

    // Fallback si rien n’est dispo (rare)
    if ($sum <= 0) {
      $sum = max(0.000001, $wS + $wHA + $wL5);
      return [
        'season' => $wS  / $sum,
        'ha'     => $wHA / $sum,
        'l5'     => $wL5 / $sum,
        'meta'   => ['playedSeason'=>$playedSeason,'playedHA'=>$playedHA,'playedL5'=>$playedL5,'scaled'=>false]
      ];
    }

    return [
      'season' => $eS  / $sum,
      'ha'     => $eHA / $sum,
      'l5'     => $eL5 / $sum,
      'meta'   => ['playedSeason'=>$playedSeason,'playedHA'=>$playedHA,'playedL5'=>$playedL5,'scaled'=>true]
    ];
  }
}

/** Calcule une stat pondérée sur un pack (gfpm ou gapm) selon contexte home/away */
if (!function_exists('core_base_foot_weighted_stat')) {
  function core_base_foot_weighted_stat(array $pack, string $key, string $context /* home|away */): array {
    $context = ($context === 'away') ? 'away' : 'home';

    $s = (float)($pack['season'][$key] ?? 0);
    $h = (float)($pack[$context][$key] ?? 0);
    $l = (float)($pack['last5'][$key] ?? 0);

    $pS  = (int)($pack['season']['played'] ?? 0);
    $pHA = (int)($pack[$context]['played'] ?? 0);
    $pL5 = (int)($pack['last5']['played'] ?? 0);

    $W = core_base_foot_effective_weights($pS, $pHA, $pL5);

    $val = ($W['season'] * $s) + ($W['ha'] * $h) + ($W['l5'] * $l);

    return [
      'value'   => $val,
      'weights' => $W,
      'parts'   => ['season'=>$s, $context=>$h, 'last5'=>$l],
    ];
  }
}

/**
 * ✅ Fonction principale : buts potentiels HOME/AWAY (ton point de vue)
 * Entrée : $home_pack, $away_pack = résultat de core_base_foot_build_team_match_stats()
 * Sortie : xG_home, xG_away + breakdown
 */
if (!function_exists('core_base_foot_calc_potential_goals')) {
  function core_base_foot_calc_potential_goals(array $home_pack, array $away_pack, array $opts = []): array {
    $mix = isset($opts['mix_attack']) ? (float)$opts['mix_attack'] : 0.50; // 0.50 = moyenne simple
    if ($mix < 0) $mix = 0; if ($mix > 1) $mix = 1;
    $capMin = isset($opts['cap_min']) ? (float)$opts['cap_min'] : 0.0;
    $capMax = isset($opts['cap_max']) ? (float)$opts['cap_max'] : 6.0;

    // HOME attaque (saison + home + L5)
    $homeAtk = core_base_foot_weighted_stat($home_pack, 'gfpm', 'home');
    // AWAY défense adverse (saison + away + L5)
    $awayDef = core_base_foot_weighted_stat($away_pack, 'gapm', 'away');

    // AWAY attaque (saison + away + L5)
    $awayAtk = core_base_foot_weighted_stat($away_pack, 'gfpm', 'away');
    // HOME défense adverse (saison + home + L5)
    $homeDef = core_base_foot_weighted_stat($home_pack, 'gapm', 'home');

    // Combinaison : moyenne (ou légèrement orientée attaque si tu veux)
    // xG = mix*attaque + (1-mix)*def_adverse
    $xG_home = ($mix * (float)$homeAtk['value']) + ((1-$mix) * (float)$awayDef['value']);
    $xG_away = ($mix * (float)$awayAtk['value']) + ((1-$mix) * (float)$homeDef['value']);

    // cap sécurité (optionnel)
    $xG_home = max($capMin, min($capMax, $xG_home));
    $xG_away = max($capMin, min($capMax, $xG_away));

    return [
      'home_xg' => round($xG_home, 2),
      'away_xg' => round($xG_away, 2),

      // breakdown (super utile pour debug/affichage)
      'home' => [
        'attack_gfpm_w'   => round((float)$homeAtk['value'], 3),
        'opp_def_gapm_w'  => round((float)$awayDef['value'], 3),
        'attack_weights'  => $homeAtk['weights'],
        'def_opp_weights' => $awayDef['weights'],
        'attack_parts'    => $homeAtk['parts'],
        'def_opp_parts'   => $awayDef['parts'],
      ],
      'away' => [
        'attack_gfpm_w'   => round((float)$awayAtk['value'], 3),
        'opp_def_gapm_w'  => round((float)$homeDef['value'], 3),
        'attack_weights'  => $awayAtk['weights'],
        'def_opp_weights' => $homeDef['weights'],
        'attack_parts'    => $awayAtk['parts'],
        'def_opp_parts'   => $homeDef['parts'],
      ],

      'meta' => [
        'mix_attack' => $mix,
        'cap_min'    => $capMin,
        'cap_max'    => $capMax,
      ],
    ];
  }
}
	if (!function_exists('core_base_foot_predict_draw_xg')) {
  function core_base_foot_predict_draw_xg(array $home_pack, array $away_pack, $pot = null, array $opts = []): array {

    // --- xG
    if (!is_array($pot) || !isset($pot['home_xg']) || !isset($pot['away_xg'])) {
      $pot = function_exists('core_base_foot_calc_potential_goals')
        ? core_base_foot_calc_potential_goals($home_pack, $away_pack, $opts['xg_opts'] ?? [])
        : ['home_xg'=>0,'away_xg'=>0,'meta'=>['missing'=>true]];
    }

    $hxg = (float)($pot['home_xg'] ?? 0);
    $axg = (float)($pot['away_xg'] ?? 0);
    $txg = $hxg + $axg;

    // ✅ NEW : PPM proj pondérée (équilibre “victoires etc.”)
    $homePPM = function_exists('core_base_foot_weighted_ppm') ? core_base_foot_weighted_ppm($home_pack, 'home') : ['value'=>0];
    $awayPPM = function_exists('core_base_foot_weighted_ppm') ? core_base_foot_weighted_ppm($away_pack, 'away') : ['value'=>0];

    $hppm = (float)($homePPM['value'] ?? 0);
    $appm = (float)($awayPPM['value'] ?? 0);

    $absDiffXG  = abs($hxg - $axg);
    $absDiffPPM = abs($hppm - $appm);

    // ✅ TES SEUILS (obligatoires)
    $diff_xg_thr  = (float)($opts['diff_xg_thr']  ?? 0.20);
    $diff_ppm_thr = (float)($opts['diff_ppm_thr'] ?? 0.40);

    // (Option) garde-fou proba de nul via matrice xG (recommandé)
    // Si tu veux le désactiver -> mets draw_prob_thr = 0
    $maxG          = (int)($opts['maxG'] ?? 6);
    $draw_prob_thr = (float)($opts['draw_prob_thr'] ?? 0.26);

    $p_draw = null;
    if (function_exists('core_base_foot_score_matrix')) {
      $M = core_base_foot_score_matrix($hxg, $axg, $maxG);
      $p_draw = isset($M['p_draw']) ? (float)$M['p_draw'] : null;
    }

    // Fiabilité L5 (comme ailleurs)
    $relL5 = function_exists('core_base_foot_l5_reliability_pair')
      ? core_base_foot_l5_reliability_pair($home_pack, $away_pack)
      : 0.50;

    // ✅ Eligibilité finale
    $ok = ($absDiffXG <= $diff_xg_thr) && ($absDiffPPM <= $diff_ppm_thr);

    // garde-fou proba (si activé)
    if ($ok && $draw_prob_thr > 0 && is_numeric($p_draw)) {
      $ok = ($p_draw >= $draw_prob_thr);
    }

    if (!$ok) {
      return [
        'ok' => false,
        'prediction' => 'Indécis',
        'confidence_pct' => null,
        'confidence_level' => null,
        'vals' => [
          'home_xg' => round($hxg,2),
          'away_xg' => round($axg,2),
          'abs_diff_xg' => round($absDiffXG,2),
          'diff_xg_thr' => $diff_xg_thr,

          'home_ppm_proj' => round($hppm,2),
          'away_ppm_proj' => round($appm,2),
          'abs_diff_ppm'  => round($absDiffPPM,2),
          'diff_ppm_thr'  => $diff_ppm_thr,

          'p_draw' => is_numeric($p_draw) ? round((float)$p_draw,3) : null,
          'draw_prob_thr' => $draw_prob_thr,
        ],
        'meta' => ['mode'=>'INDECIS', 'reliability_l5'=>round($relL5,3)],
      ];
    }

    // --- Confiance : plus c’est serré en xG & PPM, plus ça monte (+ p_draw si dispo)
    $clamp = fn($v,$min,$max)=> (function_exists('core_base_foot_clamp_float') ? core_base_foot_clamp_float((float)$v,$min,$max) : max($min, min($max, (float)$v)));

    $score_xg  = 1.0 - $clamp($absDiffXG  / max(0.0001,$diff_xg_thr),  0.0, 1.0); // 1 si parfait
    $score_ppm = 1.0 - $clamp($absDiffPPM / max(0.0001,$diff_ppm_thr), 0.0, 1.0);

    $score_draw = 0.5; // neutre si pas de matrice
    if (is_numeric($p_draw) && $draw_prob_thr > 0) {
      // map p_draw entre thr..thr+0.12 => 0..1
      $score_draw = $clamp(((float)$p_draw - $draw_prob_thr) / 0.12, 0.0, 1.0);
    }

    // mix (xG prioritaire, puis PPM, puis p_draw)
    $mix = (0.45*$score_xg) + (0.35*$score_ppm) + (0.20*$score_draw);

    $pct_raw = (int)round(62 + ($mix * (90 - 62)));        // 62..90
    $pct_raw = (int)$clamp($pct_raw, 60, 90);

// ✅ adoucit la pénalité L5 (0.75 = léger boost, 0.65 = boost + fort)
$relExp = (float)($opts['rel_exp'] ?? 0.65);
$relAdj = pow(max(0.01, $relL5), $relExp);

// ✅ même logique, mais moins punitive quand relL5 est moyen
$pct = (int)round(50 + (($pct_raw - 50) * $relAdj));
$pct = (int)$clamp($pct, 55, 90);
	  
	  // ✅ bonus 0..4 si très serré (score_xg/score_ppm proches de 1)
$close = (0.5 * $score_xg) + (0.5 * $score_ppm); // 0..1
$bonus = (int)round(4 * $clamp(($close - 0.40) / 0.60, 0.0, 1.0)); // bonus si close>0.40
$pct = (int)$clamp($pct + $bonus, 55, 90);


    return [
      'ok' => true,
      'prediction' => 'OUI',
      'confidence_pct' => $pct,
      'confidence_level' => function_exists('core_base_foot_pred_level_from_pct') ? core_base_foot_pred_level_from_pct($pct) : null,
      'vals' => [
        'home_xg' => round($hxg,2),
        'away_xg' => round($axg,2),
        'abs_diff_xg' => round($absDiffXG,2),
        'diff_xg_thr' => $diff_xg_thr,

        'home_ppm_proj' => round($hppm,2),
        'away_ppm_proj' => round($appm,2),
        'abs_diff_ppm'  => round($absDiffPPM,2),
        'diff_ppm_thr'  => $diff_ppm_thr,

        'p_draw' => is_numeric($p_draw) ? round((float)$p_draw,3) : null,
        'draw_prob_thr' => $draw_prob_thr,
      ],
      'meta' => ['mode'=>'DRAW', 'reliability_l5'=>round($relL5,3), 'maxG'=>$maxG],
    ];
  }
}

/* ============================================================
 * ✅ PRÉDICTION DOUBLE CHANCE (1X / X2) via PROJECTION PPM pondérée
 * - PPM pondérée : Saison + (Home/Away) + Last5
 * - Si |diff| <= 0.30 => pas de prédiction
 * - Confiance % dérivée du diff (et fiabilité des datas)
 * ============================================================ */

if (!function_exists('core_base_foot_clamp_float')) {
  function core_base_foot_clamp_float(float $v, float $min, float $max): float {
    if ($v < $min) return $min;
    if ($v > $max) return $max;
    return $v;
  }
}

/** Fiabilité des datas (0..1) selon le volume d’infos dispo */
if (!function_exists('core_base_foot_reliability_factor')) {
  function core_base_foot_reliability_factor(int $playedSeason, int $playedHA, int $playedL5): float {
    // saison : si >=10 matchs => 1.0
    $fs  = ($playedSeason > 0) ? min(1.0, max(0.0, $playedSeason / 10.0)) : 0.0;
    // dom/away : si >=5 matchs => 1.0
    $fha = ($playedHA > 0)     ? min(1.0, max(0.0, $playedHA / 5.0))     : 0.0;
    // last5 : si =5 => 1.0
    $fl5 = ($playedL5 > 0)     ? min(1.0, max(0.0, $playedL5 / 5.0))     : 0.0;

    // Pondération fiabilité (tu peux ajuster)
    $rel = (0.45 * $fs) + (0.25 * $fha) + (0.30 * $fl5);

    // On évite 0 absolu si au moins une donnée existe
    if ($rel <= 0 && ($playedSeason>0 || $playedHA>0 || $playedL5>0)) $rel = 0.20;

    return core_base_foot_clamp_float($rel, 0.0, 1.0);
  }
}

/** Convertit un diff PPM en % confiance + niveau */
if (!function_exists('core_base_foot_confidence_from_ppm_diff')) {
  function core_base_foot_confidence_from_ppm_diff(float $absDiff, float $reliability = 1.0): array {
    // On part d’un plancher à 60% dès qu’on dépasse 0.30
    // et on monte linéairement : à 0.80 diff => ~80% (forte)
    $base = 60.0 + max(0.0, ($absDiff - 0.30)) * 40.0; // cap ensuite
    $base = core_base_foot_clamp_float($base, 60.0, 95.0);

    // Ajuste par fiabilité (réduit la confiance si data pauvre)
    $reliability = core_base_foot_clamp_float($reliability, 0.0, 1.0);
    $pct = 50.0 + (($base - 50.0) * $reliability);
    $pct = (int)round(core_base_foot_clamp_float($pct, 0.0, 99.0));

    $level = 'Faible';
    if ($pct >= 80) $level = 'Forte';
    elseif ($pct >= 61) $level = 'Moyenne';

    return ['pct' => $pct, 'level' => $level, 'reliability' => round($reliability, 3)];
  }
}
/* ============================================================
 * ✅ PPM PROJECTION + PREDICTORS (CORE)
 * - Projection PPM pondérée (season + home/away + last5)
 * - Double chance (1X / X2)
 * - Victoire sèche (1 / 2)
 * ============================================================ */

/**
 * Projection PPM pondérée pour une équipe (pack issu de core_base_foot_build_team_match_stats)
 * Retourne: value + weights + parts + meta played
 */
if (!function_exists('core_base_foot_weighted_ppm')) {
  function core_base_foot_weighted_ppm(array $pack, string $context /* home|away */): array {
    $context = ($context === 'away') ? 'away' : 'home';

    // Si ton CORE a déjà core_base_foot_weighted_stat (générique), on la réutilise
    if (function_exists('core_base_foot_weighted_stat')) {
      $res = core_base_foot_weighted_stat($pack, 'ppm', $context);

      $pS  = (int)($pack['season']['played'] ?? 0);
      $pHA = (int)($pack[$context]['played'] ?? 0);
      $pL5 = (int)($pack['last5']['played'] ?? 0);

      $res['value'] = round((float)($res['value'] ?? 0), 3);
      $res['meta']  = [
        'playedSeason' => $pS,
        'playedHA'     => $pHA,
        'playedL5'     => $pL5,
      ];
      return $res;
    }

    // Fallback si weighted_stat n'existe pas (rare)
    $s  = (float)($pack['season']['ppm'] ?? 0);
    $ha = (float)($pack[$context]['ppm'] ?? 0);
    $l5 = (float)($pack['last5']['ppm'] ?? 0);

    $pS  = (int)($pack['season']['played'] ?? 0);
    $pHA = (int)($pack[$context]['played'] ?? 0);
    $pL5 = (int)($pack['last5']['played'] ?? 0);

    $W = function_exists('core_base_foot_effective_weights')
      ? core_base_foot_effective_weights($pS, $pHA, $pL5)
      : ['season'=>0.20,'ha'=>0.35,'l5'=>0.45,'meta'=>['scaled'=>false]];

    $val = ($W['season'] * $s) + ($W['ha'] * $ha) + ($W['l5'] * $l5);

    return [
      'value'   => round($val, 3),
      'weights' => $W,
      'parts'   => ['season'=>$s, $context=>$ha, 'last5'=>$l5],
      'meta'    => ['playedSeason'=>$pS, 'playedHA'=>$pHA, 'playedL5'=>$pL5],
    ];
  }
}

/**
 * Helper: fiabilité d'une équipe (0..1) selon le volume de data dans le pack
 * (réutilise core_base_foot_reliability_factor si dispo)
 */
if (!function_exists('core_base_foot_ppm_pack_reliability')) {
  function core_base_foot_ppm_pack_reliability(array $pack, string $context): float {
    $context = ($context === 'away') ? 'away' : 'home';

    $pS  = (int)($pack['season']['played'] ?? 0);
    $pHA = (int)($pack[$context]['played'] ?? 0);
    $pL5 = (int)($pack['last5']['played'] ?? 0);

    if (function_exists('core_base_foot_reliability_factor')) {
      return (float) core_base_foot_reliability_factor($pS, $pHA, $pL5);
    }

    // fallback simple
    $fs  = ($pS  > 0) ? min(1.0, max(0.0, $pS  / 10.0)) : 0.0;
    $fha = ($pHA > 0) ? min(1.0, max(0.0, $pHA / 5.0))  : 0.0;
    $fl5 = ($pL5 > 0) ? min(1.0, max(0.0, $pL5 / 5.0))  : 0.0;

    $rel = (0.45*$fs) + (0.25*$fha) + (0.30*$fl5);
    if ($rel <= 0 && ($pS>0 || $pHA>0 || $pL5>0)) $rel = 0.20;
    return max(0.0, min(1.0, $rel));
  }
}

/**
 * Helper: transforme absDiff + threshold en "signal" comparable au modèle DC (thr=0.30)
 * Astuce: si thr=1.30 (win), on "décale" de +1.00 pour retomber sur la même échelle.
 */
if (!function_exists('core_base_foot_ppm_abs_to_signal')) {
  function core_base_foot_ppm_abs_to_signal(float $absDiff, float $threshold): float {
    $baseThr = 0.30;
    $offset = max(0.0, $threshold - $baseThr);      // ex win: 1.30 -> offset=1.00
    $signal = max(0.0, $absDiff - $offset);         // ex abs=1.30 -> signal=0.30
    return $signal;
  }
}

/**
 * Helper: calcule % confiance depuis absDiff/threshold + reliability
 * Réutilise core_base_foot_confidence_from_ppm_diff() si dispo.
 */
if (!function_exists('core_base_foot_ppm_confidence')) {
  function core_base_foot_ppm_confidence(float $absDiff, float $threshold, float $reliability): array {
    $signal = core_base_foot_ppm_abs_to_signal($absDiff, $threshold);

    if (function_exists('core_base_foot_confidence_from_ppm_diff')) {
      $c = core_base_foot_confidence_from_ppm_diff($signal, $reliability);
      $c['signal_abs'] = round($signal, 3);
      return $c;
    }

    // fallback simple
    $base = 60.0 + max(0.0, ($signal - 0.30)) * 40.0; // cap ensuite
    $base = max(60.0, min(95.0, $base));

    $reliability = max(0.0, min(1.0, $reliability));
    $pct = (int)round(50.0 + (($base - 50.0) * $reliability));
    $pct = (int)max(0, min(99, $pct));

    $level = 'Faible';
    if ($pct >= 80) $level = 'Forte';
    elseif ($pct >= 61) $level = 'Moyenne';

    return ['pct'=>$pct, 'level'=>$level, 'reliability'=>round($reliability,3), 'signal_abs'=>round($signal,3)];
  }
}

/**
 * ✅ PREDICTOR: Double chance via PPM pondérée
 * - threshold par défaut: 0.30 (sinon "indécis")
 * - prediction: 1X si home>away, sinon X2
 */
if (!function_exists('core_base_foot_predict_double_chance_ppm')) {
  function core_base_foot_predict_double_chance_ppm(array $home_pack, array $away_pack, array $opts = []): array {
    $threshold = isset($opts['threshold']) ? (float)$opts['threshold'] : 0.30;

    $home = core_base_foot_weighted_ppm($home_pack, 'home');
    $away = core_base_foot_weighted_ppm($away_pack, 'away');

    $h = (float)($home['value'] ?? 0);
    $a = (float)($away['value'] ?? 0);

    $diff = $h - $a;
    $abs  = abs($diff);

    // fiabilité conservatrice (min des deux)
    $relH = core_base_foot_ppm_pack_reliability($home_pack, 'home');
    $relA = core_base_foot_ppm_pack_reliability($away_pack, 'away');
    $rel  = min($relH, $relA);

    $conf = core_base_foot_ppm_confidence($abs, $threshold, $rel);

    $ok = ($abs > $threshold);
    return [
      'ok' => $ok,
      'prediction' => $ok ? (($diff > 0) ? '1X' : 'X2') : null,
      'threshold' => $threshold,

      'home_ppm_proj' => round($h, 2),
      'away_ppm_proj' => round($a, 2),
      'diff' => round($diff, 3),
      'abs_diff' => round($abs, 3),

      'confidence_pct' => (int)($conf['pct'] ?? 50),
      'confidence_level' => (string)($conf['level'] ?? 'Faible'),
      'reliability' => (float)($conf['reliability'] ?? $rel),
      'signal_abs' => (float)($conf['signal_abs'] ?? 0),

      'home_detail' => $home,
      'away_detail' => $away,

      'meta' => ['source' => 'core_ppm_double_chance'],
    ];
  }
}

/**
 * ✅ PREDICTOR: Victoire sèche (1 / 2) — SAFE (PPM + xG + anti-nul + contexte + FORM L5)
 * - Garde la signature existante: (home_pack, away_pack, opts)
 * - ✅ MODIF : seuil PPM par défaut = 1.15 (au lieu de 1.30)
 * - ✅ MODIF : ajout garde-fou FORME : écart PPM L5 >= 1.10 + même sens que le pick
 *
 * opts:
 *  - threshold (float) : seuil abs diff PPM pondérée (défaut 1.15)
 *  - l5_ppm_thr (float) : seuil abs diff PPM sur les 5 derniers (défaut 1.10)
 *  - l5_min_played (int): min matchs dispo en L5 (défaut 4)
 *  - strict (bool)     : active mode plus "safe"
 *  - xg_diff_thr (float) : diff xG mini (défaut 0.35)
 *  - xg_min (float)      : xG mini du candidat (défaut 1.15)
 *  - p_win_thr (float)   : proba win mini (si matrice dispo) (défaut 0.47)
 *  - p_draw_max (float)  : proba nul max (si matrice dispo) (défaut 0.30)
 *  - score1pct_min (int) : score1pct mini du candidat (scope) (défaut 70)
 *  - concede1pct_min (int): concede1pct mini de l’adversaire (scope) (défaut 55)
 *  - xg_opts (array)     : options pour core_base_foot_calc_potential_goals
 *  - maxG (int)          : matrice score max goals (défaut 6)
 *  - pot (array)         : (optionnel) xG pré-calculé ['home_xg'=>..,'away_xg'=>..]
 */
if (!function_exists('core_base_foot_predict_win_ppm')) {
  function core_base_foot_predict_win_ppm(array $home_pack, array $away_pack, array $opts = []): array {

    $clamp = function(float $v, float $min, float $max): float {
      return function_exists('core_base_foot_clamp_float') ? core_base_foot_clamp_float($v,$min,$max) : max($min, min($max, $v));
    };

    $strict = !empty($opts['strict']);

    // ✅ Seuils (MODIF: win threshold par défaut = 1.15)
    $threshold = isset($opts['threshold']) ? (float)$opts['threshold'] : 1.15;

    // ✅ Nouveau garde-fou FORME (L5)
    $l5_ppm_thr    = isset($opts['l5_ppm_thr']) ? (float)$opts['l5_ppm_thr'] : 1.10;
    $l5_min_played = isset($opts['l5_min_played']) ? (int)$opts['l5_min_played'] : 4;

    $xg_diff_thr = isset($opts['xg_diff_thr']) ? (float)$opts['xg_diff_thr'] : 0.35;
    $xg_min      = isset($opts['xg_min'])      ? (float)$opts['xg_min']      : 1.15;

    $p_win_thr   = isset($opts['p_win_thr'])   ? (float)$opts['p_win_thr']   : 0.47;
    $p_draw_max  = isset($opts['p_draw_max'])  ? (float)$opts['p_draw_max']  : 0.30;

    $score1pct_min   = isset($opts['score1pct_min'])   ? (int)$opts['score1pct_min']   : 70;
    $concede1pct_min = isset($opts['concede1pct_min']) ? (int)$opts['concede1pct_min'] : 55;

    // Mode strict = encore + safe
    if ($strict) {
      $threshold       = max($threshold, 1.40);
      $l5_ppm_thr      = max($l5_ppm_thr, 1.20);
      $l5_min_played   = max($l5_min_played, 5);

      $xg_diff_thr     = max($xg_diff_thr, 0.45);
      $xg_min          = max($xg_min, 1.25);
      $p_win_thr       = max($p_win_thr, 0.50);
      $p_draw_max      = min($p_draw_max, 0.28);
      $score1pct_min   = max($score1pct_min, 75);
      $concede1pct_min = max($concede1pct_min, 60);
    }

    // --- PPM pondérée (force)
    $home = function_exists('core_base_foot_weighted_ppm') ? core_base_foot_weighted_ppm($home_pack, 'home') : ['value'=>0];
    $away = function_exists('core_base_foot_weighted_ppm') ? core_base_foot_weighted_ppm($away_pack, 'away') : ['value'=>0];

    $h = (float)($home['value'] ?? 0);
    $a = (float)($away['value'] ?? 0);

    $diff = $h - $a;
    $abs  = abs($diff);

    // fiabilité conservatrice
    $relH = function_exists('core_base_foot_ppm_pack_reliability') ? core_base_foot_ppm_pack_reliability($home_pack, 'home') : 0.5;
    $relA = function_exists('core_base_foot_ppm_pack_reliability') ? core_base_foot_ppm_pack_reliability($away_pack, 'away') : 0.5;
    $rel  = min($relH, $relA);

    // ✅ 1) filtre PPM pondérée (indispensable)
    if ($abs < $threshold) {
      return [
        'ok' => false,
        'prediction' => null,
        'threshold' => $threshold,
        'home_ppm_proj' => round($h, 2),
        'away_ppm_proj' => round($a, 2),
        'diff' => round($diff, 3),
        'abs_diff' => round($abs, 3),
        'confidence_pct' => null,
        'confidence_level' => null,
        'reliability' => round($rel, 3),
        'meta' => ['source'=>'core_ppm_win_safe','mode'=>'INDECIS_PPM','strict'=>$strict],
      ];
    }

    // Candidat
    $side = ($diff > 0) ? 'home' : 'away';
    $pick = ($side === 'home') ? '1' : '2';

    // ✅ 1bis) garde-fou FORME L5 : diff PPM >= 1.10 ET même sens que le pick
    $hL5ppm = (float)($home_pack['last5']['ppm'] ?? 0);
    $aL5ppm = (float)($away_pack['last5']['ppm'] ?? 0);
    $hL5p   = (int)($home_pack['last5']['played'] ?? 0);
    $aL5p   = (int)($away_pack['last5']['played'] ?? 0);

    $minPlayedL5 = min($hL5p, $aL5p);
    $l5diff      = $hL5ppm - $aL5ppm;
    $l5abs       = abs($l5diff);

    $l5_ok = ($minPlayedL5 >= $l5_min_played) && ($l5abs >= $l5_ppm_thr);

    // même sens que le pick (sinon contradiction: “PPM pondérée dit home”, mais L5 dit away)
    if ($l5_ok) {
      if ($side === 'home') $l5_ok = ($l5diff > 0);
      else                 $l5_ok = ($l5diff < 0);
    }

    if (!$l5_ok) {
      return [
        'ok' => false,
        'prediction' => null,
        'threshold' => $threshold,
        'home_ppm_proj' => round($h, 2),
        'away_ppm_proj' => round($a, 2),
        'diff' => round($diff, 3),
        'abs_diff' => round($abs, 3),
        'confidence_pct' => null,
        'confidence_level' => null,
        'reliability' => round($rel, 3),
        'vals' => [
          'side' => $side,
          'home_l5_ppm' => round($hL5ppm, 2),
          'away_l5_ppm' => round($aL5ppm, 2),
          'l5_abs_diff' => round($l5abs, 2),
          'l5_ppm_thr'  => $l5_ppm_thr,
          'l5_min_played' => $l5_min_played,
          'l5_min_played_actual' => $minPlayedL5,
          'l5_same_direction' => ($side === 'home') ? ($l5diff > 0) : ($l5diff < 0),
        ],
        'meta' => ['source'=>'core_ppm_win_safe','mode'=>'INDECIS_L5','strict'=>$strict],
      ];
    }

    // --- xG (matchup)
    $pot = (is_array($opts['pot'] ?? null) && isset($opts['pot']['home_xg'], $opts['pot']['away_xg']))
      ? (array)$opts['pot']
      : (function_exists('core_base_foot_calc_potential_goals')
          ? core_base_foot_calc_potential_goals($home_pack, $away_pack, (array)($opts['xg_opts'] ?? []))
          : ['home_xg'=>0,'away_xg'=>0,'meta'=>['missing'=>true]]
        );

    $hxg = (float)($pot['home_xg'] ?? 0);
    $axg = (float)($pot['away_xg'] ?? 0);

    $cand_xg = ($side === 'home') ? $hxg : $axg;
    $opp_xg  = ($side === 'home') ? $axg : $hxg;
    $xg_diff = abs($hxg - $axg);

    // --- contexte “marque” (scope correct)
    if ($side === 'home') {
      $cand_scorepct = (int)($home_pack['home']['score1pct'] ?? $home_pack['season']['score1pct'] ?? 0);
      $opp_concpct   = (int)($away_pack['away']['concede1pct'] ?? $away_pack['season']['concede1pct'] ?? 0);
    } else {
      $cand_scorepct = (int)($away_pack['away']['score1pct'] ?? $away_pack['season']['score1pct'] ?? 0);
      $opp_concpct   = (int)($home_pack['home']['concede1pct'] ?? $home_pack['season']['concede1pct'] ?? 0);
    }

    $ctx_ok = ($cand_scorepct >= $score1pct_min) && ($opp_concpct >= $concede1pct_min);

    // --- matrice score => p_win / p_draw (si dispo)
    $p_draw = null; $p_win = null;
    if (function_exists('core_base_foot_score_matrix')) {
      $maxG = (int)($opts['maxG'] ?? 6);
      $M = core_base_foot_score_matrix($hxg, $axg, $maxG);

      if (isset($M['p_draw']) && is_numeric($M['p_draw'])) $p_draw = (float)$M['p_draw'];

      $getp = function(array $arr, array $keys) {
        foreach ($keys as $k) {
          if (isset($arr[$k]) && is_numeric($arr[$k])) return (float)$arr[$k];
        }
        return null;
      };

      $p_home = $getp($M, ['p_home','p_home_win','p1','p_win_home','home_win']);
      $p_away = $getp($M, ['p_away','p_away_win','p2','p_win_away','away_win']);

      $p_win = ($side === 'home') ? $p_home : $p_away;
    }

    // ✅ 2) filtre xG (edge réel)
    $xg_ok = (
      ($cand_xg >= $xg_min) &&
      ($cand_xg > $opp_xg) &&
      ($xg_diff >= $xg_diff_thr)
    );

    // ✅ 3) filtre anti-nul
    $draw_ok = true;
    if (is_numeric($p_draw) && $p_draw_max > 0) {
      $draw_ok = ((float)$p_draw <= $p_draw_max);
    } else {
      $draw_ok = ($xg_diff >= ($xg_diff_thr + 0.10));
    }

    // ✅ 4) filtre proba win (si dispo) OU fallback xG_ok
    $pwin_ok = true;
    if (is_numeric($p_win) && $p_win_thr > 0) {
      $pwin_ok = ((float)$p_win >= $p_win_thr);
    } else {
      $pwin_ok = $xg_ok;
    }

    // Eligibilité finale
    $ok = ($xg_ok || $pwin_ok) && $draw_ok && $ctx_ok;

    if (!$ok) {
      return [
        'ok' => false,
        'prediction' => null,
        'threshold' => $threshold,
        'home_ppm_proj' => round($h, 2),
        'away_ppm_proj' => round($a, 2),
        'diff' => round($diff, 3),
        'abs_diff' => round($abs, 3),
        'confidence_pct' => null,
        'confidence_level' => null,
        'reliability' => round($rel, 3),
        'vals' => [
          'side'=>$side,

          // L5 guard (debug)
          'home_l5_ppm' => round($hL5ppm, 2),
          'away_l5_ppm' => round($aL5ppm, 2),
          'l5_abs_diff' => round($l5abs, 2),
          'l5_ppm_thr'  => $l5_ppm_thr,
          'l5_min_played_actual' => $minPlayedL5,

          // xG/ctx/proba
          'home_xg'=>round($hxg,2),
          'away_xg'=>round($axg,2),
          'cand_xg'=>round($cand_xg,2),
          'opp_xg'=>round($opp_xg,2),
          'xg_diff'=>round($xg_diff,2),
          'xg_diff_thr'=>$xg_diff_thr,
          'xg_min'=>$xg_min,
          'p_win'=>is_numeric($p_win)?round((float)$p_win,3):null,
          'p_win_thr'=>$p_win_thr,
          'p_draw'=>is_numeric($p_draw)?round((float)$p_draw,3):null,
          'p_draw_max'=>$p_draw_max,
          'cand_score1pct'=>$cand_scorepct,
          'opp_concede1pct'=>$opp_concpct,
          'score1pct_min'=>$score1pct_min,
          'concede1pct_min'=>$concede1pct_min,
        ],
        'meta' => ['source'=>'core_ppm_win_safe','mode'=>'INDECIS_GUARDS','strict'=>$strict],
      ];
    }

    // --- Confiance (mix PPM + xG + p_win) puis ajustement fiabilité
    $ppmConf = function_exists('core_base_foot_ppm_confidence')
      ? core_base_foot_ppm_confidence($abs, $threshold, $rel)
      : ['pct'=>70,'level'=>'Moyenne','reliability'=>$rel,'signal_abs'=>0];

    $ppmScore = $clamp(((int)$ppmConf['pct'] - 60) / 35.0, 0.0, 1.0); // 60..95
    $xgEdge   = max(0.0, ($cand_xg - $opp_xg) - $xg_diff_thr);
    $xgScore  = $clamp($xgEdge / 0.70, 0.0, 1.0);

    $pwinScore = 0.5;
    if (is_numeric($p_win) && $p_win_thr > 0) {
      $pwinScore = $clamp(((float)$p_win - $p_win_thr) / 0.18, 0.0, 1.0);
    }

    // ✅ petit bonus si L5 très dominant (au-delà du thr)
    $l5Bonus = $clamp(($l5abs - $l5_ppm_thr) / 0.60, 0.0, 1.0) * 0.06; // +0..0.06 sur le mix

    $mix = (0.50*$ppmScore) + (0.30*$xgScore) + (0.20*$pwinScore);
    $mix = $clamp($mix + $l5Bonus, 0.0, 1.0);

    $pct_raw = (int)round(66 + ($mix * (92 - 66))); // 66..92
    $pct = (int)round(50 + (($pct_raw - 50) * $rel));

    if (is_numeric($p_draw)) {
      $pen = (int)round(6 * $clamp(((float)$p_draw - 0.22) / 0.10, 0.0, 1.0)); // 0..6
      $pct -= $pen;
    }

    $pct = (int)$clamp($pct, 60, 93);

    return [
      'ok' => true,
      'prediction' => $pick,
      'threshold' => $threshold,

      'home_ppm_proj' => round($h, 2),
      'away_ppm_proj' => round($a, 2),
      'diff' => round($diff, 3),
      'abs_diff' => round($abs, 3),

      'xg_home' => round($hxg, 2),
      'xg_away' => round($axg, 2),
      'p_win'   => is_numeric($p_win)  ? round((float)$p_win, 3)  : null,
      'p_draw'  => is_numeric($p_draw) ? round((float)$p_draw, 3) : null,

      'confidence_pct' => $pct,
      'confidence_level' => function_exists('core_base_foot_pred_level_from_pct')
        ? core_base_foot_pred_level_from_pct($pct)
        : ($ppmConf['level'] ?? null),

      'reliability' => round($rel, 3),
      'signal_abs' => (float)($ppmConf['signal_abs'] ?? 0),

      'home_detail' => $home,
      'away_detail' => $away,

      'vals' => [
        'side'=>$side,

        // L5 guard (debug)
        'home_l5_ppm' => round($hL5ppm, 2),
        'away_l5_ppm' => round($aL5ppm, 2),
        'l5_abs_diff' => round($l5abs, 2),
        'l5_ppm_thr'  => $l5_ppm_thr,
        'l5_min_played' => $l5_min_played,
        'l5_min_played_actual' => $minPlayedL5,

        // autres guards
        'cand_score1pct'=>$cand_scorepct,
        'opp_concede1pct'=>$opp_concpct,
        'xg_diff'=>round($xg_diff,2),
        'xg_diff_thr'=>$xg_diff_thr,
        'xg_min'=>$xg_min,
        'p_win_thr'=>$p_win_thr,
        'p_draw_max'=>$p_draw_max,
      ],

      'meta' => ['source'=>'core_ppm_win_safe','strict'=>$strict,'l5_guard'=>true],
    ];
  }
}

/* ============================================================
 * ✅ PRÉDICTION "NOMBRE DE BUTS" (O1.5 / O2.5 / U4.5)
 * Règles Damien :
 * - O1.5 si (Total xG >= 2.7 ET L5 score+concede OK des 2 équipes)
 *   OU (xG home>=1.2 ET xG away>=1.2 ET L5 score+concede OK)
 * - O2.5 si (Total xG >= 3.6 ET L5 score+concede OK strict)
 *   OU (xG home>=1.8 ET xG away>=1.8 ET L5 score+concede OK strict)
 * - Sinon => U4.5
 * ============================================================ */

if (!function_exists('core_base_foot_conf_level_from_pct')) {
  function core_base_foot_conf_level_from_pct(int $pct): string {
    if ($pct >= 80) return 'Forte';
    if ($pct >= 61) return 'Moyenne';
    return 'Faible';
  }
}

if (!function_exists('core_base_foot_predict_total_goals')) {
  function core_base_foot_predict_total_goals(array $home_pack, array $away_pack, $pot = null, array $opts = []): array {

    // Si pas de pot (xG), on tente de le calculer
    if (!is_array($pot) || (!isset($pot['home_xg']) && !isset($pot['away_xg']))) {
      if (function_exists('core_base_foot_calc_potential_goals')) {
        $pot = core_base_foot_calc_potential_goals($home_pack, $away_pack, $opts['pot_opts'] ?? []);
      } else {
        $pot = ['home_xg'=>0, 'away_xg'=>0, 'meta'=>['missing'=>true]];
      }
    }

    $hxg = (float)($pot['home_xg'] ?? 0);
    $axg = (float)($pot['away_xg'] ?? 0);
    $txg = $hxg + $axg;

    // L5 métriques
    $hL5 = $home_pack['last5'] ?? [];
    $aL5 = $away_pack['last5'] ?? [];

    $h_score_pct = (int)($hL5['score1pct'] ?? 0);
    $a_score_pct = (int)($aL5['score1pct'] ?? 0);
    $h_gfpm      = (float)($hL5['gfpm'] ?? 0);
    $a_gfpm      = (float)($aL5['gfpm'] ?? 0);

    $h_conc_pct  = (int)($hL5['concede1pct'] ?? 0);
    $a_conc_pct  = (int)($aL5['concede1pct'] ?? 0);
    $h_gapm      = (float)($hL5['gapm'] ?? 0);
    $a_gapm      = (float)($aL5['gapm'] ?? 0);

    $h_played_l5 = (int)($hL5['played'] ?? 0);
    $a_played_l5 = (int)($aL5['played'] ?? 0);

    // Helpers conditions
    $bothScoreOk = function(int $pctMin, float $gfpmMin) use ($h_score_pct,$a_score_pct,$h_gfpm,$a_gfpm): bool {
      return ($h_score_pct >= $pctMin && $a_score_pct >= $pctMin && $h_gfpm >= $gfpmMin && $a_gfpm >= $gfpmMin);
    };
    $bothConcedeOk = function(int $pctMin, float $gapmMin) use ($h_conc_pct,$a_conc_pct,$h_gapm,$a_gapm): bool {
      return ($h_conc_pct >= $pctMin && $a_conc_pct >= $pctMin && $h_gapm >= $gapmMin && $a_gapm >= $gapmMin);
    };

  // --- Règles O1.5
$o15_score_ok   = $bothScoreOk(60, 1.0);
$o15_concede_ok = $bothConcedeOk(60, 1.0);

// ✅ NOUVELLE règle (Damien) : xG total élevé + les 2 équipes marquent souvent (L5 score1pct)
$o15_ruleC = ($txg >= 3.1) && ($h_score_pct >= 60) && ($a_score_pct >= 60);

$o15_ruleA = ($txg >= 2.7) && $o15_score_ok && $o15_concede_ok;
$o15_ruleB = ($hxg >= 1.2 && $axg >= 1.2) && $o15_score_ok && $o15_concede_ok;

$o15_ok = $o15_ruleA || $o15_ruleB || $o15_ruleC;

    // --- Règles O2.5
    $o25_score_ok   = $bothScoreOk(80, 1.2);
    $o25_concede_ok = $bothConcedeOk(60, 1.0);

    $o25_ruleA = ($txg >= 3.6) && $o25_score_ok && $o25_concede_ok;
    $o25_ruleB = ($hxg >= 1.8 && $axg >= 1.8) && $o25_score_ok && $o25_concede_ok;
    $o25_ok = $o25_ruleA || $o25_ruleB;

    // --- Pick final
    $pick = 'under_4_5';
    $line = 4.5;
    $label = 'Moins de 4.5 buts';
    $rule = 'fallback_under45';

    if ($o25_ok) {
      $pick = 'over_2_5'; $line = 2.5; $label = 'Plus de 2.5 buts';
      $rule = $o25_ruleA ? 'o25_totalxg' : 'o25_eachxg';
} elseif ($o15_ok) {
  $pick = 'over_1_5'; $line = 1.5; $label = 'Plus de 1.5 buts';
  $rule = $o15_ruleA ? 'o15_totalxg' : ($o15_ruleB ? 'o15_eachxg' : 'o15_totalxg_score60');
}

    // --- Confiance simple (avec fiabilité L5)
    $clamp = function(float $v, float $min, float $max): float {
      if (function_exists('core_base_foot_clamp_float')) return core_base_foot_clamp_float($v,$min,$max);
      return max($min, min($max, $v));
    };

    $relL5 = 0.20;
    $minPlayed = min($h_played_l5, $a_played_l5);
    if ($minPlayed > 0) $relL5 = $clamp($minPlayed / 5.0, 0.20, 1.0);

    // base + ajustements
    if ($pick === 'over_2_5') {
      $base = 74;
      $xgScore = $o25_ruleA ? $clamp(($txg - 3.6) / 0.9, 0, 1) : $clamp((min($hxg,$axg) - 1.8) / 0.6, 0, 1);
      $sMin = min($h_score_pct,$a_score_pct);
      $cMin = min($h_conc_pct,$a_conc_pct);
      $gfMin = min($h_gfpm,$a_gfpm);
      $gaMin = min($h_gapm,$a_gapm);
      $mix = ($xgScore
        + $clamp(($sMin - 80) / 15, 0, 1)
        + $clamp(($cMin - 60) / 20, 0, 1)
        + $clamp(($gfMin - 1.2) / 0.7, 0, 1)
        + $clamp(($gaMin - 1.0) / 0.8, 0, 1)
      ) / 5.0;
      $pct = (int)round($base + ($mix * 16));
      $pct = (int)round(50 + (($pct - 50) * $relL5));
      $pct = (int)$clamp($pct, 55, 92);
    } elseif ($pick === 'over_1_5') {
      $base = 68;
     if ($o15_ruleA)      $xgScore = $clamp(($txg - 2.7) / 0.9, 0, 1);
elseif ($o15_ruleB)  $xgScore = $clamp((min($hxg,$axg) - 1.2) / 0.6, 0, 1);
else /* ruleC */     $xgScore = $clamp(($txg - 3.1) / 0.9, 0, 1);
      $sMin = min($h_score_pct,$a_score_pct);
      $cMin = min($h_conc_pct,$a_conc_pct);
      $gfMin = min($h_gfpm,$a_gfpm);
      $gaMin = min($h_gapm,$a_gapm);
      $mix = ($xgScore
        + $clamp(($sMin - 60) / 25, 0, 1)
        + $clamp(($cMin - 60) / 25, 0, 1)
        + $clamp(($gfMin - 1.0) / 0.7, 0, 1)
        + $clamp(($gaMin - 1.0) / 0.8, 0, 1)
      ) / 5.0;
      $pct = (int)round($base + ($mix * 16));
      $pct = (int)round(50 + (($pct - 50) * $relL5));
      $pct = (int)$clamp($pct, 55, 90);
    } else {
      // Under 4.5 : plus le xG est bas, plus c’est “confiant”
      $base = 62;
      $inv = $clamp((4.2 - $txg) / 1.8, 0, 1);
      $pct = (int)round($base + ($inv * 14));
      $pct = (int)round(50 + (($pct - 50) * $relL5));
      $pct = (int)$clamp($pct, 55, 86);
    }

    $level = core_base_foot_conf_level_from_pct($pct);

    return [
      'ok' => true,
      'prediction' => $pick,     // over_1_5 / over_2_5 / under_4_5
      'line' => (float)$line,
      'label' => $label,

      'xg_home' => round($hxg, 2),
      'xg_away' => round($axg, 2),
      'xg_total'=> round($txg, 2),

      'confidence_pct' => (int)$pct,
      'confidence_level' => $level,
      'reliability_l5' => round($relL5, 3),

'rules' => [
  'rule' => $rule,
  'o15_ruleA' => (bool)$o15_ruleA,
  'o15_ruleB' => (bool)$o15_ruleB,
  'o15_ruleC' => (bool)$o15_ruleC, // ✅ ici
  'o25_ruleA' => (bool)$o25_ruleA,
  'o25_ruleB' => (bool)$o25_ruleB,
],

      'last5' => [
        'home' => ['played'=>$h_played_l5,'score1pct'=>$h_score_pct,'gfpm'=>round($h_gfpm,2),'concede1pct'=>$h_conc_pct,'gapm'=>round($h_gapm,2)],
        'away' => ['played'=>$a_played_l5,'score1pct'=>$a_score_pct,'gfpm'=>round($a_gfpm,2),'concede1pct'=>$a_conc_pct,'gapm'=>round($a_gapm,2)],
      ],
    ];
  }
}
/* ============================================================
 * ✅ PRÉDICTION BTTS (Les 2 équipes marquent) — OUI / NON / Indécis
 * Basée sur :
 * - xG home/away (core_base_foot_calc_potential_goals)
 * - Last5 : gfpm / gapm / score1pct / concede1pct
 * - Saison : home.score1pct et away.score1pct (pour filtre domicile/extérieur)
 *
 * OUI (éligible) :
 *  - home_xg >= 1.2 AND away_xg >= 1.2
 *  - L5 : gfpm >=1.2 (les 2), gapm >=1.0 (les 2), score1pct >=60 (les 2)
 *  - home (saison) score1pct >=75
 *  - away (saison) score1pct >=80
 *  - Confiance : base 78% -> cap 90%
 *
 * NON (éligible) :
 *  - (home_xg <= 1.0 OR away_xg <= 1.0)
 *  - (home_last5.score1pct <=20 OR away_last5.score1pct <=20)
 *  - (home_last5.concede1pct <=20 OR away_last5.concede1pct <=20)
 *  - (Confiance : base 75% -> cap 90%)  // (tu peux changer base si tu veux)
 * ============================================================ */

if (!function_exists('core_base_foot_pred_level_from_pct')) {
  function core_base_foot_pred_level_from_pct($pct): ?string {
    if ($pct === null) return null;
    $p = (int)$pct;
    if ($p >= 80) return 'Forte';
    if ($p >= 61) return 'Moyenne';
    return 'Faible';
  }
}

if (!function_exists('core_base_foot_scale_01')) {
  function core_base_foot_scale_01(float $v, float $min, float $max): float {
    if ($max <= $min) return 0.0;
    $x = ($v - $min) / ($max - $min);
    if ($x < 0) return 0.0;
    if ($x > 1) return 1.0;
    return $x;
  }
}
if (!function_exists('core_base_foot_scale_01_inv')) {
  function core_base_foot_scale_01_inv(float $v, float $hi, float $lo): float {
    // inverse : hi => 0 ; lo => 1
    if ($hi <= $lo) return 0.0;
    $x = ($hi - $v) / ($hi - $lo);
    if ($x < 0) return 0.0;
    if ($x > 1) return 1.0;
    return $x;
  }
}
/* ============================================================
 * ✅ PRÉDICTION "ÉQUIPE MARQUE ?" (Home / Away) — OUI / Indécis
 * Conditions OUI (par équipe) :
 * - xG >= 1.2
 * - Team L5 : score1pct >= 60 AND gfpm >= 1.2
 * - Opp L5  : concede1pct >= 60 AND gapm >= 1.0
 *
 * Confiance : base 68% -> cap 92% (ajustée par fiabilité L5)
 * ============================================================ */

if (!function_exists('core_base_foot_predict_team_to_score')) {
  function core_base_foot_predict_team_to_score(array $home_pack, array $away_pack, $pot = null, array $opts = []): array {

    // xG (si pas fourni)
    if (!is_array($pot) || !isset($pot['home_xg']) || !isset($pot['away_xg'])) {
      if (function_exists('core_base_foot_calc_potential_goals')) {
        $xg_opts = is_array($opts['xg_opts'] ?? null) ? (array)$opts['xg_opts'] : [];
        $pot = core_base_foot_calc_potential_goals($home_pack, $away_pack, $xg_opts);
      } else {
        $pot = ['home_xg'=>0, 'away_xg'=>0, 'meta'=>['missing'=>true]];
      }
    }

    $hxg = (float)($pot['home_xg'] ?? 0);
    $axg = (float)($pot['away_xg'] ?? 0);

    $hL5 = is_array($home_pack['last5'] ?? null) ? $home_pack['last5'] : [];
    $aL5 = is_array($away_pack['last5'] ?? null) ? $away_pack['last5'] : [];

    // ✅ Split saison (ratio "marque au moins 1" en HOME/AWAY)
    $hHome = is_array($home_pack['home'] ?? null) ? $home_pack['home'] : [];
    $aAway = is_array($away_pack['away'] ?? null) ? $away_pack['away'] : [];

    // fiabilité (0.20..1.00) selon le nb de matchs réellement dispos en L5
    $clamp = function(float $v, float $min, float $max): float {
      return function_exists('core_base_foot_clamp_float') ? core_base_foot_clamp_float($v,$min,$max) : max($min, min($max, $v));
    };
    $rel_from_played = function(int $p) use ($clamp): float {
      if ($p <= 0) return 0.20;
      return $clamp($p / 5.0, 0.20, 1.0);
    };

    // Eval 1 côté
    $eval = function(string $side) use ($hxg,$axg,$hL5,$aL5,$hHome,$aAway,$rel_from_played,$clamp) {

      $isHome = ($side === 'home');

      // Team = côté évalué ; Opp = l’autre
      $team_xg = $isHome ? $hxg : $axg;

      $teamL5 = $isHome ? $hL5 : $aL5;
      $oppL5  = $isHome ? $aL5 : $hL5;

      $team_played = (int)($teamL5['played'] ?? 0);
      $opp_played  = (int)($oppL5['played'] ?? 0);
      $relL5 = min($rel_from_played($team_played), $rel_from_played($opp_played));

      $team_scorepct5 = (int)($teamL5['score1pct'] ?? 0);
      $team_gfpm5     = (float)($teamL5['gfpm'] ?? 0);

      $opp_concpct5   = (int)($oppL5['concede1pct'] ?? 0);
      $opp_gapm5      = (float)($oppL5['gapm'] ?? 0);

      // ✅ Split saison : HOME utilise home_pack['home'], AWAY utilise away_pack['away']
      $team_split_scorepct = $isHome
        ? (int)($hHome['score1pct'] ?? 0)
        : (int)($aAway['score1pct'] ?? 0);

      // ✅ Seuils split saison demandés
      $split_min = $isHome ? 70 : 75;

      // Conditions OUI (uniformisées avec logique BTTS + split saison)
      $yes_ok =
        ($team_xg >= 1.2) &&
        ($team_scorepct5 >= 60) &&
        ($team_gfpm5 >= 1.2) &&
        ($opp_concpct5 >= 60) &&
        ($opp_gapm5 >= 1.0) &&
        ($team_split_scorepct >= $split_min);

      $vals = [
        'team_xg' => round($team_xg, 2),
        'team_last5_played' => $team_played,
        'team_last5_score1pct' => $team_scorepct5,
        'team_last5_gfpm' => round($team_gfpm5, 2),
        'opp_last5_played' => $opp_played,
        'opp_last5_concede1pct' => $opp_concpct5,
        'opp_last5_gapm' => round($opp_gapm5, 2),
        'team_season_split_score1pct' => $team_split_scorepct,
        'team_season_split_min' => $split_min,
      ];

      if (!$yes_ok) {
        return [
          'ok' => false,
          'prediction' => 'Indécis',
          'confidence_pct' => null,
          'confidence_level' => null,
          'reliability_l5' => round($relL5, 3),
          'vals' => $vals,
          'meta' => ['mode'=>'INDECIS'],
        ];
      }

      // Confiance (score 0..1 sur chaque critère)
      $parts = [];
      $parts['xg'] = function_exists('core_base_foot_scale_01') ? core_base_foot_scale_01($team_xg, 1.2, 2.2) : 0.0;
      $parts['team_scorepct5'] = function_exists('core_base_foot_scale_01') ? core_base_foot_scale_01((float)$team_scorepct5, 60, 100) : 0.0;
      $parts['team_gfpm5'] = function_exists('core_base_foot_scale_01') ? core_base_foot_scale_01($team_gfpm5, 1.2, 2.4) : 0.0;
      $parts['opp_concpct5'] = function_exists('core_base_foot_scale_01') ? core_base_foot_scale_01((float)$opp_concpct5, 60, 100) : 0.0;
      $parts['opp_gapm5'] = function_exists('core_base_foot_scale_01') ? core_base_foot_scale_01($opp_gapm5, 1.0, 2.0) : 0.0;

      // ✅ Ajout split saison dans le score (seuil min variable 70/75)
      $parts['team_split_scorepct'] = function_exists('core_base_foot_scale_01')
        ? core_base_foot_scale_01((float)$team_split_scorepct, (float)$split_min, 100.0)
        : 0.0;

      $score = array_sum($parts) / max(1, count($parts));

      // base 68 -> cap 92
      $pct_raw = (int)round(68 + ($score * (92 - 68)));

      // ajuste fiabilité L5
      $pct = (int)round(50 + (($pct_raw - 50) * $relL5));
      $pct = (int)$clamp($pct, 60, 95);

      $lvl = function_exists('core_base_foot_pred_level_from_pct') ? core_base_foot_pred_level_from_pct($pct) : null;

      return [
        'ok' => true,
        'prediction' => 'OUI',
        'confidence_pct' => $pct,
        'confidence_level' => $lvl,
        'reliability_l5' => round($relL5, 3),
        'vals' => $vals,
        'thresholds' => [
          'xg_min'=>1.2,
          'team_score1pct_min'=>60,
          'team_gfpm_min'=>1.2,
          'opp_concede1pct_min'=>60,
          'opp_gapm_min'=>1.0,
          'team_season_split_score1pct_min'=>$split_min, // ✅ 70 HOME / 75 AWAY
        ],
        'meta' => ['mode'=>'YES', 'score'=>round($score,3), 'parts'=>$parts, 'pct_raw'=>$pct_raw],
      ];
    };

    return [
      'ok' => true,
      'home' => $eval('home'),
      'away' => $eval('away'),
      'xg' => ['home_xg'=>round($hxg,2), 'away_xg'=>round($axg,2)],
      'meta' => ['pot_meta' => $pot['meta'] ?? null],
    ];
  }
}

if (!function_exists('core_base_foot_predict_btts_yesno')) {
  function core_base_foot_predict_btts_yesno(array $home_pack, array $away_pack, $pot = null, array $opts = []): array {
    // xG (si pas fourni)
    if (!is_array($pot) || !isset($pot['home_xg']) || !isset($pot['away_xg'])) {
      $xg_opts = is_array($opts['xg_opts'] ?? null) ? (array)$opts['xg_opts'] : [];
      $pot = core_base_foot_calc_potential_goals($home_pack, $away_pack, $xg_opts);
    }

    $hx = (float)($pot['home_xg'] ?? 0);
    $ax = (float)($pot['away_xg'] ?? 0);

    $hL5 = is_array($home_pack['last5'] ?? null) ? $home_pack['last5'] : [];
    $aL5 = is_array($away_pack['last5'] ?? null) ? $away_pack['last5'] : [];

    $hHome = is_array($home_pack['home'] ?? null) ? $home_pack['home'] : [];
    $aAway = is_array($away_pack['away'] ?? null) ? $away_pack['away'] : [];

    // Last5 (valeurs)
    $h_gfpm5 = (float)($hL5['gfpm'] ?? 0);
    $a_gfpm5 = (float)($aL5['gfpm'] ?? 0);
    $h_gapm5 = (float)($hL5['gapm'] ?? 0);
    $a_gapm5 = (float)($aL5['gapm'] ?? 0);

    $h_scorepct5   = (int)($hL5['score1pct'] ?? 0);
    $a_scorepct5   = (int)($aL5['score1pct'] ?? 0);
    $h_concedepct5 = (int)($hL5['concede1pct'] ?? 0);
    $a_concedepct5 = (int)($aL5['concede1pct'] ?? 0);

    // Saison home/away (ratios “marque au moins 1”)
    $h_home_scorepct = (int)($hHome['score1pct'] ?? 0);
    $a_away_scorepct = (int)($aAway['score1pct'] ?? 0);

    // --------------------------
    // ✅ ELIGIBILITY OUI
    // --------------------------
    $yes_ok =
      ($hx >= 1.2 && $ax >= 1.2) &&
      ($h_gfpm5 >= 1.2 && $a_gfpm5 >= 1.2) &&
      ($h_gapm5 >= 1.0 && $a_gapm5 >= 1.0) &&
      ($h_scorepct5 >= 60 && $a_scorepct5 >= 60) &&
      ($h_home_scorepct >= 70) &&
      ($a_away_scorepct >= 75);

    if ($yes_ok) {
      // Confiance : base 78 -> cap 90 (12 pts max)
      $score = 0.0;
      $parts = [];

      $parts['hx'] = core_base_foot_scale_01($hx, 1.2, 2.2);
      $parts['ax'] = core_base_foot_scale_01($ax, 1.2, 2.2);

      $parts['h_gfpm5'] = core_base_foot_scale_01($h_gfpm5, 1.2, 2.4);
      $parts['a_gfpm5'] = core_base_foot_scale_01($a_gfpm5, 1.2, 2.4);

      $parts['h_gapm5'] = core_base_foot_scale_01($h_gapm5, 1.0, 2.0);
      $parts['a_gapm5'] = core_base_foot_scale_01($a_gapm5, 1.0, 2.0);

      $parts['h_scorepct5'] = core_base_foot_scale_01((float)$h_scorepct5, 60, 100);
      $parts['a_scorepct5'] = core_base_foot_scale_01((float)$a_scorepct5, 60, 100);

      $parts['h_home_scorepct'] = core_base_foot_scale_01((float)$h_home_scorepct, 75, 100);
      $parts['a_away_scorepct'] = core_base_foot_scale_01((float)$a_away_scorepct, 80, 100);

      $score = array_sum($parts) / max(1, count($parts));
      $pct = (int)round(78 + ($score * (90 - 78)));
      if ($pct > 90) $pct = 90;
      if ($pct < 78) $pct = 78;

      return [
        'ok' => true,
        'prediction' => 'OUI',
        'confidence_pct' => $pct,
        'confidence_level' => core_base_foot_pred_level_from_pct($pct),
        'vals' => [
          'home_xg'=>$hx,'away_xg'=>$ax,
          'home_last5_gfpm'=>$h_gfpm5,'away_last5_gfpm'=>$a_gfpm5,
          'home_last5_gapm'=>$h_gapm5,'away_last5_gapm'=>$a_gapm5,
          'home_last5_score1pct'=>$h_scorepct5,'away_last5_score1pct'=>$a_scorepct5,
          'home_last5_concede1pct'=>$h_concedepct5,'away_last5_concede1pct'=>$a_concedepct5,
          'home_home_score1pct'=>$h_home_scorepct,'away_away_score1pct'=>$a_away_scorepct,
        ],
        'thresholds' => [
          'xg_min'=>1.2,'l5_gfpm_min'=>1.2,'l5_gapm_min'=>1.0,'l5_score1pct_min'=>60,
          'home_score1pct_min'=>75,'away_score1pct_min'=>80
        ],
        'meta' => ['mode'=>'YES', 'score'=>round($score,3), 'parts'=>$parts],
      ];
    }

    // --------------------------
    // ✅ ELIGIBILITY NON (ton principe exact)
    // --------------------------
    $no_ok =
      ($hx <= 1.0 || $ax <= 1.0) &&
      ($h_scorepct5 <= 20 || $a_scorepct5 <= 20) &&
      ($h_concedepct5 <= 20 || $a_concedepct5 <= 20);

    if ($no_ok) {
      // Confiance : base 75 -> cap 90 (15 pts max) selon “sévérité”
      $min_xg = min($hx, $ax);
      $min_score = min($h_scorepct5, $a_scorepct5);
      $min_conc  = min($h_concedepct5, $a_concedepct5);

      $p1 = core_base_foot_scale_01_inv($min_xg, 1.0, 0.4);
      $p2 = core_base_foot_scale_01_inv((float)$min_score, 20, 0);
      $p3 = core_base_foot_scale_01_inv((float)$min_conc, 20, 0);

      $score = ($p1 + $p2 + $p3) / 3.0;

      $pct = (int)round(75 + ($score * (90 - 75)));
      if ($pct > 90) $pct = 90;
      if ($pct < 75) $pct = 75;

      return [
        'ok' => true,
        'prediction' => 'NON',
        'confidence_pct' => $pct,
        'confidence_level' => core_base_foot_pred_level_from_pct($pct),
        'vals' => [
          'home_xg'=>$hx,'away_xg'=>$ax,
          'home_last5_score1pct'=>$h_scorepct5,'away_last5_score1pct'=>$a_scorepct5,
          'home_last5_concede1pct'=>$h_concedepct5,'away_last5_concede1pct'=>$a_concedepct5,
        ],
        'thresholds' => [
          'xg_max'=>1.0,'l5_score1pct_max'=>20,'l5_concede1pct_max'=>20
        ],
        'meta' => ['mode'=>'NO', 'score'=>round($score,3), 'parts'=>['xg'=>$p1,'score'=>$p2,'concede'=>$p3]],
      ];
    }

    return [
      'ok' => false,
      'prediction' => 'Indécis',
      'confidence_pct' => null,
      'confidence_level' => null,
      'vals' => [
        'home_xg'=>$hx,'away_xg'=>$ax,
        'home_last5_gfpm'=>$h_gfpm5,'away_last5_gfpm'=>$a_gfpm5,
        'home_last5_gapm'=>$h_gapm5,'away_last5_gapm'=>$a_gapm5,
        'home_last5_score1pct'=>$h_scorepct5,'away_last5_score1pct'=>$a_scorepct5,
        'home_last5_concede1pct'=>$h_concedepct5,'away_last5_concede1pct'=>$a_concedepct5,
        'home_home_score1pct'=>$h_home_scorepct,'away_away_score1pct'=>$a_away_scorepct,
      ],
      'meta' => ['mode'=>'INDECIS'],
    ];
  }
}

/* ============================================================
 * ✅ EXPORT "HIGH CONFIDENCE" — Réutilisable dans d'autres analyseurs
 * Objectif : prendre un payload predictions[] et sortir une liste standardisée
 * ============================================================ */

if (!function_exists('core_base_foot_conf_level_safe')) {
  function core_base_foot_conf_level_safe($pct): string {
    $p = (int)$pct;
    if (function_exists('core_base_foot_conf_level_from_pct')) return core_base_foot_conf_level_from_pct($p);
    if ($p >= 80) return 'Forte';
    if ($p >= 61) return 'Moyenne';
    return 'Faible';
  }
}

if (!function_exists('core_base_foot_extract_high_confidence_picks')) {
  /**
   * @param array $preds  ex: [
   *   'double_chance_ppm'=>..., 'win_ppm'=>..., 'goals_total'=>..., 'btts_yesno'=>...,
   *   'team_to_score'=>..., 'exact_score_xg'=>..., 'draw_xg'=>..., 'score_multichance_xg'=>...
   * ]
   * @param array $ctx ex: ['home_name'=>'PSG','away_name'=>'OM']
   */
  function core_base_foot_extract_high_confidence_picks(
    array $preds,
    array $ctx = [],
    int $min_pct = 80,
    int $max = 8
  ): array {
    $home = (string)($ctx['home_name'] ?? 'Home');
    $away = (string)($ctx['away_name'] ?? 'Away');

    $out = [];

    $push = function(string $key, string $market, $pick, $pct, array $meta = []) use (&$out, $min_pct) {
      if ($pick === null) return;
      $pick = is_string($pick) ? trim($pick) : $pick;
      if ($pick === '' || $pick === 'Indécis') return;

      if (!is_numeric($pct)) return;
      $pct = (int)$pct;
      if ($pct < $min_pct) return;

      $out[] = [
        'key' => $key,
        'market' => $market,
        'pick' => (string)$pick,
        'confidence_pct' => $pct,
        'confidence_level' => core_base_foot_conf_level_safe($pct),
        'meta' => $meta,
      ];
    };

    // 1) Double chance
    if (is_array($preds['double_chance_ppm'] ?? null)) {
      $p = $preds['double_chance_ppm'];
      $push(
        'double_chance',
        'Double chance',
        $p['prediction'] ?? null,
        $p['confidence_pct'] ?? null,
        [
          'home_ppm_proj' => $p['home_ppm_proj'] ?? null,
          'away_ppm_proj' => $p['away_ppm_proj'] ?? null,
          'diff' => $p['diff'] ?? null,
          'abs_diff' => $p['abs_diff'] ?? null,
          'threshold' => $p['threshold'] ?? null,
        ]
      );
    }

    // 2) Victoire (1/2)
    if (is_array($preds['win_ppm'] ?? null)) {
      $p = $preds['win_ppm'];
      $pick = $p['prediction'] ?? null; // '1' ou '2'
      if ($pick === '1') $pick = '1 (Domicile)';
      if ($pick === '2') $pick = '2 (Extérieur)';

      $push(
        'win',
        'Résultat',
        $pick,
        $p['confidence_pct'] ?? null,
        [
          'home_ppm_proj' => $p['home_ppm_proj'] ?? null,
          'away_ppm_proj' => $p['away_ppm_proj'] ?? null,
          'diff' => $p['diff'] ?? null,
          'abs_diff' => $p['abs_diff'] ?? null,
          'threshold' => $p['threshold'] ?? null,
          'source' => $p['meta']['source'] ?? null,
        ]
      );
    }

    // 3) Total buts (O1.5/O2.5/U4.5)
    if (is_array($preds['goals_total'] ?? null) && !empty(($preds['goals_total']['ok'] ?? false))) {
      $p = $preds['goals_total'];
      $push(
        'total_goals',
        'Total buts',
        $p['label'] ?? ($p['prediction'] ?? null),
        $p['confidence_pct'] ?? null,
        [
          'xg_home' => $p['xg_home'] ?? null,
          'xg_away' => $p['xg_away'] ?? null,
          'xg_total' => $p['xg_total'] ?? null,
          'rule' => $p['rules']['rule'] ?? null,
          'reliability_l5' => $p['reliability_l5'] ?? null,
        ]
      );
    }

    // 4) BTTS (OUI/NON) — seulement si exploitable
    if (is_array($preds['btts_yesno'] ?? null) && !empty(($preds['btts_yesno']['ok'] ?? false))) {
      $p = $preds['btts_yesno'];
      $push(
        'btts',
        'BTTS',
        $p['prediction'] ?? null, // OUI/NON
        $p['confidence_pct'] ?? null,
        [
          'home_xg' => $p['vals']['home_xg'] ?? null,
          'away_xg' => $p['vals']['away_xg'] ?? null,
        ]
      );
    }

    // 5) Équipe marque (Home/Away)
    if (is_array($preds['team_to_score'] ?? null) && !empty(($preds['team_to_score']['ok'] ?? false))) {
      $tts = $preds['team_to_score'];

      if (is_array($tts['home'] ?? null) && !empty(($tts['home']['ok'] ?? false)) && (($tts['home']['prediction'] ?? '') === 'OUI')) {
        $push(
          'team_score_home',
          'Équipe marque',
          $home . ' marque',
          $tts['home']['confidence_pct'] ?? null,
          ['side'=>'home', 'reliability_l5'=>$tts['home']['reliability_l5'] ?? null]
        );
      }
      if (is_array($tts['away'] ?? null) && !empty(($tts['away']['ok'] ?? false)) && (($tts['away']['prediction'] ?? '') === 'OUI')) {
        $push(
          'team_score_away',
          'Équipe marque',
          $away . ' marque',
          $tts['away']['confidence_pct'] ?? null,
          ['side'=>'away', 'reliability_l5'=>$tts['away']['reliability_l5'] ?? null]
        );
      }
    }

    // 6) Nul (X) si ton analyseur l'a mis dans preds
    if (is_array($preds['draw_xg'] ?? null) && !empty(($preds['draw_xg']['ok'] ?? false))) {
      $p = $preds['draw_xg'];
      if (($p['prediction'] ?? '') === 'OUI') {
        $push(
          'draw',
          'Match nul',
          'X',
          $p['confidence_pct'] ?? null,
          ['abs_diff_xg' => $p['vals']['abs_diff_xg'] ?? null, 'draw_thr' => $p['vals']['draw_thr'] ?? null]
        );
      }
    }

    // 7) Score exact / score multichance (si présent)
    if (is_array($preds['exact_score_xg'] ?? null) && !empty(($preds['exact_score_xg']['ok'] ?? false))) {
      $p = $preds['exact_score_xg'];
      $push(
        'exact_score',
        'Score exact',
        $p['label'] ?? null,
        $p['confidence_pct'] ?? null,
        ['home_xg'=>$p['home_xg'] ?? null, 'away_xg'=>$p['away_xg'] ?? null]
      );
    }

    if (is_array($preds['score_multichance_xg'] ?? null) && !empty(($preds['score_multichance_xg']['ok'] ?? false))) {
      $p = $preds['score_multichance_xg'];
      $push(
        'score_multichance',
        'Scores probables',
        $p['label'] ?? null,
        $p['confidence_pct'] ?? null,
        ['scores'=>$p['scores'] ?? null]
      );
    }

    // tri desc + limit
    usort($out, fn($a,$b) => ((int)$b['confidence_pct'] <=> (int)$a['confidence_pct']));
    if ($max > 0) $out = array_slice($out, 0, $max);

    return $out;
  }
}

/* ============================================================
 * ✅ ODDS — bookmaker primary (11) + fallback (4)
 * ============================================================ */

if (!defined('CORE_BASE_FOOT_BOOKMAKER_FALLBACK_ID')) define('CORE_BASE_FOOT_BOOKMAKER_FALLBACK_ID', 4);

if (!function_exists('core_base_foot_bets_have_any_odd')) {
  function core_base_foot_bets_have_any_odd($bets): bool {
    if (!is_array($bets) || empty($bets)) return false;
    foreach ($bets as $bet) {
      if (!is_array($bet)) continue;
      $vals = $bet['values'] ?? null;
      if (!is_array($vals) || empty($vals)) continue;
      foreach ($vals as $v) {
        if (!is_array($v)) continue;
        $odd = $v['odd'] ?? null;
        if (is_numeric($odd) && (float)$odd > 1.0) return true;
      }
    }
    return false;
  }
}

if (!function_exists('core_base_foot_get_odds_bookmaker')) {
  function core_base_foot_get_odds_bookmaker(int $fixture_id, int $bookmaker_id = 0): array {

    $primary  = $bookmaker_id ? (int)$bookmaker_id : (int)CORE_BASE_FOOT_BOOKMAKER_ID; // 11
    $fallback = (int)CORE_BASE_FOOT_BOOKMAKER_FALLBACK_ID; // 4

    $choiceKey = "odds_bk_choice:fx=$fixture_id:p=$primary:f=$fallback";
    $cachedChoice = core_base_foot_cache_get($choiceKey);

    if (is_array($cachedChoice) && !empty($cachedChoice['bk'])) {
      $bk = (int)$cachedChoice['bk'];
      $r = core_base_foot_api('odds', ['fixture'=>$fixture_id,'bookmaker'=>$bk], CORE_BASE_FOOT_CACHE_15M);
      if (empty($r['_error'])) {
        $resp = $r['response'][0] ?? null;
        $bets = $resp ? ($resp['bookmakers'][0]['bets'] ?? []) : [];
        if (core_base_foot_bets_have_any_odd($bets)) return $bets;
      }
      core_base_foot_cache_set($choiceKey, ['bk'=>0], 30);
    }

    // 1) primary
    $r1 = core_base_foot_api('odds', ['fixture'=>$fixture_id,'bookmaker'=>$primary], CORE_BASE_FOOT_CACHE_15M);
    if (empty($r1['_error'])) {
      $resp1 = $r1['response'][0] ?? null;
      $bets1 = $resp1 ? ($resp1['bookmakers'][0]['bets'] ?? []) : [];
      if (core_base_foot_bets_have_any_odd($bets1)) {
        core_base_foot_cache_set($choiceKey, ['bk'=>$primary], 10 * 60);
        return $bets1;
      }
    }

    // 2) fallback
    $r2 = core_base_foot_api('odds', ['fixture'=>$fixture_id,'bookmaker'=>$fallback], CORE_BASE_FOOT_CACHE_15M);
    if (empty($r2['_error'])) {
      $resp2 = $r2['response'][0] ?? null;
      $bets2 = $resp2 ? ($resp2['bookmakers'][0]['bets'] ?? []) : [];
      if (core_base_foot_bets_have_any_odd($bets2)) {
        core_base_foot_cache_set($choiceKey, ['bk'=>$fallback], 10 * 60);
        return $bets2;
      }
    }

    $e1 = $r1['_error'] ?? null;
    $e2 = $r2['_error'] ?? null;

    if ($e1 || $e2) {
      return [
        '_error' => trim(($e1 ? "BK$primary: $e1" : '') . (($e1 && $e2) ? ' | ' : '') . ($e2 ? "BK$fallback: $e2" : '')),
        '_url_primary'  => $r1['_url'] ?? '',
        '_url_fallback' => $r2['_url'] ?? '',
        '_errors_primary'  => $r1['_errors'] ?? null,
        '_errors_fallback' => $r2['_errors'] ?? null,
      ];
    }

    return [];
  }
}
/* ============================================================
 * ✅ ODDS MAP API (CORE) — map + cache + helpers
 * - Ne casse pas core_base_foot_get_odds_bookmaker() (compat)
 * - Permet aux modules d’appeler une seule fonction stable
 * ============================================================ */

if (!defined('CORE_BASE_FOOT_ODDS_MAP_TTL')) {
  define('CORE_BASE_FOOT_ODDS_MAP_TTL', CORE_BASE_FOOT_CACHE_15M); // aligné sur odds
}

if (!function_exists('core_base_foot_odds_norm_market')) {
  function core_base_foot_odds_norm_market(string $s): string {
    $s = trim($s);
    if ($s === '') return '';

    // WP helper (plus propre que ta table manuelle)
    if (function_exists('remove_accents')) $s = remove_accents($s);

    $s = function_exists('core_base_foot_strtolower') ? core_base_foot_strtolower($s) : strtolower($s);

    // uniformise tirets/apostrophes
    $s = str_replace(['’','–','—'], ["'","-","-"], $s);

    $s = preg_replace('~\s+~', ' ', $s);
    // garde a-z0-9 + espaces + / - . +
    $s = preg_replace('~[^a-z0-9 \/\-\.\+]+~', '', $s);

    return trim($s);
  }
}

if (!function_exists('core_base_foot_odds_norm_sel')) {
  function core_base_foot_odds_norm_sel(string $s): string {
    $s = trim($s);
    if ($s === '') return '';

    // ✅ Scores exacts: garder un normaliseur dédié
    if (preg_match('~^\d+\s*[-:]\s*\d+$~', $s)) {
      return function_exists('core_base_foot_norm_score_label')
        ? core_base_foot_norm_score_label($s)
        : preg_replace('~\s+~', '', str_replace(':','-',$s));
    }

    if (function_exists('remove_accents')) $s = remove_accents($s);
    $s = function_exists('core_base_foot_strtolower') ? core_base_foot_strtolower($s) : strtolower($s);

    // ✅ uniformise ponctuation -> espaces (très important pour "Braga - Yes")
    $s = str_replace(['’','–','—','/','\\','|','_','-','(',')','[',']','{','}',':'], ' ', $s);

    // ✅ FR -> EN (utile si tu cherches OUI/NON)
    // (ça ne casse rien si déjà en EN)
    $s = preg_replace('~\boui\b~', 'yes', $s);
    $s = preg_replace('~\bnon\b~', 'no', $s);

    // ✅ espaces propres
    $s = preg_replace('~\s+~', ' ', $s);
    return trim($s);
  }
}


if (!function_exists('core_base_foot_odds_float_valid')) {
  function core_base_foot_odds_float_valid($odd): ?float {
    $v = function_exists('core_base_foot_odd_float') ? core_base_foot_odd_float($odd) : (is_numeric($odd) ? (float)$odd : null);
    if ($v === null) return null;
    if ($v <= 1.0001) return null;
    return (float)$v;
  }
}

if (!function_exists('core_base_foot_odds_map_from_bets')) {
  /**
   * Transforme bets[] (API-Football) en map:
   *  map[market_norm][selection_norm] = odd(float)
   *
   * @param array $bets
   * @param array $only_markets (option) liste de marchés acceptés (strings) -> normalisés
   */
  function core_base_foot_odds_map_from_bets(array $bets, array $only_markets = []): array {
    $map = [];
    $only = [];

    if (!empty($only_markets)) {
      foreach ($only_markets as $m) {
        $k = core_base_foot_odds_norm_market((string)$m);
        if ($k !== '') $only[$k] = true;
      }
    }

    foreach ($bets as $bet) {
      if (!is_array($bet)) continue;

      $bn = (string)($bet['name'] ?? '');
      if ($bn === '') continue;

      $kBet = core_base_foot_odds_norm_market($bn);
      if ($kBet === '') continue;

      if ($only && !isset($only[$kBet])) continue;

      $vals = $bet['values'] ?? null;
      if (!is_array($vals) || empty($vals)) continue;

      foreach ($vals as $v) {
        if (!is_array($v)) continue;

        $selRaw = (string)($v['value'] ?? '');
        if ($selRaw === '') continue;

        $odd = core_base_foot_odds_float_valid($v['odd'] ?? null);
        if (!$odd) continue;

        $kSel = core_base_foot_odds_norm_sel($selRaw);
        if ($kSel === '') continue;

        if (!isset($map[$kBet])) $map[$kBet] = [];
        // 3 décimales suffisent, + stable pour UI/export
        $map[$kBet][$kSel] = round($odd, 3);
      }
    }

    return $map;
  }
}

if (!function_exists('core_base_foot_get_odds_map')) {
  /**
   * API stable CORE : récupère les odds (bets via primary/fallback) puis map normalisée.
   * Retour:
   *  ['ok'=>bool,'map'=>array,'bookmaker_id'=>int|null,'_error'=>string|null,'only_markets'=>array]
   */
  function core_base_foot_get_odds_map(int $fixture_id, array $opts = []): array {
    $primary  = (int)($opts['primary'] ?? (defined('CORE_BASE_FOOT_BOOKMAKER_ID') ? (int)CORE_BASE_FOOT_BOOKMAKER_ID : 11));
    $fallback = (int)($opts['fallback'] ?? (defined('CORE_BASE_FOOT_BOOKMAKER_FALLBACK_ID') ? (int)CORE_BASE_FOOT_BOOKMAKER_FALLBACK_ID : 0));
    $ttl      = (int)($opts['ttl'] ?? (defined('CORE_BASE_FOOT_ODDS_MAP_TTL') ? (int)CORE_BASE_FOOT_ODDS_MAP_TTL : 900));
    $only     = is_array($opts['only_markets'] ?? null) ? (array)$opts['only_markets'] : [];

    // ✅ Normalise only_markets => cache stable (ordre + accents + casing)
    if (!empty($only)) {
      $tmp = [];
      foreach ($only as $m) {
        $k = function_exists('core_base_foot_odds_norm_market')
          ? core_base_foot_odds_norm_market((string)$m)
          : trim(strtolower((string)$m));
        if ($k !== '') $tmp[] = $k;
      }
      sort($tmp);
      $only = array_values(array_unique($tmp));
    }

    // cache map (évite re-parse sur la même page + multi modules)
    $ck = "odds_map:v1:fx=$fixture_id:p=$primary:f=$fallback:only=" . md5(json_encode($only));
    $cached = function_exists('core_base_foot_cache_get') ? core_base_foot_cache_get($ck) : false;
    if (is_array($cached) && isset($cached['ok'])) return $cached;

    if (!function_exists('core_base_foot_get_odds_bookmaker')) {
      return function_exists('core_base_foot_cache_set')
        ? core_base_foot_cache_set($ck, ['ok'=>false,'map'=>[],'bookmaker_id'=>null,'_error'=>'core_base_foot_get_odds_bookmaker indisponible','only_markets'=>$only], 120)
        : ['ok'=>false,'map'=>[],'bookmaker_id'=>null,'_error'=>'core_base_foot_get_odds_bookmaker indisponible','only_markets'=>$only];
    }

    // ✅ on réutilise ton choix bookmaker + fallback existant (compat)
    $bets = core_base_foot_get_odds_bookmaker($fixture_id, $primary);

    if (!is_array($bets) || empty($bets) || !empty($bets['_error'])) {
      $err = is_array($bets) ? ($bets['_error'] ?? 'odds empty') : 'odds invalid';
      $out = ['ok'=>false,'map'=>[],'bookmaker_id'=>null,'_error'=>(string)$err,'only_markets'=>$only];
      return function_exists('core_base_foot_cache_set') ? core_base_foot_cache_set($ck, $out, 120) : $out;
    }

    if (!function_exists('core_base_foot_odds_map_from_bets')) {
      $out = ['ok'=>false,'map'=>[],'bookmaker_id'=>null,'_error'=>'core_base_foot_odds_map_from_bets indisponible','only_markets'=>$only];
      return function_exists('core_base_foot_cache_set') ? core_base_foot_cache_set($ck, $out, 120) : $out;
    }

    $map = core_base_foot_odds_map_from_bets($bets, $only);

    // On tente de lire le bookmaker réellement choisi via ta clé choiceKey
    $bookmaker_id = null;
    $choiceKey = "odds_bk_choice:fx=$fixture_id:p=$primary:f=$fallback";
    $choice = function_exists('core_base_foot_cache_get') ? core_base_foot_cache_get($choiceKey) : null;
    if (is_array($choice) && !empty($choice['bk'])) $bookmaker_id = (int)$choice['bk'];

    $out = [
      'ok' => !empty($map),
      'map' => $map,
      'bookmaker_id' => $bookmaker_id,
      '_error' => empty($map) ? 'no odds in map' : null,
      'only_markets' => $only, // ✅ utile debug
    ];

    // TTL normal si data, sinon court
    return function_exists('core_base_foot_cache_set')
      ? core_base_foot_cache_set($ck, $out, !empty($map) ? $ttl : 120)
      : $out;
  }
}

if (!function_exists('core_base_foot_odds_get')) {
  /**
   * Récupère une cote dans la map.
   * - $market : string marché (ex: "Both Teams To Score", "Goals Over/Under", "Match Winner")
   * - $selection : string (ex: "Yes", "Over 2.5", "1", "X", "2", "1-0")
   */
  function core_base_foot_odds_get(array $oddsMap, string $market, string $selection): ?float {
    $m = core_base_foot_odds_norm_market($market);
    $s = core_base_foot_odds_norm_sel($selection);
    if ($m === '' || $s === '') return null;

    if (!isset($oddsMap[$m]) || !is_array($oddsMap[$m])) return null;

    // match direct
    if (isset($oddsMap[$m][$s]) && is_numeric($oddsMap[$m][$s])) return (float)$oddsMap[$m][$s];

    // fallback : parfois la sélection est stockée sous une variante (rare)
    foreach ($oddsMap[$m] as $kSel => $odd) {
      if (!is_numeric($odd)) continue;
      if ($kSel === $s) return (float)$odd;
    }
    return null;
  }
}
if (!function_exists('core_base_foot_odds_get_any')) {
  /**
   * Essaie plusieurs noms de marchés + plusieurs sélections (match fuzzy robuste).
   * - Match marché : exact puis "contains" sur les marchés existants
   * - Match sélection : exact, contains, loose (sans ponctuation), puis match par tokens
   */
  function core_base_foot_odds_get_any(array $oddsMap, array $markets, array $sels): ?float {

    if (!is_array($oddsMap) || empty($oddsMap)) return null;

    // --- normalise marchés demandés
    $mKeys = [];
    foreach ($markets as $m) {
      $km = function_exists('core_base_foot_odds_norm_market')
        ? core_base_foot_odds_norm_market((string)$m)
        : trim(strtolower((string)$m));
      if ($km !== '') $mKeys[] = $km;
    }
    $mKeys = array_values(array_unique($mKeys));

    // --- normalise sélections demandées
    $sKeys = [];
    foreach ($sels as $s) {
      $ks = function_exists('core_base_foot_odds_norm_sel')
        ? core_base_foot_odds_norm_sel((string)$s)
        : trim(strtolower((string)$s));
      if ($ks !== '') $sKeys[] = $ks;
    }
    $sKeys = array_values(array_unique($sKeys));

    if (!$mKeys || !$sKeys) return null;

    // --- helper loose (sans ponctuation)
    $loose = function(string $x): string {
      $x = trim($x);
      if ($x === '') return '';
      if (function_exists('remove_accents')) $x = remove_accents($x);
      $x = function_exists('core_base_foot_strtolower') ? core_base_foot_strtolower($x) : strtolower($x);
      // garde seulement a-z0-9
      $x = preg_replace('~[^a-z0-9]+~', '', $x);
      return $x ?: '';
    };

    // --- helper tokens match (tous les tokens de $needle doivent exister dans $hayTokens)
    $tokens_all_in = function(string $needle, string $haystack): bool {
      $n = trim($needle);
      $h = trim($haystack);
      if ($n === '' || $h === '') return false;

      $nTok = preg_split('~\s+~', $n);
      $hTok = preg_split('~\s+~', $h);

      $set = [];
      foreach ($hTok as $t) {
        $t = trim($t);
        if ($t !== '') $set[$t] = true;
      }

      $hit = 0;
      foreach ($nTok as $t) {
        $t = trim($t);
        if ($t === '') continue;
        if (!isset($set[$t])) return false;
        $hit++;
      }
      return $hit > 0;
    };

    // --- construit la liste des marchés candidats (exact + fuzzy)
    $candidateMarkets = [];

    // 1) exact keys
    foreach ($mKeys as $m) {
      if (isset($oddsMap[$m]) && is_array($oddsMap[$m]) && !empty($oddsMap[$m])) {
        $candidateMarkets[$m] = $oddsMap[$m];
      }
    }

    // 2) fuzzy: si le marché exact n’existe pas, cherche un marché proche
    if (count($candidateMarkets) < count($mKeys)) {
      foreach ($mKeys as $m) {
        if (isset($candidateMarkets[$m])) continue;
        // évite fuzzy sur mini-chaînes
        if (strlen($m) < 4) continue;

        foreach ($oddsMap as $mk => $bucket) {
          if (!is_array($bucket) || empty($bucket)) continue;
          if (!is_string($mk) || $mk === '') continue;

          // contains dans un sens ou l’autre
          if (strpos($mk, $m) !== false || strpos($m, $mk) !== false) {
            $candidateMarkets[$mk] = $bucket;
          }
        }
      }
    }

    if (empty($candidateMarkets)) return null;

    // --- match selections dans les buckets candidats
    foreach ($candidateMarkets as $bucket) {
      if (!is_array($bucket) || empty($bucket)) continue;

      // 1) exact
      foreach ($sKeys as $s) {
        if (isset($bucket[$s]) && is_numeric($bucket[$s])) return (float)$bucket[$s];
      }

      // 2) contains (ex: "over 2.5" dans "over 2.5 goals")
      foreach ($sKeys as $s) {
        foreach ($bucket as $kSel => $odd) {
          if (!is_numeric($odd)) continue;
          if (!is_string($kSel) || $kSel === '') continue;
          if ($kSel === $s) return (float)$odd;
          if ($s !== '' && strpos($kSel, $s) !== false) return (float)$odd;
        }
      }

      // 3) loose contains (ignore ponctuation) -> règle le cas "braga yes" vs "sc braga - yes"
      foreach ($sKeys as $s) {
        $sL = $loose($s);
        if ($sL === '') continue;

        foreach ($bucket as $kSel => $odd) {
          if (!is_numeric($odd)) continue;
          if (!is_string($kSel) || $kSel === '') continue;

          $kL = $loose($kSel);
          if ($kL !== '' && strpos($kL, $sL) !== false) return (float)$odd;
        }
      }

      // 4) tokens match (tous les tokens doivent apparaître)
      foreach ($sKeys as $s) {
        foreach ($bucket as $kSel => $odd) {
          if (!is_numeric($odd)) continue;
          if (!is_string($kSel) || $kSel === '') continue;

          if ($tokens_all_in($s, $kSel)) return (float)$odd;
        }
      }
    }

    return null;
  }
}

if (!function_exists('core_base_foot_odds_for_double_chance')) {
  function core_base_foot_odds_for_double_chance(array $oddsMap, string $pick /*1X|X2*/): ?float {
    $pick = strtoupper(trim($pick));
    if ($pick !== '1X' && $pick !== 'X2') return null;

    $sels = ($pick === '1X')
      ? ['1X','Home/Draw','Home or Draw','1 or X','Domicile ou Nul','1 ou X','1X (FT)']
      : ['X2','Draw/Away','Draw or Away','X or 2','Nul ou Extérieur','X ou 2','X2 (FT)'];

    return core_base_foot_odds_get_any(
      $oddsMap,
      ['Double Chance','Double chance','Double chance - Full Time','DC'],
      $sels
    );
  }
}

if (!function_exists('core_base_foot_odds_for_match_winner')) {
  function core_base_foot_odds_for_match_winner(array $oddsMap, string $pick /*1|X|2*/): ?float {
    $pick = strtoupper(trim($pick));
    if (!in_array($pick, ['1','X','2'], true)) return null;

    $sels = ($pick === '1') ? ['Home','1','Domicile']
          : (($pick === '2') ? ['Away','2','Exterieur','Extérieur'] : ['Draw','X','Nul']);

    return core_base_foot_odds_get_any(
      $oddsMap,
      ['Match Winner','1X2','Match Result','Result'],
      $sels
    );
  }
}

if (!function_exists('core_base_foot_odds_for_btts')) {
  function core_base_foot_odds_for_btts(array $oddsMap, string $pick /*OUI|NON*/): ?float {
    $pick = strtoupper(trim($pick));
    if ($pick === 'OUI') $sels = ['Yes','Oui'];
    elseif ($pick === 'NON') $sels = ['No','Non'];
    else return null;

    return core_base_foot_odds_get_any(
      $oddsMap,
      ['Both Teams To Score','BTTS','Both Teams Score'],
      $sels
    );
  }
}

if (!function_exists('core_base_foot_odds_for_total_goals')) {
  function core_base_foot_odds_for_total_goals(array $oddsMap, string $pred /*over_1_5|over_2_5|under_4_5*/): ?float {
    $pred = strtolower(trim($pred));
    $sel = null;

    if ($pred === 'over_1_5')  $sel = ['Over 1.5','Over 1.5 Goals'];
    if ($pred === 'over_2_5')  $sel = ['Over 2.5','Over 2.5 Goals'];
    if ($pred === 'under_4_5') $sel = ['Under 4.5','Under 4.5 Goals'];

    if (!$sel) return null;

    return core_base_foot_odds_get_any(
      $oddsMap,
      ['Goals Over/Under','Total Goals','Over/Under'],
      $sel
    );
  }
}
if (!function_exists('core_base_foot_odds_for_team_to_score')) {
  /**
   * Équipe marque ? (par side)
   * API-Football bookmaker: Total - Home (id 16) / Total - Away (id 17)
   * OUI = Over 0.5 ; NON = Under 0.5
   *
   * @param array  $oddsMap  => $out['map'] de core_base_foot_get_odds_map()
   * @param string $side    'home'|'away'
   * @param string $pick    'OUI'|'NON' (ou 'YES'|'NO')
   */
  function core_base_foot_odds_for_team_to_score(array $oddsMap, string $side, string $pick): ?float {

    $side = strtolower(trim($side));
    $side = ($side === 'away') ? 'away' : 'home';

    $p = strtoupper(trim($pick));
    if ($p === 'YES') $p = 'OUI';
    if ($p === 'NO')  $p = 'NON';
    if ($p !== 'OUI' && $p !== 'NON') return null;

    // ✅ marchés réels (tes dénominations)
    $markets = ($side === 'home')
      ? ['Total - Home', 'Total Home', 'Team Total - Home', 'Home Team Total']
      : ['Total - Away', 'Total Away', 'Team Total - Away', 'Away Team Total'];

    // ✅ sélections réelles
    $sels = ($p === 'OUI')
      ? ['Over 0.5', 'Over 0.5 Goals']
      : ['Under 0.5', 'Under 0.5 Goals'];

    return core_base_foot_odds_get_any($oddsMap, $markets, $sels);
  }
}

/* ============================================================
 * ✅ ODDS HELPERS — Correct Score FT (évite Half Time Score)
 * ============================================================ */

if (!function_exists('core_base_foot_norm_txt')) {
  function core_base_foot_norm_txt($s): string {
    $s = trim((string)$s);
    if ($s === '') return '';
    if (function_exists('remove_accents')) $s = remove_accents($s);
    $s = function_exists('core_base_foot_strtolower') ? core_base_foot_strtolower($s) : strtolower($s);
    $s = preg_replace('/\s+/', ' ', $s);
    return trim($s);
  }
}

if (!function_exists('core_base_foot_norm_score_label')) {
  function core_base_foot_norm_score_label($s): string {
    $s = trim((string)$s);
    if ($s === '') return '';
    $s = str_replace(['–','—',' : ',':',' '], ['-','-','-','-',''], $s);
    // ex: "1-0" ok ; "1- 0" => "1-0"
    $s = preg_replace('/\s+/', '', $s);
    return $s;
  }
}

if (!function_exists('core_base_foot_odd_float')) {
  function core_base_foot_odd_float($odd): ?float {
    if ($odd === null || $odd === '') return null;
    if (is_string($odd)) $odd = str_replace(',', '.', $odd);
    return is_numeric($odd) ? (float)$odd : null;
  }
}

/**
 * Choisit le "bet" le plus pertinent selon mots-clés inclus/exclus.
 * - Priorise match exact (score +100), contains (score +30)
 * - Pénalise fortement si contient un mot exclu (-1000)
 */
if (!function_exists('core_base_foot_pick_best_bet')) {
  function core_base_foot_pick_best_bet(array $bets, array $want, array $avoid): ?array {
    $want  = array_values(array_filter(array_map('core_base_foot_norm_txt', $want)));
    $avoid = array_values(array_filter(array_map('core_base_foot_norm_txt', $avoid)));

    $best = null;
    $bestScore = -999999;

    foreach ($bets as $b) {
      if (!is_array($b)) continue;
      $nameRaw = $b['name'] ?? '';
      $name = core_base_foot_norm_txt($nameRaw);
      if ($name === '') continue;

      $score = 0;

      foreach ($avoid as $a) {
        if ($a !== '' && strpos($name, $a) !== false) {
          $score -= 1000; // disqualifiant
        }
      }

      foreach ($want as $w) {
        if ($w === '') continue;
        if ($name === $w) $score += 100;
        elseif (strpos($name, $w) !== false) $score += 30;
      }

      if ($score > $bestScore) {
        $bestScore = $score;
        $best = $b;
      }
    }

    return ($bestScore > 0) ? $best : null;
  }
}

if (!function_exists('core_base_foot_get_odd_in_bet_values')) {
  function core_base_foot_get_odd_in_bet_values(array $values, string $wantedValue): ?float {
    $wanted = core_base_foot_norm_score_label($wantedValue);
    if ($wanted === '') return null;

    foreach ($values as $v) {
      if (!is_array($v)) continue;
      $val = core_base_foot_norm_score_label($v['value'] ?? '');
      if ($val === $wanted) {
        return core_base_foot_odd_float($v['odd'] ?? null);
      }
    }
    return null;
  }
}

/**
 * ✅ Score exact FT (temps réglementaire) à partir du payload bets[]
 */
if (!function_exists('core_base_foot_get_odd_correct_score_ft_from_bets')) {
  function core_base_foot_get_odd_correct_score_ft_from_bets(array $bets, string $score): ?float {

    // Mots-clés FT (inclut) + HT (exclut)
    $want = [
      'correct score',
      'exact score',
      'score exact',
      'correct score - full time',
      'exact score - full time',
      'score exact - temps reglementaire',
      'temps reglementaire', // parfois inclus dans le nom
      'full time',
    ];

    $avoid = [
      'half time',
      '1st half',
      'first half',
      'mi-temps',
      '1ere mi-temps',
      'ht',
      'half-time/full-time',
      'ht/ft',
      'extra time',
      'after extra time',
      'aet',
      'pen',
      'penalties',
      'tirs au but',
      'prolongation',
    ];

    $bet = core_base_foot_pick_best_bet($bets, $want, $avoid);
    if (!$bet) return null;

    $vals = $bet['values'] ?? null;
    if (!is_array($vals) || empty($vals)) return null;

    return core_base_foot_get_odd_in_bet_values($vals, $score);
  }
}

if (!function_exists('core_base_foot_get_odd_correct_score_ft')) {
  function core_base_foot_get_odd_correct_score_ft(int $fixture_id, string $score): ?float {
    $bets = core_base_foot_get_odds_bookmaker($fixture_id);
    if (!is_array($bets) || empty($bets) || !empty($bets['_error'])) return null;
    return core_base_foot_get_odd_correct_score_ft_from_bets($bets, $score);
  }
}
}
/* ============================================================
 * ✅ SCORE MULTI-CHANCE (TOP 3) via xG -> matrice Poisson
 * Retourne exactement 3 scores (sauf si "indécis")
 * ============================================================ */

if (!function_exists('core_base_foot_l5_reliability_pair')) {
  function core_base_foot_l5_reliability_pair(array $home_pack, array $away_pack): float {
    $hp = (int)($home_pack['last5']['played'] ?? 0);
    $ap = (int)($away_pack['last5']['played'] ?? 0);
    $minP = min($hp, $ap);
    $rel = $minP > 0 ? ($minP / 5.0) : 0.0;
    // clamp 0.20..1.00 (comme tes autres modules)
    $rel = max(0.20, min(1.00, $rel));
    return $rel;
  }
}

/** PMF Poisson (k petit) */
if (!function_exists('core_base_foot_poisson_pmf')) {
  function core_base_foot_poisson_pmf(int $k, float $lambda): float {
    if ($k < 0) return 0.0;
    if ($lambda <= 0) return ($k === 0) ? 1.0 : 0.0;

    // factorial k (k<=10 ici)
    $fact = 1.0;
    for ($i=2; $i<=$k; $i++) $fact *= $i;

    return exp(-$lambda) * pow($lambda, $k) / $fact;
  }
}

/** Matrice de scores si tu ne l’as pas déjà (ne s’exécute pas si déjà définie) */
if (!function_exists('core_base_foot_score_matrix')) {
  function core_base_foot_score_matrix(float $hxg, float $axg, int $maxG = 6): array {
    $maxG = max(3, min(10, (int)$maxG));

    $pH = [];
    $pA = [];
    for ($h=0; $h<=$maxG; $h++) $pH[$h] = core_base_foot_poisson_pmf($h, $hxg);
    for ($a=0; $a<=$maxG; $a++) $pA[$a] = core_base_foot_poisson_pmf($a, $axg);

    $pScore = [];
    $pDraw = 0.0; $pHome = 0.0; $pAway = 0.0;

    for ($h=0; $h<=$maxG; $h++) {
      for ($a=0; $a<=$maxG; $a++) {
        $p = $pH[$h] * $pA[$a];
        $key = $h . '-' . $a;
        $pScore[$key] = $p;

        if ($h === $a) $pDraw += $p;
        elseif ($h > $a) $pHome += $p;
        else $pAway += $p;
      }
    }

    arsort($pScore);

    return [
      'p_score' => $pScore, // map score=>proba
      'p_draw'  => $pDraw,
      'p_home'  => $pHome,
      'p_away'  => $pAway,
      'meta'    => ['maxG'=>$maxG, 'hxg'=>$hxg, 'axg'=>$axg],
    ];
  }
}

if (!function_exists('core_base_foot_predict_score_multichance_xg')) {
  function core_base_foot_predict_score_multichance_xg(array $home_pack, array $away_pack, $pot = null, array $opts = []): array {

    $maxG = (int)($opts['maxG'] ?? 6);
    $k    = (int)($opts['k'] ?? 3);          // ✅ TOP 3
    $k    = max(3, min(3, $k));              // force 3 (tu as demandé 3, pas 6)

    $min_sum = (float)($opts['min_sum'] ?? 0.22); // somme probas mini pour être “exploitable”
    $min_top = (float)($opts['min_top'] ?? 0.09); // proba du #1 mini

    // xG (si pas fourni)
    if (!is_array($pot) || !isset($pot['home_xg']) || !isset($pot['away_xg'])) {
      $pot = function_exists('core_base_foot_calc_potential_goals')
        ? core_base_foot_calc_potential_goals($home_pack, $away_pack, (array)($opts['xg_opts'] ?? []))
        : ['home_xg'=>0,'away_xg'=>0,'meta'=>['missing'=>true]];
    }

    $hxg = (float)($pot['home_xg'] ?? 0);
    $axg = (float)($pot['away_xg'] ?? 0);

    // matrice scores
    $M = function_exists('core_base_foot_score_matrix')
      ? core_base_foot_score_matrix($hxg, $axg, $maxG)
      : ['p_score'=>[],'p_draw'=>null,'p_home'=>null,'p_away'=>null,'meta'=>['missing'=>true]];

    $pScore = $M['p_score'] ?? [];
    if (!is_array($pScore) || empty($pScore)) {
      return [
        'ok' => false,
        'prediction' => 'Indécis',
        'confidence_pct' => null,
        'confidence_level' => null,
        'scores' => [],
        'label' => null,
        'home_xg' => round($hxg,2),
        'away_xg' => round($axg,2),
        'meta' => ['mode'=>'NO_MATRIX'],
      ];
    }

    // TOP 3
    $top = [];
    $i = 0;
    foreach ($pScore as $score => $p) {
      if ($i >= 3) break;
      $top[] = ['score'=>$score, 'p'=>(float)$p];
      $i++;
    }

    $p1 = (float)($top[0]['p'] ?? 0.0);
    $psum = 0.0;
    foreach ($top as $row) $psum += (float)$row['p'];

    // Eligibilité
    if ($psum < $min_sum || $p1 < $min_top) {
      return [
        'ok' => false,
        'prediction' => 'Indécis',
        'confidence_pct' => null,
        'confidence_level' => null,
        'scores' => $top,
        'label' => implode(' / ', array_map(fn($x)=>$x['score'], $top)),
        'home_xg' => round($hxg,2),
        'away_xg' => round($axg,2),
        'vals' => ['p_top1'=>round($p1,3), 'p_sum3'=>round($psum,3), 'min_sum'=>$min_sum, 'min_top'=>$min_top],
        'meta' => ['mode'=>'LOW_SIGNAL'],
      ];
    }

    // (Option) odds par score — 1 seul fetch bets
    $fixture_id = (int)($opts['fixture_id'] ?? 0);
    if ($fixture_id > 0 && function_exists('core_base_foot_get_odds_bookmaker') && function_exists('core_base_foot_get_odd_correct_score_ft_from_bets')) {
      $bets = core_base_foot_get_odds_bookmaker($fixture_id);
      if (is_array($bets) && empty($bets['_error'])) {
        foreach ($top as &$row) {
          $row['odd'] = core_base_foot_get_odd_correct_score_ft_from_bets($bets, (string)$row['score']);
        }
        unset($row);
      }
    }

    // Confiance : basée sur somme(3) + force du #1, puis ajustée fiabilité L5
    $clamp = fn($v,$min,$max) => (function_exists('core_base_foot_clamp_float') ? core_base_foot_clamp_float((float)$v,$min,$max) : max($min, min($max, (float)$v)));

    // mapping simple (tu peux resserrer si tu veux)
    $sumScore = $clamp(($psum - 0.22) / 0.20, 0.0, 1.0); // 0.22..0.42
    $topScore = $clamp(($p1   - 0.09) / 0.08, 0.0, 1.0); // 0.09..0.17

    $pct_raw = (int)round(62 + (0.70*$sumScore + 0.30*$topScore) * (88 - 62)); // 62..88

    $relL5 = function_exists('core_base_foot_l5_reliability_pair')
      ? core_base_foot_l5_reliability_pair($home_pack, $away_pack)
      : 0.50;

    $pct = (int)round(50 + (($pct_raw - 50) * $relL5));
    $pct = (int)$clamp($pct, 55, 90);

    return [
      'ok' => true,
      'prediction' => 'TOP3',
      'confidence_pct' => $pct,
      'confidence_level' => function_exists('core_base_foot_pred_level_from_pct') ? core_base_foot_pred_level_from_pct($pct) : null,
      'scores' => $top,
      'label' => implode(' / ', array_map(fn($x)=>$x['score'], $top)),
      'home_xg' => round($hxg, 2),
      'away_xg' => round($axg, 2),
      'vals' => [
        'p_top1' => round($p1, 3),
        'p_sum3' => round($psum, 3),
        'reliability_l5' => round($relL5, 3),
      ],
      'meta' => ['mode'=>'TOP3', 'maxG'=>$maxG],
    ];
  }
}

6ème Analyse

<?php


/**
 * ============================================================
 * ✅ MODULE UI — Match Analyzer (dépend de CORE BASE FOOT)
 * Shortcode : [tp_match_analyzer]
 * ============================================================
 */
if (!defined('ABSPATH')) exit;

if (!function_exists('core_base_foot_api')) {
  add_shortcode('tp_match_analyzer', function(){
    return '<div style="padding:12px;border:1px solid #ddd;border-radius:12px;background:#fff">
      <b>Erreur :</b> le snippet <i>CORE BASE FOOT</i> n’est pas actif.
    </div>';
  });
  return;
}

/* ============================================================
   HELPERS (fallback traduction si le CORE ne fournit pas)
   ============================================================ */
if (!function_exists('tp_match_analyzer_translate_round')) {
  function tp_match_analyzer_translate_round($round){
    $round = (string)$round;
    if (function_exists('core_base_foot_translate_round')) {
      try { return (string) core_base_foot_translate_round($round); } catch (Throwable $e) {}
    }
    $r = $round;
    $r = str_ireplace('Regular Season', 'Saison régulière', $r);
    $r = str_ireplace('League', 'Ligue', $r);
    $r = str_ireplace('Round', 'Journée', $r);
    $r = str_ireplace('Stage', 'Phase', $r);
    $r = str_ireplace('Group', 'Groupe', $r);
    return $r;
  }
}

if (!function_exists('tp_match_analyzer_translate_status')) {
  function tp_match_analyzer_translate_status($short, $long = ''){
    $short = (string)$short; $long = (string)$long;
    if (function_exists('core_base_foot_translate_status')) {
      try {
        $tr = core_base_foot_translate_status($short, $long);
        if (is_array($tr)) return $tr + ['short_fr'=>$short, 'long_fr'=>$long];
      } catch (Throwable $e) {}
    }
    $mapShort = [
      'NS'=>'À venir','TBD'=>'À définir','PST'=>'Reporté','CANC'=>'Annulé','SUSP'=>'Suspendu',
      'INT'=>'Interrompu','LIVE'=>'En direct','1H'=>'1re mi-temps','HT'=>'Mi-temps','2H'=>'2e mi-temps','ET'=>'Prolongation',
      'PEN'=>'Tirs au but','BT'=>'Pause','FT'=>'Terminé','AET'=>'Terminé (a.p.)',
      'AWD'=>'Victoire sur tapis vert','WO'=>'Forfait'
    ];
    $mapLong = [
      'Not Started'=>'Match à venir',
      'Time to be defined'=>'Horaire à définir',
      'Match Finished'=>'Match terminé',
      'Match Finished After Extra Time'=>'Terminé après prolongation',
      'Match Finished After Penalty'=>'Terminé après tirs au but',
      'Postponed'=>'Reporté',
      'Cancelled'=>'Annulé',
      'Suspended'=>'Suspendu',
      'Interrupted'=>'Interrompu',
      'In Progress'=>'En cours',
      'Halftime'=>'Mi-temps',
      'Extra Time'=>'Prolongation',
      'Penalty In Progress'=>'Tirs au but',
    ];
    return [
      'short_fr' => $mapShort[$short] ?? ($short ?: '—'),
      'long_fr'  => $mapLong[$long]  ?? ($long ?: ''),
    ];
  }
}

if (!function_exists('tp_match_analyzer_translate_injury_text')) {
  function tp_match_analyzer_translate_injury_text($text){
    $text = (string)$text;
    if ($text === '') return $text;

    if (function_exists('core_base_foot_translate_injury_text')) {
      try { return (string) core_base_foot_translate_injury_text($text); } catch (Throwable $e) {}
    }

    static $translations = [
      'Broken ankle'=>'Cheville cassée','Illness'=>'Maladie','Knee Injury'=>'Blessure au genou','Knock'=>'Coup',
      'Muscle Injury'=>'Blessure musculaire','Hamstring Injury'=>'Blessure aux ischio-jambiers',
      'Foot Injury'=>'Blessure au pied','Groin Injury'=>'Blessure à l’aine','Head Injury'=>'Blessure à la tête',
      'Calf Injury'=>'Blessure au mollet','Thigh Injury'=>'Blessure à la cuisse','Back Injury'=>'Blessure au dos',
      'Red Card'=>'Carton rouge','Ankle Injury'=>'Blessure à la cheville','Toe Injury'=>'Blessure à l’orteil',
      'Achilles Tendon Injury'=>'Blessure au tendon d’Achille','Finger Injury'=>'Blessure au doigt',
      'Leg Injury'=>'Blessure à la jambe','Injury'=>'Blessure','Unknown'=>'Inconnu',
      "Jumper's knee"=>'Tendinite rotulienne','Jumpers knee'=>'Tendinite rotulienne',
      'Muscle bruise'=>'Contusion musculaire','Wound'=>'Plaie','Sprained ankle'=>'Entorse de la cheville',
      'Thigh problems'=>'Problèmes à la cuisse','Foot injury'=>'Blessure au pied','Fitness'=>'Condition physique',
      'Heel pain'=>'Douleur au talon',
      'Groin'=>'Aine','Ankle'=>'Cheville','Knee'=>'Genou','Hip'=>'Hanche','Shoulder'=>'Épaule','Wrist'=>'Poignet',
      'Elbow'=>'Coude','Hand'=>'Main','Neck'=>'Cou','Concussion'=>'Commotion','Suspension'=>'Suspension',
      'Doubtful'=>'Incertain','Questionable'=>'Incertain','Out'=>'Absent','Injured'=>'Blessé',
      'Rest'=>'Repos','Personal reasons'=>'Raisons personnelles',
    ];

    if (isset($translations[$text])) return $translations[$text];
    $key = trim($text);
    foreach ($translations as $en => $fr) if (strcasecmp($en, $key) === 0) return $fr;
    return $text;
  }
}
/* ============================================================
   ✅ ODDS (COTES) — Fetch + normalize + map
   - Utilise core_base_foot_api('odds', ...) si dispo
   - Cache dédié par fixture (évite de recharger)
   ============================================================ */

if (!function_exists('tp_match_analyzer_odds_norm_key')) {
  function tp_match_analyzer_odds_norm_key(string $s): string {
    $s = strtolower(trim($s));
    $repl = [
      'é'=>'e','è'=>'e','ê'=>'e','ë'=>'e','à'=>'a','â'=>'a','ä'=>'a','î'=>'i','ï'=>'i',
      'ô'=>'o','ö'=>'o','ù'=>'u','û'=>'u','ü'=>'u','ç'=>'c','’'=>"'",'–'=>'-','—'=>'-',
    ];
    $s = strtr($s, $repl);
    $s = preg_replace('~\s+~', ' ', $s);
    $s = preg_replace('~[^a-z0-9 \/\-\.\+]+~', '', $s);
    return trim($s);
  }
}

if (!function_exists('tp_match_analyzer_odd_to_float')) {
  function tp_match_analyzer_odd_to_float($odd): ?float {
    if (is_array($odd)) {
      if (isset($odd['odd'])) $odd = $odd['odd'];
      elseif (isset($odd['value'])) $odd = $odd['value'];
      else return null;
    }
    if ($odd === null) return null;
    $s = trim((string)$odd);
    if ($s === '') return null;
    $s = str_replace(',', '.', $s);
    if (!is_numeric($s)) return null;
    $v = (float)$s;
    if ($v <= 1.0001) return null;
    return $v;
  }
}

if (!function_exists('tp_match_analyzer_extract_odds_map')) {
  function tp_match_analyzer_extract_odds_map(array $api, int $preferred_bookmaker_id = 0): array {
    $out = ['ok'=>false, 'map'=>[], 'bookmaker'=>null, 'updated'=>null];

    $resp = $api['response'] ?? null;
    if (!is_array($resp) || empty($resp)) return $out;

    $item = $resp[0] ?? null;
    if (!is_array($item)) return $out;

    $out['updated'] = $item['update'] ?? ($item['updated'] ?? null);

    $bookmakers = $item['bookmakers'] ?? [];
    if (!is_array($bookmakers) || empty($bookmakers)) return $out;

    // ✅ choix bookmaker : priorité à l’ID demandé
    $bm = null;
    if ($preferred_bookmaker_id > 0) {
      foreach ($bookmakers as $b) {
        if (!is_array($b)) continue;
        if ((int)($b['id'] ?? 0) === (int)$preferred_bookmaker_id) { $bm = $b; break; }
      }
    }
    if (!$bm) $bm = $bookmakers[0] ?? null;
    if (!is_array($bm)) return $out;

    $out['bookmaker'] = [
      'id' => $bm['id'] ?? null,
      'name' => $bm['name'] ?? null,
    ];

    $bets = $bm['bets'] ?? [];
    if (!is_array($bets) || empty($bets)) return $out;

    $map = [];
    foreach ($bets as $bet) {
      if (!is_array($bet)) continue;
      $bn = (string)($bet['name'] ?? '');
      if ($bn === '') continue;

      $kBet = tp_match_analyzer_odds_norm_key($bn);

      $vals = $bet['values'] ?? [];
      if (!is_array($vals)) continue;

      foreach ($vals as $v) {
        if (!is_array($v)) continue;
        $sel = trim((string)($v['value'] ?? ''));
        $odd = tp_match_analyzer_odd_to_float($v['odd'] ?? null);
        if ($sel === '' || !$odd) continue;

        if (!isset($map[$kBet])) $map[$kBet] = [];
        $map[$kBet][$sel] = round($odd, 3);
      }
    }

    if (empty($map)) return $out;

    $out['ok'] = true;
    $out['map'] = $map;
    return $out;
  }
}

if (!function_exists('tp_match_analyzer_fetch_fixture_odds')) {
  function tp_match_analyzer_fetch_fixture_odds(int $fixture_id, int $league_id, int $season): array {
    $bookmaker_id =
      defined('CORE_BASE_FOOT_BOOKMAKER_ID') ? (int)CORE_BASE_FOOT_BOOKMAKER_ID :
      (defined('CORE_BASE_FOOT_ODDS_BOOKMAKER_ID') ? (int)CORE_BASE_FOOT_ODDS_BOOKMAKER_ID : 8); // 8 = souvent Bet365 (si dispo)

  $cache_key = "tp_match_analyzer:odds:v4:fx={$fixture_id}:lg={$league_id}:season={$season}:bm={$bookmaker_id}";
    if (function_exists('core_base_foot_cache_get')) {
      $cached = core_base_foot_cache_get($cache_key);
      if (is_array($cached) && isset($cached['ok'])) return $cached;
    }

    if (!function_exists('core_base_foot_api')) {
      return ['ok'=>false,'map'=>[],'_error'=>'core_base_foot_api indisponible'];
    }

    $api = null;
    try {
      // tentative avec bookmaker
      $api = core_base_foot_api('odds', [
        'fixture'   => $fixture_id,
        'league'    => $league_id,
        'season'    => $season,
        'bookmaker' => $bookmaker_id,
      ]);
    } catch (Throwable $e) {
      $api = null;
    }

    if (!is_array($api) || empty($api['response'])) {
      try {
        // fallback sans bookmaker
        $api = core_base_foot_api('odds', [
          'fixture' => $fixture_id,
          'league'  => $league_id,
          'season'  => $season,
        ]);
      } catch (Throwable $e) {
        $api = null;
      }
    }

    if (!is_array($api)) {
      $out = ['ok'=>false,'map'=>[],'_error'=>'appel odds échoué'];
      if (function_exists('core_base_foot_cache_set')) core_base_foot_cache_set($cache_key, $out, 10*60);
      return $out;
    }

$out = tp_match_analyzer_extract_odds_map($api, $bookmaker_id);

    // si bookmaker demandé pas trouvé, mais odds ok => on garde quand même
    if (function_exists('core_base_foot_cache_set')) {
      core_base_foot_cache_set($cache_key, $out, 10 * 60);
    }
    return $out;
  }
}

/* ============================================================
   ✅ FALLBACK PREDICTOR (si pas encore dans le CORE)
   - Double chance via projection PPM pondérée
   ============================================================ */
if (!function_exists('tp_match_analyzer_effective_weights')) {
  function tp_match_analyzer_effective_weights(int $playedSeason, int $playedHA, int $playedL5): array {
    $wS  = defined('CORE_BASE_FOOT_W_SEASON') ? (float)CORE_BASE_FOOT_W_SEASON : 0.20;
    $wHA = defined('CORE_BASE_FOOT_W_HA')     ? (float)CORE_BASE_FOOT_W_HA     : 0.35;
    $wL5 = defined('CORE_BASE_FOOT_W_L5')     ? (float)CORE_BASE_FOOT_W_L5     : 0.45;

    $fS  = ($playedSeason > 0) ? 1.0 : 0.0;
    $fHA = ($playedHA     > 0) ? 1.0 : 0.0;
    $fL5 = ($playedL5 > 0) ? min(1.0, max(0.0, $playedL5 / 5.0)) : 0.0;

    $eS  = $wS  * $fS;
    $eHA = $wHA * $fHA;
    $eL5 = $wL5 * $fL5;

    $sum = $eS + $eHA + $eL5;
    if ($sum <= 0) {
      $sum = max(0.000001, $wS + $wHA + $wL5);
      return ['season'=>$wS/$sum,'ha'=>$wHA/$sum,'l5'=>$wL5/$sum,'meta'=>['scaled'=>false]];
    }
    return ['season'=>$eS/$sum,'ha'=>$eHA/$sum,'l5'=>$eL5/$sum,'meta'=>['scaled'=>true]];
  }
}

if (!function_exists('tp_match_analyzer_weighted_ppm')) {
  function tp_match_analyzer_weighted_ppm(array $pack, string $context /* home|away */): array {
    $context = ($context === 'away') ? 'away' : 'home';

    $s  = (float)($pack['season']['ppm'] ?? 0);
    $ha = (float)($pack[$context]['ppm'] ?? 0);
    $l5 = (float)($pack['last5']['ppm'] ?? 0);

    $pS  = (int)($pack['season']['played'] ?? 0);
    $pHA = (int)($pack[$context]['played'] ?? 0);
    $pL5 = (int)($pack['last5']['played'] ?? 0);

    $W = function_exists('core_base_foot_effective_weights')
      ? core_base_foot_effective_weights($pS, $pHA, $pL5)
      : tp_match_analyzer_effective_weights($pS, $pHA, $pL5);

    $val = ($W['season'] * $s) + ($W['ha'] * $ha) + ($W['l5'] * $l5);

    return [
      'value' => round($val, 3),
      'weights' => $W,
      'parts' => ['season'=>$s, $context=>$ha, 'last5'=>$l5],
      'meta' => ['playedSeason'=>$pS, 'playedHA'=>$pHA, 'playedL5'=>$pL5],
    ];
  }
}

if (!function_exists('tp_match_analyzer_predict_double_chance_ppm')) {
  function tp_match_analyzer_predict_double_chance_ppm(array $home_pack, array $away_pack, array $opts = []): array {
    $threshold = isset($opts['threshold']) ? (float)$opts['threshold'] : 0.30;

    $home = tp_match_analyzer_weighted_ppm($home_pack, 'home');
    $away = tp_match_analyzer_weighted_ppm($away_pack, 'away');

    $h = (float)($home['value'] ?? 0);
    $a = (float)($away['value'] ?? 0);

    $diff = $h - $a;
    $abs  = abs($diff);

    // 0.00 => ~50% ; 0.30 => ~60% ; 1.00 => ~84% ; cap 95
    $pct = (int)round(max(50, min(95, 50 + ($abs * 34))));

    $level = 'Faible';
    if ($pct >= 80) $level = 'Forte';
    elseif ($pct >= 61) $level = 'Moyenne';

    if ($abs <= $threshold) {
      return [
        'ok' => false,
        'prediction' => null,
        'threshold' => $threshold,
        'home_ppm_proj' => round($h, 2),
        'away_ppm_proj' => round($a, 2),
        'diff' => round($diff, 3),
        'abs_diff' => round($abs, 3),
        'confidence_pct' => $pct,
        'confidence_level' => $level,
        'home_detail' => $home,
        'away_detail' => $away,
      ];
    }

    return [
      'ok' => true,
      'prediction' => ($diff > 0) ? '1X' : 'X2',
      'threshold' => $threshold,
      'home_ppm_proj' => round($h, 2),
      'away_ppm_proj' => round($a, 2),
      'diff' => round($diff, 3),
      'abs_diff' => round($abs, 3),
      'confidence_pct' => $pct,
      'confidence_level' => $level,
      'home_detail' => $home,
      'away_detail' => $away,
    ];
  }
}

/* ============================================================
   ✅ AJOUT : Victoire en sec (1/2) — priorité CORE, fallback sinon
   - Structure normalisée: ok / prediction(1|2) / diff / abs_diff / threshold / confidence_*
   ============================================================ */
if (!function_exists('tp_match_analyzer_predict_win_ppm')) {
  function tp_match_analyzer_predict_win_ppm(array $home_pack, array $away_pack, array $opts = []): array {
    $threshold = isset($opts['threshold']) ? (float)$opts['threshold'] : 1.30;

    $home = tp_match_analyzer_weighted_ppm($home_pack, 'home');
    $away = tp_match_analyzer_weighted_ppm($away_pack, 'away');

    $h = (float)($home['value'] ?? 0);
    $a = (float)($away['value'] ?? 0);

    $diff = $h - $a;
    $abs  = abs($diff);

    $pct = (int)round(max(50, min(95, 50 + ($abs * 34))));
    $level = 'Faible';
    if ($pct >= 80) $level = 'Forte';
    elseif ($pct >= 61) $level = 'Moyenne';

    if ($abs < $threshold) {
      return [
        'ok' => false,
        'prediction' => null,
        'threshold' => $threshold,
        'home_ppm_proj' => round($h, 2),
        'away_ppm_proj' => round($a, 2),
        'diff' => round($diff, 3),
        'abs_diff' => round($abs, 3),
        'confidence_pct' => $pct,
        'confidence_level' => $level,
        'home_detail' => $home,
        'away_detail' => $away,
      ];
    }

    return [
      'ok' => true,
      'prediction' => ($diff > 0) ? '1' : '2',
      'threshold' => $threshold,
      'home_ppm_proj' => round($h, 2),
      'away_ppm_proj' => round($a, 2),
      'diff' => round($diff, 3),
      'abs_diff' => round($abs, 3),
      'confidence_pct' => $pct,
      'confidence_level' => $level,
      'home_detail' => $home,
      'away_detail' => $away,
    ];
  }
}

if (!function_exists('tp_match_analyzer_core_predict_win')) {
  function tp_match_analyzer_core_predict_win(array $home_pack, array $away_pack, array $opts = []): ?array {
    // On tente plusieurs noms possibles côté CORE (au cas où)
    $candidates = [
      'core_base_foot_predict_win_ppm',
      'core_base_foot_predict_match_winner_ppm',
      'core_base_foot_predict_1x2_ppm',
      'core_base_foot_predict_moneyline_ppm',
      'core_base_foot_predict_win',
      'core_base_foot_predict_1x2',
    ];
    foreach ($candidates as $fn) {
      if (!function_exists($fn)) continue;
      try {
        $res = call_user_func($fn, $home_pack, $away_pack, $opts);
        if (is_array($res)) return $res;
      } catch (Throwable $e) {}
    }
    return null;
  }
}

if (!function_exists('tp_match_analyzer_normalize_win_pred')) {
  function tp_match_analyzer_normalize_win_pred($core_res, array $fallback): array {
    if (!is_array($core_res) || empty($core_res)) return $fallback;

    // Si le CORE a déjà le bon format, on l’essaie
    $ok = isset($core_res['ok']) ? (bool)$core_res['ok'] : null;

    $predRaw = $core_res['prediction'] ?? ($core_res['pick'] ?? ($core_res['winner'] ?? ($core_res['result'] ?? null)));
    $pred = null;

    if ($predRaw !== null) {
      $p = is_string($predRaw) ? strtolower(trim($predRaw)) : $predRaw;

      // formats possibles
      if ($p === '1' || $p === 1 || $p === 'home' || $p === 'h' || $p === 'dom' || $p === 'domicile') $pred = '1';
      if ($p === '2' || $p === 2 || $p === 'away' || $p === 'a' || $p === 'ext' || $p === 'exterieur' || $p === 'extérieur') $pred = '2';
    }

    // Valeurs numériques si dispo
    $h = isset($core_res['home_ppm_proj']) ? (float)$core_res['home_ppm_proj'] : (float)($fallback['home_ppm_proj'] ?? 0);
    $a = isset($core_res['away_ppm_proj']) ? (float)$core_res['away_ppm_proj'] : (float)($fallback['away_ppm_proj'] ?? 0);

    $diff = isset($core_res['diff']) ? (float)$core_res['diff'] : ($h - $a);
    $abs  = isset($core_res['abs_diff']) ? (float)$core_res['abs_diff'] : abs($diff);

    $thr = isset($core_res['threshold']) ? (float)$core_res['threshold'] : (float)($fallback['threshold'] ?? 1.30);
    $pct = isset($core_res['confidence_pct']) ? (int)$core_res['confidence_pct'] : (int)($fallback['confidence_pct'] ?? 50);
    $lvl = isset($core_res['confidence_level']) ? (string)$core_res['confidence_level'] : (string)($fallback['confidence_level'] ?? 'Faible');

    // Si ok n’est pas fourni, on déduit
    if ($ok === null) $ok = ($pred !== null) && ($abs >= $thr);

    // Si le CORE renvoie un truc inexploitable (pas de prediction), fallback
    if ($pred === null && $ok) return $fallback;

    return [
      'ok' => (bool)$ok,
      'prediction' => $ok ? $pred : null,
      'threshold' => $thr,
      'home_ppm_proj' => round($h, 2),
      'away_ppm_proj' => round($a, 2),
      'diff' => round($diff, 3),
      'abs_diff' => round($abs, 3),
      'confidence_pct' => $pct,
      'confidence_level' => $lvl,
      // On garde les détails si le CORE les donne, sinon fallback
      'home_detail' => is_array($core_res['home_detail'] ?? null) ? $core_res['home_detail'] : ($fallback['home_detail'] ?? []),
      'away_detail' => is_array($core_res['away_detail'] ?? null) ? $core_res['away_detail'] : ($fallback['away_detail'] ?? []),
      'meta' => ['source' => isset($core_res['ok']) || isset($core_res['prediction']) ? 'core' : 'fallback'],
    ];
  }
}

/* ============================================================
   AJAX : détails (+ buts potentiels + prédictions)
   ============================================================ */
if (!function_exists('tp_match_analyzer_ajax_details')) {
  function tp_match_analyzer_ajax_details() {
    if (!check_ajax_referer('tp_match_analyzer_nonce', 'nonce', false)) {
      wp_send_json_error(['message' => 'Nonce invalide']);
    }

    $t0 = microtime(true);

    $fixture_id = (int)($_POST['fixture_id'] ?? 0);
    $league_id  = (int)($_POST['league_id'] ?? 0);
    $home_id    = (int)($_POST['home_id'] ?? 0);
    $away_id    = (int)($_POST['away_id'] ?? 0);

    $home_name  = sanitize_text_field($_POST['home_name'] ?? '');
    $away_name  = sanitize_text_field($_POST['away_name'] ?? '');
    $home_logo  = esc_url_raw($_POST['home_logo'] ?? '');
    $away_logo  = esc_url_raw($_POST['away_logo'] ?? '');

    $season = (int)CORE_BASE_FOOT_SEASON;
    $N = 40;

    $pot_opts = [
      'mix_attack' => 0.50,
      'cap_min'    => 0.2,
      'cap_max'    => 3.8,
    ];

$cache_key = "tp_match_analyzer:details:v22:fx={$fixture_id}:league={$league_id}:season={$season}:n={$N}"
           . ":mix={$pot_opts['mix_attack']}:min={$pot_opts['cap_min']}:max={$pot_opts['cap_max']}";
    $cached = core_base_foot_cache_get($cache_key);
    if (is_array($cached)) wp_send_json_success($cached);

    $home_pack = core_base_foot_build_team_match_stats($home_id, $league_id, $season);
    $away_pack = core_base_foot_build_team_match_stats($away_id, $league_id, $season);

    $pot = function_exists('core_base_foot_calc_potential_goals')
      ? core_base_foot_calc_potential_goals($home_pack, $away_pack, $pot_opts)
      : null;

    $pred_dc = function_exists('core_base_foot_predict_double_chance_ppm')
      ? core_base_foot_predict_double_chance_ppm($home_pack, $away_pack, ['threshold' => 0.30])
      : tp_match_analyzer_predict_double_chance_ppm($home_pack, $away_pack, ['threshold' => 0.30]);

	  $pred_goals = function_exists('core_base_foot_predict_total_goals')
  ? core_base_foot_predict_total_goals($home_pack, $away_pack, $pot)
  : null;

    // ✅ Victoire en sec : priorité CORE, fallback sinon
    $win_thr = 1.30;
    $pred_win_fallback = tp_match_analyzer_predict_win_ppm($home_pack, $away_pack, ['threshold' => $win_thr]);
    $pred_win_core = tp_match_analyzer_core_predict_win($home_pack, $away_pack, ['threshold' => $win_thr]);
    $pred_win = tp_match_analyzer_normalize_win_pred($pred_win_core, $pred_win_fallback);
// ✅ BTTS (OUI / NON / Indécis) — calcul CORE
$btts = function_exists('core_base_foot_predict_btts_yesno')
  ? core_base_foot_predict_btts_yesno($home_pack, $away_pack, $pot, ['xg_opts' => $pot_opts])
  : null;
$team_score = function_exists('core_base_foot_predict_team_to_score')
  ? core_base_foot_predict_team_to_score($home_pack, $away_pack, $pot, ['xg_opts' => $pot_opts])
  : null;
// ✅ Score exact (arrondi xG)
$exact_score = null;

if (is_array($pot) && isset($pot['home_xg'], $pot['away_xg'])) {

  $hx = (float)$pot['home_xg'];
  $ax = (float)$pot['away_xg'];

  // ✅ Seuil nul
  $draw_thr = 0.20; // teste 0.20 -> 0.30 selon ton goût

  // arrondi "au plus proche"
  $hs = (int) round($hx, 0, PHP_ROUND_HALF_UP);
  $as = (int) round($ax, 0, PHP_ROUND_HALF_UP);

  // ✅ Si xG proches => on force un score de nul
  if (abs($hx - $ax) <= $draw_thr) {
    $avg = ($hx + $ax) / 2;
    $hs  = (int) round($avg, 0, PHP_ROUND_HALF_UP);
    $as  = $hs;
  }

  // clamp
  $hs = max(0, min(7, $hs));
  $as = max(0, min(7, $as));

  // confiance score exact
  $dist = abs($hx - $hs) + abs($ax - $as);
  $pct  = (int) round(max(45, min(85, 85 - ($dist * 20))));
  $lvl  = ($pct >= 80) ? 'Forte' : (($pct >= 61) ? 'Moyenne' : 'Faible');

  $exact_score = [
    'ok' => true,
    'home_score' => $hs,
    'away_score' => $as,
    'label' => $hs . ' - ' . $as,
    'home_xg' => round($hx, 2),
    'away_xg' => round($ax, 2),
    'method' => 'xg_round_nearest',
    'confidence_pct' => $pct,
    'confidence_level' => $lvl,
    'vals' => ['dist' => round($dist, 2), 'draw_thr' => $draw_thr],
  ];

} else {
  $exact_score = ['ok' => false];
}


	  // ✅ Match nul (X) + Score multi-chance (Top 3) — basé sur xG déjà calculé (pas de recalcul buts)
$draw_xg = ['ok'=>false];
$score_multichance = ['ok'=>false];

	      // ✅ ODDS (cotes)
    $odds_pack = tp_match_analyzer_fetch_fixture_odds($fixture_id, $league_id, $season);

if (is_array($exact_score) && !empty($exact_score['ok']) && isset($exact_score['home_score'], $exact_score['away_score'], $exact_score['home_xg'], $exact_score['away_xg'])) {

  $hs = (int)$exact_score['home_score'];
  $as = (int)$exact_score['away_score'];
  $hx = (float)$exact_score['home_xg'];
  $ax = (float)$exact_score['away_xg'];

  $abs = abs($hx - $ax);

  // Confiance simple (indicative) : plus l'écart de xG est petit, plus le nul est plausible
  $pct = (int) round(max(50, min(85, 85 - ($abs * 20)))); // abs=0 => 85 ; abs~1.75 => 50
  $lvl = ($pct >= 80) ? 'Forte' : (($pct >= 61) ? 'Moyenne' : 'Faible');

$draw_thr = 0.20; // le même seuil que plus haut (mets-le en constante si tu veux)

$is_draw = (abs($hx - $ax) <= $draw_thr);

$draw_xg = [
  'ok' => true,
  'prediction' => $is_draw ? 'OUI' : 'Indécis',
  'confidence_pct' => $pct,
  'confidence_level' => $lvl,
  'vals' => [
    'home_xg' => round($hx,2),
    'away_xg' => round($ax,2),
    'abs_diff_xg' => round(abs($hx-$ax),2),
    'exact_score' => ($hs.'-'.$as),
    'draw_thr' => $draw_thr, // debug utile
  ],
];


  // Top 3 scores proches autour du score exact (sans calculer une proba)
  $cands = [];

  $push = function($i,$j) use (&$cands){
    $i = max(0, min(7, (int)$i));
    $j = max(0, min(7, (int)$j));
    $k = $i.'-'.$j;
    if (!isset($cands[$k])) $cands[$k] = ['label'=>$k,'h'=>$i,'a'=>$j];
  };

  // score exact + voisins
  $push($hs,$as);
  $push($hs-1,$as); $push($hs+1,$as);
  $push($hs,$as-1); $push($hs,$as+1);
  $push($hs-1,$as-1); $push($hs+1,$as+1);

  // tri par "proximité" aux xG (heuristique)
  foreach($cands as &$c){
    $c['dist'] = abs($c['h'] - $hx) + abs($c['a'] - $ax);
  }
  unset($c);

  uasort($cands, function($a,$b){
    return ($a['dist'] <=> $b['dist']);
  });

  $top = array_slice(array_values($cands), 0, 3);
  $labels = array_map(fn($x)=>$x['label'], $top);

// confiance indicative selon la proximité moyenne
$avgDist = 0;
foreach($top as $t) $avgDist += (float)$t['dist'];
$avgDist = $top ? $avgDist / count($top) : 99;

$pct2 = (int) round(max(45, min(80, 80 - ($avgDist * 15))));
$lvl2 = ($pct2 >= 80) ? 'Forte' : (($pct2 >= 61) ? 'Moyenne' : 'Faible');

// ✅ Seuil d’affichage
$min_mc = 75;

if ($pct2 >= $min_mc) {
  $score_multichance = [
    'ok' => true,
    'scores' => $labels,
    'label' => implode(' · ', $labels),
    'confidence_pct' => $pct2,
    'confidence_level' => $lvl2,
    'vals' => [
      'home_xg' => round($hx,2),
      'away_xg' => round($ax,2),
      'exact_score' => ($hs.'-'.$as),
      'avg_dist' => round($avgDist,2),
      'min_conf' => $min_mc,
    ],
    'meta' => ['mode'=>'TOP3', 'min_conf'=>$min_mc],
  ];
} else {
  // ✅ En dessous de 75% => on coupe (pas d’affichage)
  $score_multichance = [
    'ok' => false,
    'confidence_pct' => $pct2,
    'confidence_level' => $lvl2,
    'vals' => [
      'avg_dist' => round($avgDist,2),
      'min_conf' => $min_mc,
    ],
    'meta' => ['mode'=>'LOW_CONF', 'min_conf'=>$min_mc],
  ];
}
}
	  
    $home_goals = core_base_foot_build_team_goal_matrix($home_id, $league_id, $season, $N, 'all');
    $away_goals = core_base_foot_build_team_goal_matrix($away_id, $league_id, $season, $N, 'all');

    $home_played = (int)($home_goals['played'] ?? 0);
    $away_played = (int)($away_goals['played'] ?? 0);

    if ($home_played === 0) {
      $home_goals = core_base_foot_build_team_goal_matrix($home_id, $league_id, $season, 0, 'all');
      $home_played = (int)($home_goals['played'] ?? 0);
      if ($home_played === 0) $home_goals = core_base_foot_build_team_goal_matrix($home_id, $league_id, $season, 120, 'all');
    }

    if ($away_played === 0) {
      $away_goals = core_base_foot_build_team_goal_matrix($away_id, $league_id, $season, 0, 'all');
      $away_played = (int)($away_goals['played'] ?? 0);
      if ($away_played === 0) $away_goals = core_base_foot_build_team_goal_matrix($away_id, $league_id, $season, 120, 'all');
    }

    $standings = core_base_foot_get_standings($league_id, $season);
    $injuries  = core_base_foot_get_injuries_with_suspensions($fixture_id, CORE_BASE_FOOT_TZ, true);

    if (is_array($injuries) && empty($injuries['_error']) && isset($injuries[0])) {
      foreach ($injuries as &$inj) {
        if (!is_array($inj)) continue;
        if (!empty($inj['type']))   $inj['type']   = tp_match_analyzer_translate_injury_text((string)$inj['type']);
        if (!empty($inj['reason'])) $inj['reason'] = tp_match_analyzer_translate_injury_text((string)$inj['reason']);
        if (!empty($inj['player']['type']))   $inj['player']['type']   = tp_match_analyzer_translate_injury_text((string)$inj['player']['type']);
        if (!empty($inj['player']['reason'])) $inj['player']['reason'] = tp_match_analyzer_translate_injury_text((string)$inj['player']['reason']);
        if (!empty($inj['suspension']['type'])) $inj['suspension']['type'] = tp_match_analyzer_translate_injury_text((string)$inj['suspension']['type']);
      }
      unset($inj);
    }

    $h2h     = core_base_foot_get_h2h_seasons($home_id, $away_id, [$season, $season-1, $season-2], CORE_BASE_FOOT_TZ);
    $lineups = core_base_foot_get_lineups($fixture_id);

    $payload = [
      'match_stats' => [
        'home' => [
          'id' => $home_id,
          'name' => $home_name,
          'logo' => $home_logo ?: ($home_pack['team_logo'] ?? ''),
          'stats' => $home_pack,
        ],
        'away' => [
          'id' => $away_id,
          'name' => $away_name,
          'logo' => $away_logo ?: ($away_pack['team_logo'] ?? ''),
          'stats' => $away_pack,
        ],
      ],
      'potential_goals' => $pot,
'predictions' => [
  'double_chance_ppm' => $pred_dc,
  'win_ppm' => $pred_win,
  'goals_total' => $pred_goals, // ✅ AJOUT ICI
  'btts_yesno' => $btts,
	'team_to_score' => $team_score,
	'exact_score_xg' => $exact_score,
	'draw_xg' => $draw_xg,
	'score_multichance_xg' => $score_multichance,
'odds' => $odds_pack, // ✅ compat / debug
],

      'standings' => $standings,
      'injuries'  => $injuries,
      'h2h'       => $h2h,
      'lineups'   => $lineups,
      'goals_matrix' => [
        'n_requested' => $N,
        'home' => $home_goals,
        'away' => $away_goals,
      ],
      'meta' => [
        'time_used_sec' => round(microtime(true) - $t0, 2),
        'season' => $season,
        'league_id' => $league_id,
        'fixture_id' => $fixture_id,
      ],
    ];

    core_base_foot_cache_set($cache_key, $payload, 15 * 60);
    wp_send_json_success($payload);
  }
}
add_action('wp_ajax_tp_match_analyzer_details', 'tp_match_analyzer_ajax_details');
add_action('wp_ajax_nopriv_tp_match_analyzer_details', 'tp_match_analyzer_ajax_details');

/* ============================================================
   AJAX : L5 (5 derniers matchs d’une équipe)
   ============================================================ */
if (!function_exists('tp_match_analyzer_ajax_team_l5')) {
  function tp_match_analyzer_ajax_team_l5() {
    if (!check_ajax_referer('tp_match_analyzer_nonce', 'nonce', false)) {
      wp_send_json_error(['message' => 'Nonce invalide']);
    }

    $team_id = (int)($_POST['team_id'] ?? 0);
    $season  = (int)($_POST['season'] ?? (defined('CORE_BASE_FOOT_SEASON') ? CORE_BASE_FOOT_SEASON : 0));
    $tz      = defined('CORE_BASE_FOOT_TZ') ? CORE_BASE_FOOT_TZ : 'Europe/Paris';

    if ($team_id <= 0 || $season <= 0) {
      wp_send_json_error(['message' => 'Paramètres manquants (team_id/season)']);
    }

    $cache_key = "tp_match_analyzer:l7:v1:team={$team_id}:season={$season}";
    if (function_exists('core_base_foot_cache_get')) {
      $cached = core_base_foot_cache_get($cache_key);
      if (is_array($cached) && isset($cached['ok'])) {
        wp_send_json_success($cached);
      }
    }

    if (!function_exists('core_base_foot_api')) {
      wp_send_json_error(['message' => 'core_base_foot_api indisponible']);
    }

    try {
      // ✅ API-Sports fixtures : last=5 renvoie les 5 derniers matchs du team (toutes compétitions de la saison)
      $api = core_base_foot_api('fixtures', [
        'team'     => $team_id,
        'season'   => $season,
        'last'     => 5,
        'timezone' => $tz,
      ]);
    } catch (Throwable $e) {
      $api = null;
    }

    $resp = is_array($api) ? ($api['response'] ?? []) : [];
    if (!is_array($resp) || empty($resp)) {
      $out = ['ok'=>false, 'fixtures'=>[], '_error'=>'Aucun match renvoyé'];
      if (function_exists('core_base_foot_cache_set')) core_base_foot_cache_set($cache_key, $out, 30 * 60);
      wp_send_json_success($out);
    }

    $items = [];
    foreach ($resp as $fx) {
      if (!is_array($fx)) continue;

      $items[] = [
        'date'   => $fx['fixture']['date'] ?? '',
        'league' => [
          'name'  => $fx['league']['name'] ?? '',
          'round' => $fx['league']['round'] ?? '',
        ],
        'status' => [
          'short' => $fx['fixture']['status']['short'] ?? '',
          'long'  => $fx['fixture']['status']['long'] ?? '',
        ],
        'teams'  => [
          'home' => [
            'id'     => (int)($fx['teams']['home']['id'] ?? 0),
            'name'   => (string)($fx['teams']['home']['name'] ?? ''),
            'logo'   => (string)($fx['teams']['home']['logo'] ?? ''),
            'winner' => $fx['teams']['home']['winner'] ?? null,
          ],
          'away' => [
            'id'     => (int)($fx['teams']['away']['id'] ?? 0),
            'name'   => (string)($fx['teams']['away']['name'] ?? ''),
            'logo'   => (string)($fx['teams']['away']['logo'] ?? ''),
            'winner' => $fx['teams']['away']['winner'] ?? null,
          ],
        ],
        'goals' => [
          'home' => $fx['goals']['home'] ?? null,
          'away' => $fx['goals']['away'] ?? null,
        ],
      ];
    }

    $out = [
      'ok' => true,
      'fixtures' => $items,
      'meta' => ['team_id'=>$team_id, 'season'=>$season],
    ];

    if (function_exists('core_base_foot_cache_set')) core_base_foot_cache_set($cache_key, $out, 2 * HOUR_IN_SECONDS);
    wp_send_json_success($out);
  }
}
add_action('wp_ajax_tp_match_analyzer_team_l5', 'tp_match_analyzer_ajax_team_l5');
add_action('wp_ajax_nopriv_tp_match_analyzer_team_l5', 'tp_match_analyzer_ajax_team_l5');

/* ============================================================
   Shortcode
   ============================================================ */
if (!function_exists('tp_match_analyzer_shortcode')) {
  function tp_match_analyzer_shortcode($atts = []) {
    $fixtures = core_base_foot_get_fixtures_window();
    $nonce = wp_create_nonce('tp_match_analyzer_nonce');
    $ajax_url = admin_url('admin-ajax.php');

    $leagueMap = [];
    $statusMap = [];

    foreach ((array)$fixtures as $fx) {
      if (!is_array($fx) || isset($fx['_error'])) continue;

      $lid = (int)($fx['league']['id'] ?? 0);
      $lname = (string)($fx['league']['name'] ?? '');
      if ($lid && $lname) $leagueMap[$lid] = $lname;

      $stShort = (string)($fx['fixture']['status']['short'] ?? '');
      $stLong  = (string)($fx['fixture']['status']['long'] ?? '');
      if ($stShort) {
        $tr = tp_match_analyzer_translate_status($stShort, $stLong);
        $statusMap[$stShort] = $tr['short_fr'] ?? $stShort;
      }
    }

    ksort($leagueMap);
    ksort($statusMap);

    ob_start();
    ?>
<style>
/* =========================
   BASE
   ========================= */
.tpm-wrap{
  --bg:#070b14; --card:#0b1220; --card2:#0a1020;
  --txt:#e5e7eb; --muted:#9ca3af; --line:rgba(255,255,255,.10); --line2:rgba(255,255,255,.06);
  --shadow:0 18px 55px rgba(0,0,0,.45);
  --green:rgba(34,197,94,.22); --greenB:rgba(34,197,94,.40);
  --red:rgba(239,68,68,.20); --redB:rgba(239,68,68,.36);
  --pill:rgba(148,163,184,.14); --pillB:rgba(148,163,184,.26);
  --accent1:#22c55e; --accent2:#16a34a;
  --cta1:#a7f3d0; --cta2:#60a5fa;

  max-width:1120px;margin:0 auto;padding:14px;
  font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Arial;
  color:var(--txt);
}
.tpm-title{font-size:20px;font-weight:1000;letter-spacing:.2px;margin:6px 0 2px}
.tpm-sub{color:var(--muted);font-size:12px;margin-bottom:12px}

.tpm-filters{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin:10px 0 14px}
.tpm-field{display:flex;flex-direction:column;gap:6px}
.tpm-label{font-size:11px;color:var(--muted);font-weight:1000;letter-spacing:.2px}

.tpm-select,.tpm-input{
  min-width:220px;border-radius:14px;padding:10px 12px;border:1px solid var(--line);
  background:rgba(11,18,32,.92);color:var(--txt);outline:none;
  box-shadow:0 6px 18px rgba(0,0,0,.20) inset;
}
.tpm-select:focus,.tpm-input:focus{
  border-color:rgba(96,165,250,.45);
  box-shadow:0 0 0 3px rgba(96,165,250,.12)
}

.tpm-reset{
  border:0;border-radius:14px;padding:10px 12px;font-weight:1000;cursor:pointer;color:#07140f;
  background:linear-gradient(135deg,var(--cta1),var(--cta2));
  box-shadow:0 10px 26px rgba(0,0,0,.22);
}
.tpm-count{font-size:12px;color:var(--muted);margin-left:auto}

.tpm-grid{display:grid;grid-template-columns:1fr;gap:12px}
.tpm-card{
  background:linear-gradient(180deg,var(--card),var(--card2));
  border:1px solid rgba(255,255,255,.08);
  border-radius:18px;box-shadow:var(--shadow);
  padding:12px;color:var(--txt);
}
.tpm-top{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}
.tpm-league{font-weight:1000;font-size:13px}
.tpm-round{font-size:12px;color:var(--muted);margin-top:2px}
.tpm-when{font-weight:1000;font-size:13px;text-align:right}
.tpm-pill{
  display:inline-flex;align-items:center;gap:6px;
  padding:2px 9px;border-radius:999px;background:rgba(255,255,255,.10);
  border:1px solid rgba(255,255,255,.12);font-weight:1000;margin-left:6px;
}
.tpm-mid{display:grid;grid-template-columns:1fr 170px 1fr;gap:10px;align-items:center;margin:10px 0 6px}
.tpm-team{display:flex;gap:10px;align-items:center;min-width:0}
.tpm-right{justify-content:flex-end}
.tpm-logo{width:30px;height:30px;object-fit:contain;filter:drop-shadow(0 6px 10px rgba(0,0,0,.25))}
.tpm-name{font-weight:1000;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tpm-score{text-align:center}
.tpm-score-big{font-weight:1000;font-size:22px;letter-spacing:.2px}
.tpm-score-sub{font-size:11px;color:var(--muted);margin-top:2px}

.tpm-actions{
  display:flex;
  gap:10px;
  align-items:center;
  flex-wrap:wrap;
  margin-top:10px;
}
.tpm-hint{
  font-size:12px;
  color:var(--muted);
  flex:1;
  min-width:240px;
}
.tpm-panel{
  margin-top:10px;padding:12px;border-radius:16px;background:rgba(255,255,255,.04);
  border:1px solid rgba(255,255,255,.08);display:none
}
.tpm-loading{font-weight:1000}
.tpm-empty{font-size:12px;color:var(--muted)}

/* =========================
   STATS
   ========================= */
.tpm-stats{border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,.10);margin-top:6px;background:rgba(0,0,0,.12)}
.tpm-stats-head{background:linear-gradient(180deg,rgba(34,197,94,.20),rgba(34,197,94,.06));border-bottom:1px solid rgba(255,255,255,.08)}
.tpm-stats-title{
  padding:10px 12px;font-weight:1000;letter-spacing:.3px;color:#eafff2;
  display:flex;justify-content:space-between;align-items:center;gap:10px;
}
.tpm-meta-mini{font-size:11px;color:rgba(234,255,242,.70);font-weight:900}

.tpm-stats-teams{display:grid;grid-template-columns:1fr auto 1fr;gap:10px;align-items:center;padding:10px 12px 12px}
.tpm-teambox{
  display:flex;align-items:center;gap:10px;min-width:0;
  padding:9px 11px;border-radius:14px;background:rgba(0,0,0,.18);
  border:1px solid rgba(255,255,255,.08);
}
.tpm-teambox.right{justify-content:flex-end;flex-direction:row-reverse;text-align:right}
.tpm-teambox .stack{display:flex;flex-direction:column;min-width:0}
.tpm-teambox .nm{font-weight:1000;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tpm-teambox .sub{font-size:11px;color:var(--muted);font-weight:900;margin-top:1px}
.tpm-teambox .lg{width:26px;height:26px;object-fit:contain}

/* clickable team (injuries) */
.tpm-teambtn{
  appearance:none;background:transparent;border:0;padding:0;margin:0;
  cursor:pointer;color:inherit;text-align:inherit;width:100%;
}
.tpm-teambtn:focus{outline:none;box-shadow:0 0 0 3px rgba(96,165,250,.14);border-radius:14px}

/* middle buttons */
.tpm-midbtns{display:flex;gap:8px;justify-content:center;align-items:center;flex-wrap:wrap}
.tpm-mini{
  appearance:none;border:1px solid rgba(255,255,255,.12);
  background:rgba(255,255,255,.08);color:var(--txt);
  border-radius:999px;padding:7px 10px;font-size:12px;font-weight:1000;
  cursor:pointer;white-space:nowrap;
}
.tpm-mini:hover{background:rgba(255,255,255,.12)}
.tpm-mini:active{transform:translateY(1px)}
.tpm-mini:focus{outline:none;box-shadow:0 0 0 3px rgba(96,165,250,.14)}

.tpm-stats table{width:100%;border-collapse:collapse}
.tpm-stats th,.tpm-stats td{
  padding:12px;border-top:1px solid rgba(255,255,255,.08);
  border-right:1px solid rgba(255,255,255,.06);font-size:12px;vertical-align:middle
}
.tpm-stats th{
  width:280px;text-align:left;color:#d1d5db;background:rgba(255,255,255,.04);
  font-weight:1000
}
.tpm-stats td:last-child{border-right:0}
.tpm-rowhint{display:block;margin-top:6px;color:var(--muted);font-size:11px;font-weight:900}
.tpm-cells{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.tpm-tag{color:var(--muted);font-size:11px;font-weight:1000;min-width:74px}
.tpm-pill2{
  display:inline-flex;align-items:center;justify-content:center;min-width:54px;
  padding:6px 10px;border-radius:999px;font-weight:1000;border:1px solid var(--pillB);
  background:var(--pill);line-height:1;
}
.tpm-green{border-color:var(--greenB);background:var(--green)}
.tpm-red{border-color:var(--redB);background:var(--red)}

/* ✅ BADGES CONFIANCE */
.tpm-amber{border-color:rgba(251,191,36,.40)!important;background:rgba(251,191,36,.18)!important;}
.tpm-gray{border-color:rgba(148,163,184,.35)!important;background:rgba(148,163,184,.14)!important;}
.tpm-preds{margin-top:12px}

/* ✅ ACCORDION (Goal Matrix fermé par défaut) */
.tpm-acc-body{display:none}
.tpm-acc.is-open .tpm-acc-body{display:block}
.tpm-acc-toggle{display:inline-flex;align-items:center;gap:6px}

/* =========================
   MODAL (anti transparence)
   ========================= */
.tpm-modal{
  position:fixed; inset:0; display:none; z-index:2147483647;
  opacity:1 !important; filter:none !important; transform:none !important; mix-blend-mode:normal !important;
}
.tpm-modal.is-open{display:block}
.tpm-modal .backdrop{
  position:absolute; inset:0;
  background: rgba(0,0,0,.85) !important;
  backdrop-filter: blur(10px) !important;
  -webkit-backdrop-filter: blur(10px) !important;
  opacity:1 !important; filter:none !important;
}
.tpm-modal .panel{
  position:relative; max-width:980px; margin:6vh auto; border-radius:18px; overflow:hidden;
  background-color:#0b1220 !important;
  background-image: linear-gradient(180deg,#0b1220 0%, #070b14 100%) !important;
  border:1px solid rgba(255,255,255,.16) !important;
  box-shadow: 0 30px 90px rgba(0,0,0,.78) !important;
  opacity:1 !important; filter:none !important; mix-blend-mode:normal !important; isolation:isolate;
}
.tpm-modal .head{
  display:flex;justify-content:space-between;align-items:center;gap:10px;
  padding:12px 14px; background:#0a1020 !important;
  border-bottom:1px solid rgba(255,255,255,.12) !important; opacity:1 !important;
}
.tpm-modal .title{font-weight:1000;color:#fff}
.tpm-modal .close{
  appearance:none;border:1px solid rgba(255,255,255,.16);
  background:rgba(255,255,255,.10);color:var(--txt);
  border-radius:12px;padding:8px 10px;cursor:pointer;font-weight:1000
}
.tpm-modal .body{
  padding:14px; max-height:70vh; overflow:auto;
  background-color:#0b1220 !important; opacity:1 !important;
}

.tpm-mtbl{width:100%;border-collapse:collapse;border:1px solid rgba(255,255,255,.12);border-radius:14px;overflow:hidden;background-color:#0a1020 !important;}
.tpm-mtbl th,.tpm-mtbl td{padding:10px 12px;border-top:1px solid rgba(255,255,255,.08);font-size:12px;background-color:transparent;}
.tpm-mtbl th{background:#0b1220 !important;text-align:left;color:#fff;font-weight:1000}
.tpm-mtbl td{background:rgba(0,0,0,.18) !important;color:#e5e7eb}
.tpm-mtbl tbody tr:nth-child(even) td{background:rgba(255,255,255,.03) !important;}
.tpm-mtbl tbody tr:hover td{background:rgba(96,165,250,.10) !important;}
.tpm-rank{font-weight:1000}
.tpm-teamcell{display:flex;align-items:center;gap:10px;min-width:0}
.tpm-teamcell img{width:18px;height:18px;object-fit:contain}
.tpm-teamcell span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:900;color:#fff}

.tpm-badge{
  display:inline-flex;align-items:center;gap:6px;
  padding:4px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.12);
  background:rgba(255,255,255,.10);
  font-size:12px;font-weight:1000;color:#fff
}

.tpm-h2h-item{
  display:grid;grid-template-columns:170px 1fr 100px;gap:10px;
  padding:10px 12px;border:1px solid rgba(255,255,255,.10);border-radius:14px;
  background:#0a1020 !important;margin-bottom:8px;align-items:center
}
.tpm-h2h-date{font-size:11px;color:#cbd5e1;font-weight:1000}
.tpm-h2h-match{font-weight:1000;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#fff}
.tpm-h2h-score{justify-self:end;font-weight:1000;color:#fff}
.tpm-kpis{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 10px}

.tpm-inj-list{display:flex;flex-direction:column;gap:8px}
.tpm-inj{
  display:grid;grid-template-columns:48px 1fr;gap:10px;
  padding:10px 12px;border:1px solid rgba(255,255,255,.10);
  border-radius:14px;background:#0a1020 !important;align-items:center
}
.tpm-inj img{
  width:48px;height:48px;border-radius:12px;object-fit:cover;
  background:#111827 !important;border:1px solid rgba(255,255,255,.16) !important;
}
.tpm-inj .nm{font-weight:1000;color:#fff}
.tpm-inj .sub{font-size:12px;color:#cbd5e1;font-weight:900;margin-top:2px}

.tpm-lineups{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.tpm-lbox{border:1px solid rgba(255,255,255,.10);border-radius:16px;background:#0a1020 !important;overflow:hidden}
.tpm-lbox .h{
  padding:10px 12px;background:#0b1220 !important;border-bottom:1px solid rgba(255,255,255,.10);
  font-weight:1000;display:flex;justify-content:space-between;gap:10px;align-items:center;color:#fff
}
.tpm-lbox .b{padding:10px 12px}
.tpm-lsec{margin-top:10px}
.tpm-lsec .t{font-weight:1000;margin-bottom:6px;color:#fff}
.tpm-lul{margin:0;padding-left:16px}
.tpm-lul li{margin:4px 0;color:#e5e7eb}

@media (max-width:860px){
  .tpm-mid{grid-template-columns:1fr 120px 1fr}
  .tpm-select,.tpm-input{min-width:180px}
  .tpm-count{width:100%;margin-left:0}
  .tpm-stats th{width:auto}
  .tpm-stats-teams{grid-template-columns:1fr;gap:8px}
  .tpm-lineups{grid-template-columns:1fr}
  .tpm-h2h-item{grid-template-columns:1fr;gap:6px}
  .tpm-h2h-score{justify-self:start}
}
body .tpm-modal, body .tpm-modal *{opacity:1 !important;filter:none !important;}
.tpm-stats thead th{
  padding:12px;border-top:1px solid rgba(255,255,255,.08);
  border-right:1px solid rgba(255,255,255,.06);
  background:rgba(255,255,255,.04);
  color:#d1d5db;font-size:12px;font-weight:1000;
}
.tpm-stats thead th:last-child{border-right:0}
.muted{color:#cbd5e1}
	   /* =========================
   ✅ XG STRIP (3 items sur 1 ligne)
   ========================= */
.tpm-xg-grid{
  display:grid;
  grid-template-columns:repeat(3, minmax(0,1fr));
  gap:10px;
  align-items:stretch;
}
.tpm-xg-item{
  border:1px solid rgba(255,255,255,.10);
  background:rgba(0,0,0,.16);
  border-radius:14px;
  padding:10px 10px 9px;
  min-width:0;
}
.tpm-xg-top{
  display:flex;
  justify-content:space-between;
  gap:8px;
  align-items:flex-start;
}
.tpm-xg-label{
  font-weight:1000;
  font-size:12px;
  color:#e5e7eb;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
.tpm-xg-sub{
  margin-top:2px;
  font-size:11px;
  color:rgba(234,255,242,.65);
  font-weight:900;
}
.tpm-xg-pills{
  display:flex;
  gap:8px;
  align-items:center;
  justify-content:flex-end;
  flex-wrap:wrap;
}
.tpm-xg-note{
  margin-top:8px;
  font-size:11px;
  color:rgba(234,255,242,.65);
  font-weight:900;
}

@media (max-width:860px){
  .tpm-xg-grid{grid-template-columns:1fr; }
  .tpm-xg-label{white-space:normal;}
}
	   /* =========================
   ✅ TEAM TO SCORE (2 équipes sur 1 ligne)
   ========================= */
.tpm-tts-grid{
  display:grid;
  --tts-cols:2;
  grid-template-columns:repeat(var(--tts-cols), minmax(0,1fr));
  gap:10px;
  align-items:stretch;
}
.tpm-tts-item{
  border:1px solid rgba(255,255,255,.10);
  background:rgba(0,0,0,.16);
  border-radius:14px;
  padding:10px 10px 9px;
  min-width:0;
}
.tpm-tts-head{
  display:flex;
  justify-content:space-between;
  gap:8px;
  align-items:center;
}
.tpm-tts-name{
  font-weight:1000;
  font-size:12px;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
.tpm-tts-pick{
  font-weight:1000;
  font-size:12px;
  white-space:nowrap;
}
.tpm-tts-detail{
  margin-top:6px;
  font-size:11px;
  color:#cbd5e1;
  font-weight:900;
  line-height:1.35;
}
.tpm-tts-confitem{
  text-align:center;
}
@media (max-width:860px){
  .tpm-tts-grid{grid-template-columns:1fr;}
  .tpm-tts-name{white-space:normal;}
}
/* =========================
   ✅ FEU (nb de confiances fortes)
   ========================= */
/* ✅ FEU (nb de confiances fortes) */
.tpm-fires{
  display:none;
  gap:6px;
  align-items:center;
  margin-left:auto; /* pousse à droite dans .tpm-actions */
  flex-wrap:wrap;
}

.tpm-fire{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  min-width:28px;
  height:28px;
  padding:0 10px;
  border-radius:999px;
  border:1px solid rgba(251,191,36,.40);
  background:rgba(251,191,36,.16);
  font-weight:1000;
  font-size:12px;
  line-height:1;
  box-shadow:0 10px 22px rgba(0,0,0,.22);
}
	   .tpm-btn{
  border:0;border-radius:14px;padding:10px 12px;
  font-weight:1000;cursor:pointer;color:#07140f;
  background:linear-gradient(135deg,var(--cta1),var(--cta2));
  box-shadow:0 10px 26px rgba(0,0,0,.22);
}
.tpm-btn:active{transform:translateY(1px)}
/* =========================================================
   ✅ PREDICTIONS — UI v10 (plus clair / intuitif abonnés)
   Scope: uniquement le tableau PRÉDICTIONS (.tpm-preds)
   ========================================================= */
.tpm-preds{ overflow-x:auto; border-radius:16px; }
.tpm-preds table{ min-width:780px; }

/* Header plus lisible + sticky */
.tpm-preds thead th{
  position:sticky; top:0; z-index:3;
  background:rgba(11,18,32,.92);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom:1px solid rgba(255,255,255,.14);
}

/* Zebra + hover */
.tpm-preds tbody tr{ background:rgba(255,255,255,.02); }
.tpm-preds tbody tr:nth-child(even){ background:rgba(255,255,255,.045); }
.tpm-preds tbody tr:hover{ background:rgba(96,165,250,.10); }

/* Col 1 (marché) plus contrastée */
.tpm-preds tbody th{
  font-size:12px;
  font-weight:1000;
  letter-spacing:.2px;
  color:#eef2ff;
  background:rgba(255,255,255,.03);
}

/* Valeur: pick plus gros */
.tpm-preds tbody td:nth-child(2) b{
  font-size:13px;
  color:#ffffff;
}

/* Cote & confiance centrées */
.tpm-preds tbody td:nth-child(3),
.tpm-preds tbody td:nth-child(4){
  text-align:center;
  vertical-align:middle;
  white-space:nowrap;
}

/* Chip cote plus visible */
.tpm-pill2.tpm-odd{
  min-width:72px;
  padding:7px 12px;
  font-size:12px;
  border-color:rgba(255,255,255,.16);
  background:rgba(255,255,255,.08);
  box-shadow:0 8px 18px rgba(0,0,0,.18) inset;
}

/* ✅ OUI/NON/Indécis */
.tpm-yn{
  display:inline-flex;
  align-items:center;
  gap:6px;
  padding:6px 10px;
  border-radius:999px;
  border:1px solid rgba(255,255,255,.12);
  font-weight:1000;
  line-height:1;
}
.tpm-yn.yes{ background:rgba(34,197,94,.18); border-color:rgba(34,197,94,.40); }
.tpm-yn.no{  background:rgba(239,68,68,.16); border-color:rgba(239,68,68,.36); }
.tpm-yn.mid{ background:rgba(148,163,184,.12); border-color:rgba(148,163,184,.28); color:#cbd5e1; }

/* Non éligible / indispo */
.tpm-na{
  display:inline-flex;
  align-items:center;
  gap:6px;
  padding:6px 10px;
  border-radius:999px;
  background:rgba(148,163,184,.10);
  border:1px solid rgba(148,163,184,.26);
  color:#cbd5e1;
  font-weight:1000;
}

/* ✅ Confiance = % + barre */
.tpm-conf{
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:6px;
  min-width:92px;
}
.tpm-confbar{
  height:7px;
  width:92px;
  border-radius:999px;
  background:rgba(255,255,255,.10);
  overflow:hidden;
  border:1px solid rgba(255,255,255,.10);
}
.tpm-confbar > i{
  display:block;
  height:100%;
  width:0%;
  background:rgba(255,255,255,.35);
}

/* ✅ Confiances élevées (chips) */
.tpm-fires{ display:none; gap:6px; align-items:center; flex-wrap:wrap; margin-left:auto; }
.tpm-firechip{
  display:inline-flex;
  align-items:center;
  gap:6px;
  padding:6px 10px;
  border-radius:999px;
  border:1px solid rgba(251,191,36,.42);
  background:rgba(251,191,36,.14);
  font-weight:1000;
  font-size:12px;
  line-height:1;
}
.tpm-firechip.no{
  border-color:rgba(239,68,68,.38);
  background:rgba(239,68,68,.12);
}
/* --- Jauge confiance --- */
.tpm-conf{
  display:flex;
  flex-direction:column;
  gap:6px;
  align-items:flex-start;
}

.tpm-conf-pill{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  padding:4px 10px;
  border-radius:999px;
  font-weight:800;
  font-size:12px;
  border:1px solid rgba(255,255,255,.10);
  background:rgba(255,255,255,.06);
}

.tpm-conf-bar{
  width:110px;            /* ajuste si tu veux plus long */
  height:7px;
  border-radius:999px;
  overflow:hidden;
  background:rgba(255,255,255,.10);
  border:1px solid rgba(255,255,255,.08);
}

.tpm-conf-fill{
  display:block;
  height:100%;
  width:0%;
  border-radius:999px;
  transition:width .35s ease;
}

/* --- Couleurs (3 niveaux) --- */
.tpm-conf-strong .tpm-conf-pill{
  background:rgba(0, 200, 83, .18);
  border-color:rgba(0, 200, 83, .35);
}
.tpm-conf-strong .tpm-conf-fill{
  background:linear-gradient(90deg, rgba(0,200,83,.65), rgba(0,230,118,.95));
}

.tpm-conf-medium .tpm-conf-pill{
  background:rgba(255, 179, 0, .18);
  border-color:rgba(255, 179, 0, .35);
}
.tpm-conf-medium .tpm-conf-fill{
  background:linear-gradient(90deg, rgba(255,179,0,.65), rgba(255,213,79,.95));
}

.tpm-conf-low .tpm-conf-pill{
  background:rgba(229, 57, 53, .18);
  border-color:rgba(229, 57, 53, .35);
}
.tpm-conf-low .tpm-conf-fill{
  background:linear-gradient(90deg, rgba(229,57,53,.65), rgba(255,82,82,.95));
}

.tpm-conf-na .tpm-conf-fill{
  background:rgba(255,255,255,.15);
}
/* ✅ L5 buttons sous chaque équipe */
.tpm-teamcol{display:flex;flex-direction:column;gap:8px;min-width:0}
.tpm-teamcol.right{align-items:flex-end}
.tpm-teamcol .tpm-teambtn{width:100%}
.tpm-l5btn{width:max-content}
@media (max-width:860px){
  .tpm-teamcol.right{align-items:flex-start}
}
/* =========================
   ✅ TABLES RESPONSIVE (≤600px)
   Inspiré du snippet "font-size/padding"
   ========================= */

/* Desktop / défaut (si tu veux forcer comme dans ton snippet, dé-commente) */
/*
.tpm-stats table{ font-size:12px; width:100%; border-collapse:collapse; }
.tpm-stats th, .tpm-stats td{ padding:5px; text-align:center; }
.tpm-stats th{ color:#fff; background:#2797C6; }
*/

@media only screen and (max-width:600px){
  /* Table Stats (les 2 équipes) */
  .tpm-stats table{
    font-size:10px; /* comme ton snippet */
  }
  .tpm-stats th,
  .tpm-stats td{
    padding:3px;   /* comme ton snippet */
  }

  /* (optionnel) Table du modal aussi, si tu veux le même effet */
  .tpm-mtbl{
    font-size:10px;
  }
  .tpm-mtbl th,
  .tpm-mtbl td{
    padding:6px 8px;
  }
}
</style>

    <div class="tpm-wrap" data-ajax="<?php echo esc_url($ajax_url); ?>" data-nonce="<?php echo esc_attr($nonce); ?>">
      <div class="tpm-title">TP Match Analyzer — Saison <?php echo (int)CORE_BASE_FOOT_SEASON; ?></div>
      <div class="tpm-sub">Fenêtre : aujourd’hui → demain 10h (<?php echo esc_html(CORE_BASE_FOOT_TZ); ?>)</div>

      <div class="tpm-filters">
        <div class="tpm-field">
          <div class="tpm-label">Filtrer par ligue</div>
          <select class="tpm-select" id="tpm-filter-league">
            <option value="">Toutes les ligues</option>
            <?php foreach ($leagueMap as $lid => $lname): ?>
              <option value="<?php echo (int)$lid; ?>"><?php echo esc_html($lname); ?> (<?php echo (int)$lid; ?>)</option>
            <?php endforeach; ?>
          </select>
        </div>

        <div class="tpm-field">
          <div class="tpm-label">Statut du match</div>
          <select class="tpm-select" id="tpm-filter-status">
            <option value="">Tous les statuts</option>
            <?php foreach ($statusMap as $stShort => $stFr): ?>
              <option value="<?php echo esc_attr($stShort); ?>"><?php echo esc_html($stFr . ' (' . $stShort . ')'); ?></option>
            <?php endforeach; ?>
          </select>
        </div>

        <div class="tpm-field">
          <div class="tpm-label">Recherche équipe</div>
          <input class="tpm-input" id="tpm-filter-team" type="text" placeholder="Ex: Bologna, Fiorentina, PSG...">
        </div>

        <button class="tpm-reset" id="tpm-filter-reset" type="button">Réinitialiser</button>
        <div class="tpm-count" id="tpm-count"></div>
      </div>

      <div class="tpm-grid" id="tpm-grid">
        <?php
        if (!$fixtures) {
          echo '<div class="tpm-card"><div class="tpm-empty">Aucun match dans la fenêtre.</div></div>';
        } else {
          foreach ($fixtures as $fx) {
            if (!is_array($fx)) continue;

            if (isset($fx['_error'])) {
              echo '<div class="tpm-card"><div class="tpm-empty">Erreur ligue '.(int)$fx['_league'].' : '.esc_html($fx['_error']).'</div></div>';
              continue;
            }

            $fixture_id = (int)($fx['fixture']['id'] ?? 0);
            $league_id  = (int)($fx['league']['id'] ?? 0);

            $league = (string)($fx['league']['name'] ?? '');
            $round  = (string)($fx['league']['round'] ?? '');
            $roundFr = tp_match_analyzer_translate_round($round);

            $home = (string)($fx['teams']['home']['name'] ?? 'Domicile');
            $away = (string)($fx['teams']['away']['name'] ?? 'Extérieur');
            $home_id = (int)($fx['teams']['home']['id'] ?? 0);
            $away_id = (int)($fx['teams']['away']['id'] ?? 0);

            $hlogo = (string)($fx['teams']['home']['logo'] ?? '');
            $alogo = (string)($fx['teams']['away']['logo'] ?? '');

            $status = (string)($fx['fixture']['status']['short'] ?? '');
            $statusLong = (string)($fx['fixture']['status']['long'] ?? '');
            $trStatus = tp_match_analyzer_translate_status($status, $statusLong);

            $dateStr = (string)($fx['fixture']['date'] ?? '');
            $when = $dateStr ? (function_exists('core_base_foot_format_match_datetime')
              ? core_base_foot_format_match_datetime($dateStr, CORE_BASE_FOOT_TZ)
              : $dateStr
            ) : '';

            $score = '—';
            if (function_exists('core_base_foot_get_ft_goals')) {
              [$sH,$sA] = core_base_foot_get_ft_goals($fx);
              if (is_int($sH) && is_int($sA)) $score = esc_html($sH.' - '.$sA);
            } else {
              $scoreH = $fx['goals']['home'] ?? null;
              $scoreA = $fx['goals']['away'] ?? null;
              if ($scoreH !== null || $scoreA !== null) $score = esc_html((string)$scoreH.' - '.(string)$scoreA);
            }

            $panelId = 'tpm-panel-' . $fixture_id;
            $teamSearch = function_exists('core_base_foot_strtolower')
              ? core_base_foot_strtolower($home.' '.$away)
              : strtolower($home.' '.$away);
            ?>
            <div class="tpm-card"
              data-league="<?php echo (int)$league_id; ?>"
              data-status="<?php echo esc_attr($status); ?>"
              data-teams="<?php echo esc_attr($teamSearch); ?>">

              <div class="tpm-top">
                <div>
                  <div class="tpm-league"><?php echo esc_html($league); ?></div>
                  <div class="tpm-round"><?php echo esc_html($roundFr ?: $round); ?></div>
                </div>
                <div>
                  <div class="tpm-when">
                    <?php echo esc_html($when); ?>
                    <span class="tpm-pill"><?php echo esc_html($trStatus['short_fr'] ?? $status); ?></span>
                  </div>
                  <div class="tpm-round" style="text-align:right">
                    <?php echo esc_html($trStatus['long_fr'] ?? $statusLong); ?>
                  </div>
                </div>
              </div>

              <div class="tpm-mid">
                <div class="tpm-team">
                  <?php if ($hlogo): ?><img class="tpm-logo" src="<?php echo esc_url($hlogo); ?>" alt=""><?php endif; ?>
                  <div class="tpm-name"><?php echo esc_html($home); ?></div>
                </div>

                <div class="tpm-score">
                  <div class="tpm-score-big"><?php echo $score; ?></div>
                  <div class="tpm-score-sub"><?php echo (int)$league_id; ?> · Saison <?php echo (int)CORE_BASE_FOOT_SEASON; ?></div>
                </div>

                <div class="tpm-team tpm-right">
                  <div class="tpm-name"><?php echo esc_html($away); ?></div>
                  <?php if ($alogo): ?><img class="tpm-logo" src="<?php echo esc_url($alogo); ?>" alt=""><?php endif; ?>
                </div>
              </div>

  <div class="tpm-actions">
                <button class="tpm-btn"
                  data-fixture="<?php echo (int)$fixture_id; ?>"
                  data-league="<?php echo (int)$league_id; ?>"
                  data-home="<?php echo (int)$home_id; ?>"
                  data-away="<?php echo (int)$away_id; ?>"
                  data-home-name="<?php echo esc_attr($home); ?>"
                  data-away-name="<?php echo esc_attr($away); ?>"
                  data-home-logo="<?php echo esc_url($hlogo); ?>"
                  data-away-logo="<?php echo esc_url($alogo); ?>"
                  data-target="<?php echo esc_attr($panelId); ?>">
                  Ouvrir l’analyse
                </button>

                <!-- ✅ FEU : badges confiance forte (AU BON ENDROIT) -->
                <div class="tpm-fires" id="tpm-fires-<?php echo (int)$fixture_id; ?>" title="Confiances élevées"></div>
              </div>

              <!-- ✅ PANEL DOIT RESTER DANS LA CARD -->
              <div class="tpm-panel" id="<?php echo esc_attr($panelId); ?>">
                <div class="tpm-loading">Chargement…</div>
              </div>
            </div>
            <?php
          }
        }
        ?>
      </div>
    </div>

    <!-- ✅ MODAL UNIQUE -->
    <div class="tpm-modal" id="tpm-modal" aria-hidden="true">
      <div class="backdrop" data-close="1"></div>
      <div class="panel" role="dialog" aria-modal="true">
        <div class="head">
          <div class="title" id="tpm-modal-title">Modal</div>
          <button class="close" type="button" data-close="1">Fermer ✕</button>
        </div>
        <div class="body" id="tpm-modal-body"></div>
      </div>
    </div>

<script>
(function(){
  const root = document.querySelector('.tpm-wrap');
  if(!root) return;

  const ajaxUrl = root.getAttribute('data-ajax');
  const nonce = root.getAttribute('data-nonce');
  const store = new Map();

  const leagueSel = document.getElementById('tpm-filter-league');
  const statusSel = document.getElementById('tpm-filter-status');
  const teamInp = document.getElementById('tpm-filter-team');
  const resetBtn = document.getElementById('tpm-filter-reset');
  const countEl = document.getElementById('tpm-count');

  const modal = document.getElementById('tpm-modal');
  const modalTitle = document.getElementById('tpm-modal-title');
  const modalBody = document.getElementById('tpm-modal-body');

  const norm = s => (s ?? '').toString().toLowerCase().trim();
const esc = s => (s ?? '').toString()
  .replace(/&/g,'&')
  .replace(/</g,'<')
  .replace(/>/g,'>')
  .replace(/"/g,'"')
  .replace(/'/g,''');

// =========================
// ✅ PERSISTENCE badges "feu" (localStorage)
// =========================
const SEASON = <?php echo (int)CORE_BASE_FOOT_SEASON; ?>;
const FIRES_STORAGE_KEY = `tp_match_analyzer:fires:v1:season=${SEASON}`;
const FIRES_TTL_MS = 48 * 60 * 60 * 1000; // 48h (ajuste)

function firesLoad(){
  try { return JSON.parse(localStorage.getItem(FIRES_STORAGE_KEY) || '{}') || {}; }
  catch(e){ return {}; }
}
function firesSave(obj){
  try { localStorage.setItem(FIRES_STORAGE_KEY, JSON.stringify(obj)); } catch(e){}
}
function firesPrune(obj){
  const now = Date.now();
  for(const k in obj){
    const ts = Number(obj?.[k]?.ts || 0);
    if(!ts || (now - ts) > FIRES_TTL_MS) delete obj[k];
  }
  return obj;
}
function firesWrite(fxid, chips){
  const fx = String(fxid || '');
  const obj = firesPrune(firesLoad());
  if(!chips || !chips.length){
    delete obj[fx];
  } else {
    obj[fx] = { ts: Date.now(), chips: chips };
  }
  firesSave(obj);
}
function restoreFireBadges(){
  const obj = firesPrune(firesLoad());
  const now = Date.now();

  document.querySelectorAll('.tpm-fires[id^="tpm-fires-"]').forEach(el=>{
    const fxid = el.id.replace('tpm-fires-','');
    const entry = obj[fxid];
    if(!entry || !Array.isArray(entry.chips) || !entry.chips.length) return;

    const ts = Number(entry.ts || 0);
    if(!ts || (now - ts) > FIRES_TTL_MS) return;

    el.style.display = 'inline-flex';
    el.innerHTML = entry.chips
      .map(c => `<span class="tpm-firechip ${c.isNo ? 'no' : ''}">${esc(c.txt)}</span>`)
      .join('');
  });

  firesSave(obj); // sauvegarde après prune
}

  function fmtDate(iso){
    if(!iso) return '';
    try{
      const d = new Date(iso);
      if(isNaN(d.getTime())) return iso;
      return d.toLocaleDateString('fr-FR',{weekday:'short', day:'2-digit', month:'2-digit'})+' '+d.toLocaleTimeString('fr-FR',{hour:'2-digit', minute:'2-digit'});
    }catch(e){ return iso; }
  }

  function applyFilters(){
    const leagueVal = leagueSel ? leagueSel.value : '';
    const statusVal = statusSel ? statusSel.value : '';
    const teamVal = norm(teamInp ? teamInp.value : '');
    const cards = Array.from(document.querySelectorAll('.tpm-card'));
    let shown = 0;

    for(const c of cards){
      const cl = c.getAttribute('data-league') || '';
      const cs = c.getAttribute('data-status') || '';
      const ct = c.getAttribute('data-teams') || '';
      let ok = true;
      if(leagueVal && cl !== leagueVal) ok = false;
      if(ok && statusVal && cs !== statusVal) ok = false;
      if(ok && teamVal && !ct.includes(teamVal)) ok = false;
      c.style.display = ok ? '' : 'none';
      if(ok) shown++;
    }
    if(countEl) countEl.textContent = shown + ' match(s) affiché(s)';
  }
  function resetFilters(){
    if(leagueSel) leagueSel.value = '';
    if(statusSel) statusSel.value = '';
    if(teamInp) teamInp.value = '';
    applyFilters();
  }
  leagueSel && leagueSel.addEventListener('change', applyFilters);
  statusSel && statusSel.addEventListener('change', applyFilters);
  teamInp && teamInp.addEventListener('input', applyFilters);
  resetBtn && resetBtn.addEventListener('click', resetFilters);
  applyFilters();

  function openModal(title, html){
    modalTitle.textContent = title || 'Infos';
    modalBody.innerHTML = html || '<div class="muted">Aucune donnée.</div>';
    modal.classList.add('is-open');
    modal.setAttribute('aria-hidden','false');
    document.body.style.overflow = 'hidden';
  }
  function closeModal(){
    modal.classList.remove('is-open');
    modal.setAttribute('aria-hidden','true');
    modalBody.innerHTML = '';
    document.body.style.overflow = '';
  }
  modal.addEventListener('click', (e)=>{
    if(e.target && e.target.getAttribute('data-close')==='1') closeModal();
  });
  document.addEventListener('keydown', (e)=>{
    if(e.key === 'Escape' && modal.classList.contains('is-open')) closeModal();
  });

  const pill = (val, cls='') => `<span class="tpm-pill2 ${cls}">${esc(val)}</span>`;
  const confClass = (level='') => {
    const lv = (level || '').toLowerCase();
    if(lv.includes('fort')) return 'tpm-green';
    if(lv.includes('moy'))  return 'tpm-amber';
    if(lv.includes('faib')) return 'tpm-gray';
    return 'tpm-gray';
  };
	
function confBadge(pct, level){
  const p = Number(pct);
  if(!Number.isFinite(p)) {
    return `
      <div class="tpm-conf tpm-conf-na">
        <div class="tpm-conf-pill">—</div>
        <div class="tpm-conf-bar"><span class="tpm-conf-fill" style="width:0%"></span></div>
      </div>`;
  }

  const v = Math.max(0, Math.min(100, Math.round(p)));
  const cls = (v >= 80) ? 'tpm-conf-strong' : (v >= 61) ? 'tpm-conf-medium' : 'tpm-conf-low';

  return `
    <div class="tpm-conf ${cls}" data-pct="${v}">
      <div class="tpm-conf-pill">${v}%</div>
      <div class="tpm-conf-bar">
        <span class="tpm-conf-fill" style="width:${v}%"></span>
      </div>
    </div>`;
}

restoreFireBadges();
	
	const ynChip = (v) => {
  const s = String(v || '').toUpperCase().trim();
  if(s === 'OUI' || s === 'YES') return `<span class="tpm-yn yes">✅ OUI</span>`;
  if(s === 'NON' || s === 'NO')  return `<span class="tpm-yn no">❌ NON</span>`;
  if(s.includes('IND'))          return `<span class="tpm-yn mid">🤷 Indécis</span>`;
  return `<span class="tpm-yn mid">${esc(v || '—')}</span>`;
};

	
	  // ✅ ICI : ajoute ttsSide (elle utilise esc + confBadge)
  function ttsSide(sideObj, teamName, oppName){
    let pick = '<span class="muted">Indécis</span>';
    let detail = '<span class="muted">Conditions non réunies</span>';
    let conf = confBadge(null,'');

    if(!sideObj) return { pick, detail, conf };

    const pred = String(sideObj.prediction || 'Indécis');
    const v = sideObj.vals || {};

    const baseDetail =
      `xG <b>${esc(v.team_xg ?? '—')}</b> · L5 score% <b>${esc(v.team_last5_score1pct ?? '—')}</b>% · gfpm <b>${esc(v.team_last5_gfpm ?? '—')}</b>`
      + ` · ${esc(oppName)} concede% <b>${esc(v.opp_last5_concede1pct ?? '—')}</b>% · gapm <b>${esc(v.opp_last5_gapm ?? '—')}</b>`;

if(sideObj.ok){
  pick = ynChip(pred);
      conf = confBadge(sideObj.confidence_pct, sideObj.confidence_level);
      const rel = (sideObj.reliability_l5 !== null && sideObj.reliability_l5 !== undefined)
        ? ` · rel <b>${esc(sideObj.reliability_l5)}</b>`
        : '';
      detail = baseDetail + rel;
    } else {
      pick = ynChip(pred || 'Indécis');
      detail = baseDetail;
      conf = confBadge(null,'');
    }

    return { pick, detail, conf };
  }
function isHighConf(obj){
  if(!obj || obj.ok !== true) return false;
  const pct = Number(obj.confidence_pct ?? NaN);
  const lvl = String(obj.confidence_level || '').toLowerCase();
  return (lvl.includes('fort')) || (!isNaN(pct) && pct >= 80);
}

function countHighConfs(d){
  const p = d?.predictions || {};
  let n = 0;
  if(isHighConf(p.double_chance_ppm)) n++;
  if(isHighConf(p.win_ppm)) n++;
  if(isHighConf(p.goals_total)) n++;
  if(isHighConf(p.btts_yesno)) n++;
  if(isHighConf(p.team_to_score?.home)) n++;
  if(isHighConf(p.team_to_score?.away)) n++;
  return n;
}

function getHighConfChips(d){
  const p = d?.predictions || {};
  const ms = d?.match_stats || {};
  const hName = ms?.home?.name || 'Domicile';
  const aName = ms?.away?.name || 'Extérieur';
  const out = [];

  const push = (txt, isNo=false) => out.push({txt, isNo});

  if(isHighConf(p.double_chance_ppm)){
    const pr = String(p.double_chance_ppm.prediction||'').toUpperCase().replace(/\s+/g,'');
    push(`🔥 DC ${pr}`);
  }
  if(isHighConf(p.win_ppm)){
    const pr = String(p.win_ppm.prediction||'').toLowerCase();
    const team = (pr==='1'||pr==='home'||pr==='dom'||pr==='h') ? hName : aName;
    push(`🔥 WIN ${team}`);
  }
  if(isHighConf(p.goals_total)){
    const lbl = p.goals_total.label || p.goals_total.pick || p.goals_total.prediction || 'Buts';
    push(`🔥 BUTS ${lbl}`.trim());
  }
  if(isHighConf(p.btts_yesno)){
    const pr = String(p.btts_yesno.prediction||'').toUpperCase();
    push(`🔥 BTTS ${pr}`, pr === 'NON');
  }
  if(isHighConf(p.team_to_score?.home)){
    const pr = String(p.team_to_score.home.prediction||'').toUpperCase();
    push(`🔥 ${hName} marque ${pr}`, pr === 'NON');
  }
  if(isHighConf(p.team_to_score?.away)){
    const pr = String(p.team_to_score.away.prediction||'').toUpperCase();
    push(`🔥 ${aName} marque ${pr}`, pr === 'NON');
  }
if (d?.predictions?.score_multichance_xg?.ok === true
    && Number(d.predictions.score_multichance_xg.confidence_pct || 0) >= 75) {
  push(`🔥 SCORE MC ${d.predictions.score_multichance_xg.label || ''}`.trim());
}

  return out;
}

function updateFireBadges(fxid, d){
  const el = document.getElementById('tpm-fires-' + String(fxid));
  if(!el) return;

  const chips = getHighConfChips(d);

  if(!chips.length){
    el.style.display = 'none';
    el.innerHTML = '';
    firesWrite(fxid, []); // ✅ supprime en mémoire
    return;
  }

  el.style.display = 'inline-flex';
  el.innerHTML = chips
    .map(c => `<span class="tpm-firechip ${c.isNo ? 'no' : ''}">${esc(c.txt)}</span>`)
    .join('');

  firesWrite(fxid, chips); // ✅ sauvegarde en mémoire
}


/* ============================================================
   ✅ 1 SEUL TABLEAU PRÉDICTIONS (version COMPACT)
   - Supprime les petites lignes sous le pick
   - Garde les détails en tooltip (title)
   - Ajout “if function exists” (JS) pour éviter redéclaration
   ============================================================ */

if (typeof window.predictionsUnifiedTable !== 'function') {
  window.predictionsUnifiedTable = function predictionsUnifiedTable(d){
    const fx = d?.meta?.fixture_id ?? '';

    const matchStats = d?.match_stats || {};
    const hName = matchStats?.home?.name || 'Domicile';
    const aName = matchStats?.away?.name || 'Extérieur';

    const dc   = d?.predictions?.double_chance_ppm || null;
    const win  = d?.predictions?.win_ppm || null;
    const pot  = d?.potential_goals || null;

    const gt   = d?.predictions?.goals_total || null;
    const btts = d?.predictions?.btts_yesno || null;

    const tts  = d?.predictions?.team_to_score || null;
    const es   = d?.predictions?.exact_score_xg || null;

    const draw = d?.predictions?.draw_xg || null;
    const msc  = d?.predictions?.score_multichance_xg || null;
const showMSC = (msc && msc.ok === true && Number(msc.confidence_pct || 0) >= 75);

    // ✅ ODDS : dans ton payload elles sont dans predictions.odds
    const oddsPack = d?.predictions?.odds || d?.odds || {};
    const oddsMap  = oddsPack?.map || {};

    // -------------------------
    // Helpers odds
    // -------------------------
    const nOdd = (x) => {
      const v = Number(String(x ?? '').replace(',', '.'));
      return (Number.isFinite(v) && v > 1.0001) ? v : null;
    };

    const oddPill = (odd) => {
      const v = nOdd(odd);
      if(!v) return pill('—','tpm-gray');
      return pill(v.toFixed(2), 'tpm-odd');
    };

    const normSel = (s) => String(s ?? '')
      .toLowerCase()
      .trim()
      .replace(/\s+/g,'')
      .replace(/[.\-\/:]/g,'')
      .replace(/,/g,'.');

    function findMarket(re){
      for(const k in oddsMap){
        if(!Object.prototype.hasOwnProperty.call(oddsMap,k)) continue;
        if(re.test(String(k))) return oddsMap[k];
      }
      return null;
    }

    function pickOdd(mkt, labels){
      if(!mkt) return null;
      const wanted = (labels || []).map(normSel);

      for(const sel in mkt){
        const s0 = normSel(sel);
        if(wanted.includes(s0)) return mkt[sel];
      }
      // fallback fuzzy
      for(const sel in mkt){
        const s0 = normSel(sel);
        for(const w of wanted){
          if(w && (s0.includes(w) || w.includes(s0))) return mkt[sel];
        }
      }
      return null;
    }

    function pickTotalOdd(mkt, ou, line){
      if(!mkt || !ou || line===null || line===undefined) return null;

      const L = String(line).replace(',', '.');
      const isOver = String(ou).toLowerCase() === 'over';

      for(const sel in mkt){
        const s = String(sel).toLowerCase().replace(',', '.').trim();

        const okOU = isOver
          ? (s.includes('over') || /^o[0-9]/.test(s) || s.startsWith('o ') || s.startsWith('o'))
          : (s.includes('under') || /^u[0-9]/.test(s) || s.startsWith('u ') || s.startsWith('u'));

        if(okOU && s.includes(L)) return mkt[sel];
      }
      return null;
    }

    function marketLooksLikeOU(m){
      if(!m || typeof m !== 'object') return false;
      const sels = Object.keys(m);
      let c = 0;
      for(const s of sels){
        const t = String(s).toLowerCase().replace(',', '.').trim();
        if((t.includes('over') || t.includes('under') || /^o[0-9]/.test(t) || /^u[0-9]/.test(t)) && /\d/.test(t)) c++;
      }
      return c >= 2;
    }

    const mtot = (() => {
      const patterns = [
        /goals\s*over\/under/i,
        /total\s*goals/i,
        /over\/under/i,
        /nombre\s*de\s*buts/i,
        /total\s*buts/i,
      ];

      // 1) priorité aux clés “propres”
      for(const re of patterns){
        for(const k in oddsMap){
          if(!Object.prototype.hasOwnProperty.call(oddsMap,k)) continue;
          if(re.test(String(k))){
            const m = oddsMap[k];
            if(marketLooksLikeOU(m)) return m;
          }
        }
      }

      // 2) fallback : marché qui contient le + de sélections over/under
      let best = null, bestScore = 0;
      for(const k in oddsMap){
        if(!Object.prototype.hasOwnProperty.call(oddsMap,k)) continue;
        const m = oddsMap[k];
        if(!m || typeof m !== 'object') continue;
        let score = 0;
        for(const s in m){
          const t = String(s).toLowerCase().replace(',', '.');
          if((t.includes('over') || t.includes('under')) && /\d/.test(t)) score++;
        }
        if(score > bestScore){ bestScore = score; best = m; }
      }
      return bestScore >= 2 ? best : null;
    })();

    // marchés usuels
    const m1x2  = findMarket(/match winner|1x2|resultat du match|resultat|winner/);
    const mdc   = findMarket(/double chance|doublechance/);
    const mbtts = findMarket(/both teams to score|btts|les 2 equipes|both teams/);
    const mcs   = findMarket(/correct score|score exact/);

    // ✅ Team totals (Équipe marque = Team Total Over 0.5)
    const mTotHome = findMarket(/(^|\b)total\s*-\s*home(\b|$)|home\s*total|total\s*home|team\s*total.*home/);
    const mTotAway = findMarket(/(^|\b)total\s*-\s*away(\b|$)|away\s*total|total\s*away|team\s*total.*away/);

    // -------------------------
    // ✅ helper tooltip (strip tags)
    // -------------------------
    const plain = (html) => String(html || '')
      .replace(/<[^>]*>/g, '')
      .replace(/\s+/g, ' ')
      .trim();

    // -------------------------
    // ✅ xG strip
    // -------------------------
    let xgTot = '—', xgH = '—', xgA = '—';
    if(pot){
      const hx = Number(pot?.home_xg ?? NaN);
      const ax = Number(pot?.away_xg ?? NaN);
      xgH = Number.isFinite(hx) ? hx.toFixed(2) : '—';
      xgA = Number.isFinite(ax) ? ax.toFixed(2) : '—';
      xgTot = (Number.isFinite(hx) && Number.isFinite(ax)) ? (hx + ax).toFixed(2) : '—';
    }

    // -------------------------
    // ✅ Double chance + odd
    // -------------------------
    let dcPick = '—', dcDetail = '', dcConf = confBadge(null,''), dcOdd = null;
    if(dc && dc.ok){
      const p = String(dc.prediction ?? '').toUpperCase().replace(/\s+/g,'');
      const team = (p === '1X') ? hName : (p === 'X2') ? aName : '';
      dcPick = team ? `<b>${esc(team)} ou nul</b>` : `<b>${esc(dc.prediction)}</b>`;
      dcDetail = `PPM : <b>${esc(dc.home_ppm_proj)}</b> vs <b>${esc(dc.away_ppm_proj)}</b> · diff ${esc(dc.diff)}`;
      dcConf = confBadge(dc.confidence_pct, dc.confidence_level);

      dcOdd = (p === '1X')
        ? pickOdd(mdc, ['1X','Home/Draw','Home or Draw','1 or Draw'])
        : (p === 'X2')
          ? pickOdd(mdc, ['X2','Draw/Away','Draw or Away','X or 2'])
          : pickOdd(mdc, [p]);
    } else if(dc && dc.ok === false){
      dcPick = '<span class="tpm-na">⛔ Non éligible</span>';
      dcDetail = `diff ${esc(dc.diff ?? '—')} · seuil ${esc(dc.threshold ?? '—')}`;
      dcConf = confBadge(dc.confidence_pct ?? null, dc.confidence_level ?? '');
    }

    // -------------------------
    // ✅ Win 1/2 + odd
    // -------------------------
    let winPick = '—', winDetail = '', winConf = confBadge(null,''), winOdd = null;
    if(win && win.ok){
      const p = String(win.prediction ?? '').trim().toLowerCase();
      const isHome = (p === '1' || p === 'home' || p === 'h' || p === 'dom' || p === 'domicile');
      const team = isHome ? hName : aName;
      winPick = `<b>${esc(team)}</b>`;
      winDetail = `|diff| ${esc(win.abs_diff ?? '—')} · seuil ${esc(win.threshold ?? '—')}`;
      winConf = confBadge(win.confidence_pct, win.confidence_level);

      winOdd = isHome
        ? pickOdd(m1x2, ['1','Home'])
        : pickOdd(m1x2, ['2','Away']);
    } else if(win && win.ok === false){
      winPick = '<span class="tpm-na">⛔ Non éligible</span>';
      winDetail = `|diff| ${esc(win.abs_diff ?? '—')} · seuil ${esc(win.threshold ?? '—')}`;
      winConf = confBadge(win.confidence_pct ?? null, win.confidence_level ?? '');
    }

  // -------------------------
// ✅ Match nul (OUI/Indécis) + odd (X)
// -------------------------
let drawPick='—', drawDetail='', drawConf=confBadge(null,''), drawOdd=null;

if(draw && draw.ok){
  const dp = String(draw.prediction || '').toUpperCase().trim();

  // On considère "nul probable" si OUI / X / DRAW / NUL
  const isDraw = (dp === 'OUI' || dp === 'YES' || dp === 'X' || dp === 'DRAW' || dp === 'NUL');

  // ✅ AFFICHAGE : OUI au lieu de X
  drawPick = isDraw ? ynChip('OUI') : ynChip('Indécis');

  const v = draw.vals || {};
  drawDetail = `xG ${esc(v.home_xg ?? '—')} / ${esc(v.away_xg ?? '—')} · |diff| ${esc(v.abs_diff_xg ?? '—')} · score ${esc(v.exact_score ?? '—')}`;
  drawConf = confBadge(draw.confidence_pct, draw.confidence_level);

  // ✅ COTE : on garde bien le marché 1X2 => "X"
  drawOdd = pickOdd(m1x2, ['X','Draw','Nul']);
} else if(draw && draw.ok===false){
  drawPick = '<span class="tpm-na">⛔ Indispo</span>';
  drawDetail = '<span class="muted">xG non disponible</span>';
}

    // -------------------------
    // ✅ Nombre de buts + odd
    // -------------------------
    let goalsPick  = '<span class="muted">Indécis</span>';
    let goalsDetail= '<span class="muted">Conditions non réunies</span>';
    let goalsConf  = confBadge(null,'');
    let goalsOdd   = null;

    if(gt && gt.ok){
      goalsPick   = `<b>${esc(gt.label || gt.pick || gt.prediction || '—')}</b>`;
      goalsDetail = `xG: <b>${esc(gt.xg_total ?? '—')}</b> (${esc(gt.xg_home ?? '—')} + ${esc(gt.xg_away ?? '—')})`;
      goalsConf   = confBadge(gt.confidence_pct, gt.confidence_level);

      const raw = `${gt.label||''} ${gt.prediction||''} ${gt.pick||''}`
        .toLowerCase()
        .replace(',', '.');

      // EN: over/under 2.5
      let m = raw.match(/\b(over|under)\s*([0-9]+(?:\.[0-9]+)?)\b/);
      // FR: plus/moins de 2.5
      if(!m) m = raw.match(/\b(plus|moins)\s*(?:de)?\s*([0-9]+(?:\.[0-9]+)?)\b/);

      if(m){
        const ou   = (m[1] === 'over' || m[1] === 'plus') ? 'over' : 'under';
        const line = m[2];

        goalsOdd = pickTotalOdd(mtot, ou, line);

        // fallback variantes
        if(!goalsOdd){
          goalsOdd = pickOdd(mtot, [
            `Over ${line}`, `Under ${line}`,
            `Over ${line} Goals`, `Under ${line} Goals`,
            `O${line}`, `U${line}`, `o${line}`, `u${line}`,
          ]);
        }
      } else {
        goalsOdd = pickOdd(mtot, [gt.label, gt.prediction, gt.pick]);
      }
    } else if(gt && gt.ok === false){
      goalsPick   = '<span class="tpm-na">⛔ Non éligible</span>';
      goalsDetail = '<span class="muted">Seuil/conditions non atteints</span>';
      goalsConf   = confBadge(gt.confidence_pct ?? null, gt.confidence_level ?? '');
    }

    // -------------------------
    // ✅ BTTS + odd
    // -------------------------
    let bttsPick = '<span class="muted">Indécis</span>',
        bttsDetail = '<span class="muted">Conditions non réunies</span>',
        bttsConf = confBadge(null,''),
        bttsOdd = null;

    if(btts && btts.ok){
      const p = String(btts.prediction || '').toUpperCase();
      bttsPick = ynChip(p);
      bttsConf = confBadge(btts.confidence_pct, btts.confidence_level);
      const v = btts.vals || {};
      bttsDetail = `xG <b>${esc(v.home_xg ?? '—')}</b> / <b>${esc(v.away_xg ?? '—')}</b>`;

      if(p === 'OUI') bttsOdd = pickOdd(mbtts, ['Yes','OUI','Oui']);
      if(p === 'NON') bttsOdd = pickOdd(mbtts, ['No','NON','Non']);
    } else if(btts && String(btts.prediction||'').toLowerCase().includes('ind')){
      const v = btts.vals || {};
      bttsPick = '<span class="muted">Indécis</span>';
      bttsDetail = `xG <b>${esc(v.home_xg ?? '—')}</b> / <b>${esc(v.away_xg ?? '—')}</b>`;
    }

    // -------------------------
    // ✅ Team to score (utilise ta fonction ttsSide déjà définie)
    // -------------------------
    const ttsHome = ttsSide(tts?.home || null, hName, aName);
    const ttsAway = ttsSide(tts?.away || null, aName, hName);

    const ttsHomePred = String(tts?.home?.prediction || '').toUpperCase();
    const ttsAwayPred = String(tts?.away?.prediction || '').toUpperCase();

    const ttsOddHome = (ttsHomePred === 'OUI')
      ? (pickTotalOdd(mTotHome, 'over', 0.5) || pickOdd(mTotHome, ['Over 0.5','Over 0,5','O0.5','O0,5','o0.5','o0,5']))
      : null;

    const ttsOddAway = (ttsAwayPred === 'OUI')
      ? (pickTotalOdd(mTotAway, 'over', 0.5) || pickOdd(mTotAway, ['Over 0.5','Over 0,5','O0.5','O0,5','o0.5','o0,5']))
      : null;
// ✅ TEAM TO SCORE — si au moins 1 "OUI", on n'affiche que les "OUI"
const ttsItemsAll = [
  {
    side: 'home',
    name: hName,
    pred: String(tts?.home?.prediction || '').toUpperCase().trim(),
    ui: ttsHome,
    odd: ttsOddHome,
    lvl: tts?.home?.confidence_level || ''
  },
  {
    side: 'away',
    name: aName,
    pred: String(tts?.away?.prediction || '').toUpperCase().trim(),
    ui: ttsAway,
    odd: ttsOddAway,
    lvl: tts?.away?.confidence_level || ''
  }
];

const ttsYes = ttsItemsAll.filter(x => x.pred === 'OUI' || x.pred === 'YES');
const ttsShown = (ttsYes.length > 0) ? ttsYes : ttsItemsAll; // si aucun OUI => on garde les 2
const ttsCols = Math.max(1, ttsShown.length);

const ttsValueHTML = `
  <div class="tpm-tts-grid" style="--tts-cols:${ttsCols}">
    ${ttsShown.map(x => `
      <div class="tpm-tts-item" title="${x.ui.detail ? esc(plain(x.ui.detail)) : ''}">
        <div class="tpm-tts-head">
          <div class="tpm-tts-name">${esc(x.name)}</div>
          <div class="tpm-tts-pick">${x.ui.pick}</div>
        </div>
      </div>
    `).join('')}
  </div>
`;

const ttsOddsHTML = `
  <div class="tpm-tts-grid" style="--tts-cols:${ttsCols}">
    ${ttsShown.map(x => `
      <div class="tpm-tts-item tpm-tts-confitem">${oddPill(x.odd)}</div>
    `).join('')}
  </div>
`;

const ttsConfHTML = `
  <div class="tpm-tts-grid" style="--tts-cols:${ttsCols}">
    ${ttsShown.map(x => `
      <div class="tpm-tts-item tpm-tts-confitem">
        ${x.ui.conf}
        ${x.lvl ? `<div style="margin-top:6px"><small class="muted">${esc(x.lvl)}</small></div>` : ''}
      </div>
    `).join('')}
  </div>
`;

    // -------------------------
    // ✅ Score exact + odd
    // -------------------------
    let esPick = '<span class="muted">Indispo</span>', esDetail = '', esConf = confBadge(null,''), esOdd=null;
    if(es && es.ok){
      const label = es.label || (String(es.home_score)+' - '+String(es.away_score));
      esPick = `<b>${esc(label)}</b>`;
      esDetail = `Arrondi xG : ${esc(es.home_xg ?? '—')} / ${esc(es.away_xg ?? '—')}`;
      esConf = confBadge(es.confidence_pct ?? null, es.confidence_level ?? '');

      const key = String(label).replace(/\s+/g,''); // "1-0"
      esOdd = pickOdd(mcs, [key, key.replace('-',':')]);
    }

    // -------------------------
    // ✅ Score multichance (Top3)
    // -------------------------
    let msPick='—', msDetail='', msConf=confBadge(null,'');
    if(msc && msc.ok){
      msPick   = `<b>${esc(msc.label || '—')}</b>`;
      const v = msc.vals || {};
      msDetail = `Base ${esc(v.exact_score ?? '—')} · xG ${esc(v.home_xg ?? '—')} / ${esc(v.away_xg ?? '—')}`;
      msConf   = confBadge(msc.confidence_pct, msc.confidence_level);
    } else if(msc && msc.ok===false){
      msPick='<span class="tpm-na">⛔ Indispo</span>';
      msDetail='<span class="muted">xG non disponible</span>';
    }

    // -------------------------
    // ✅ Combinés => cote combinée (si 2 cotes dispo)
    // -------------------------
    const comboOdd = (nOdd(dcOdd) && nOdd(goalsOdd)) ? (nOdd(dcOdd) * nOdd(goalsOdd)) : null;
    const funOdd   = (nOdd(dcOdd) && nOdd(bttsOdd))  ? (nOdd(dcOdd) * nOdd(bttsOdd))  : ((nOdd(winOdd) && nOdd(goalsOdd)) ? (nOdd(winOdd) * nOdd(goalsOdd)) : null);

    // combiné SAFE (DC + Goals)
    let comboPick = '—', comboDetail = '', comboConf = confBadge(null,''), comboLevel = '';
    if(dc && dc.ok && gt && gt.ok){
      const p = String(dc.prediction ?? '').toUpperCase().replace(/\s+/g,'');
      const team = (p === '1X') ? `${hName} ou nul` : (p === 'X2') ? `${aName} ou nul` : (dc.prediction || '—');
      const goalsLbl = gt.label || gt.prediction || gt.pick || '—';
      comboPick = `<b>${esc(team)}</b> + <b>${esc(goalsLbl)}</b>`;

      const c1 = Number(dc.confidence_pct ?? NaN);
      const c2 = Number(gt.confidence_pct ?? NaN);
      if(!isNaN(c1) && !isNaN(c2)){
        let pct = Math.round(((c1 + c2) / 2) - 3);
        pct = Math.max(0, Math.min(99, pct));
        comboLevel = pct >= 80 ? 'Forte' : (pct >= 61 ? 'Moyenne' : 'Faible');
        comboConf = confBadge(pct, comboLevel);
        comboDetail = `(${Math.round(c1)}% + ${Math.round(c2)}%) / 2 - 3`;
      } else comboDetail = `<span class="muted">Confiance indisponible</span>`;
    } else {
      comboPick = '<span class="tpm-na">⛔ Non éligible</span>';
      comboDetail = 'Il faut Double chance OK + Nombre de buts OK';
    }

    // combiné FUN (DC + BTTS OUI) sinon Win + Goals
    const funPenalty = 5;
    let funPick = '—', funDetail = '', funConf = confBadge(null,''), funLevel = '';
    const clampPct = x => Math.max(0, Math.min(99, Math.round(x)));
    const lvlFromPct = p => p >= 80 ? 'Forte' : (p >= 61 ? 'Moyenne' : 'Faible');
    const bttsPred = String(btts?.prediction || '').toUpperCase();

    if(dc && dc.ok && btts && btts.ok && bttsPred === 'OUI'){
      const p = String(dc.prediction ?? '').toUpperCase().replace(/\s+/g,'');
      const dcLabel = (p === '1X') ? `${hName} ou nul` : (p === 'X2') ? `${aName} ou nul` : (dc.prediction || '—');
      funPick = `<b>${esc(dcLabel)}</b> + <b>BTTS OUI</b>`;

      const c1 = Number(dc.confidence_pct ?? NaN);
      const c2 = Number(btts.confidence_pct ?? NaN);
      if(!isNaN(c1) && !isNaN(c2)){
        const pct = clampPct(((c1 + c2) / 2) - funPenalty);
        funLevel = lvlFromPct(pct);
        funConf  = confBadge(pct, funLevel);
        funDetail = `(${Math.round(c1)}% + ${Math.round(c2)}%) / 2 - ${funPenalty}`;
      } else funDetail = `<span class="muted">Confiance indisponible</span>`;
    } else if(win && win.ok && gt && gt.ok){
      const wp = String(win.prediction ?? '').trim().toLowerCase();
      const team = (wp === '1' || wp === 'home' || wp === 'h' || wp === 'dom' || wp === 'domicile') ? hName : aName;
      const goalsLbl = gt.label || gt.prediction || gt.pick || '—';
      funPick = `<b>${esc(team)}</b> + <b>${esc(goalsLbl)}</b>`;

      const c1 = Number(win.confidence_pct ?? NaN);
      const c2 = Number(gt.confidence_pct ?? NaN);
      if(!isNaN(c1) && !isNaN(c2)){
        const pct = clampPct(((c1 + c2) / 2) - funPenalty);
        funLevel = lvlFromPct(pct);
        funConf  = confBadge(pct, funLevel);
        funDetail = `Fallback · (${Math.round(c1)}% + ${Math.round(c2)}%) / 2 - ${funPenalty}`;
      } else funDetail = `Fallback · <span class="muted">Confiance indisponible</span>`;
    } else {
      funPick = '<span class="tpm-na">⛔ Non éligible</span>';
      funDetail = 'DC + BTTS OUI non dispo, et Win + Goals non dispo';
    }

const bmName = oddsPack?.bookmaker?.name || '';
const bmUpd  = oddsPack?.updated || '';
const bmLine = bmName ? `Cotes: <b>${esc(bmName)}</b>${bmUpd ? ' · maj ' + esc(fmtDate(bmUpd)) : ''}` : '';

    return `
    <div class="tpm-stats tpm-preds" data-fx="${esc(fx)}">
      <div class="tpm-stats-head">
<div class="tpm-stats-title">
  <div>PRÉDICTIONS <span class="tpm-badge">UI v10</span></div>
  <div class="tpm-meta-mini">${bmLine || ''}</div>
</div>
      </div>

      <table>
        <thead>
          <tr>
            <th style="width:280px;text-align:left">Marché / Indicateur</th>
            <th style="text-align:left">Valeur</th>
            <th style="text-align:left">Cote</th>
            <th style="text-align:left">Confiance</th>
          </tr>
        </thead>

        <tbody>
          <tr>
            <th>Buts potentiels</th>
            <td colspan="3">
              <div class="tpm-xg-grid">
                <div class="tpm-xg-item">
                  <div class="tpm-xg-top">
                    <div class="tpm-xg-label">Total</div>
                    <div class="tpm-xg-pills">${pill(xgTot, xgTot==='—' ? 'tpm-gray' : '')}</div>
                  </div>
                </div>
                <div class="tpm-xg-item">
                  <div class="tpm-xg-top">
                    <div class="tpm-xg-label">${esc(hName)} </div>
                    <div class="tpm-xg-pills">${pill(xgH, xgH==='—' ? 'tpm-gray' : '')}</div>
                  </div>
                </div>
                <div class="tpm-xg-item">
                  <div class="tpm-xg-top">
                    <div class="tpm-xg-label">${esc(aName)} </div>
                    <div class="tpm-xg-pills">${pill(xgA, xgA==='—' ? 'tpm-gray' : '')}</div>
                  </div>
                </div>
              </div>
            </td>
          </tr>

          <tr>
            <th>Double chance 5% BK</th>
            <td title="${dcDetail ? esc(plain(dcDetail)) : ''}">${dcPick}</td>
            <td>${oddPill(dcOdd)}</td>
            <td>${dcConf}${dc?.confidence_level ? `<div style="margin-top:6px"><small class="muted">${esc(dc.confidence_level)}</small></div>` : ''}</td>
          </tr>

          <tr>
            <th>Victoire 1.5% BK</th>
            <td title="${winDetail ? esc(plain(winDetail)) : ''}">${winPick}</td>
            <td>${oddPill(winOdd)}</td>
            <td>${winConf}${win?.confidence_level ? `<div style="margin-top:6px"><small class="muted">${esc(win.confidence_level)}</small></div>` : ''}</td>
          </tr>

          <tr>
            <th>Match nul 1% BK</th>
            <td title="${drawDetail ? esc(plain(drawDetail)) : ''}">${drawPick}</td>
            <td>${oddPill(drawOdd)}</td>
            <td>${drawConf}${draw?.confidence_level ? `<div style="margin-top:6px"><small class="muted">${esc(draw.confidence_level)}</small></div>` : ''}</td>
          </tr>

          <tr>
            <th>Nombre de buts 5% BK</th>
            <td title="${goalsDetail ? esc(plain(goalsDetail)) : ''}">${goalsPick}</td>
            <td>${oddPill(goalsOdd)}</td>
            <td>${goalsConf}${gt?.confidence_level ? `<div style="margin-top:6px"><small class="muted">${esc(gt.confidence_level)}</small></div>` : ''}</td>
          </tr>

         <tr>
  <th>Les 2 équipes marquent 3% BK</th>
  <td title="${bttsDetail ? esc(plain(bttsDetail)) : ''}">${bttsPick}</td>
  <td>${oddPill(bttsOdd)}</td>
  <td>${bttsConf}${btts && btts.ok ? `<div style="margin-top:6px"><small class="muted">${esc(btts?.confidence_level ?? '')}</small></div>` : ''}</td>
</tr>

<tr>
  <th>Équipe marque ?</th>
  <td>
    ${(() => {
      const all = [
        {
          name: hName,
          pred: String(tts?.home?.prediction || '').toUpperCase().trim(),
          ui: ttsHome,
        },
        {
          name: aName,
          pred: String(tts?.away?.prediction || '').toUpperCase().trim(),
          ui: ttsAway,
        }
      ];
      const yes = all.filter(x => x.pred === 'OUI' || x.pred === 'YES');
      const shown = (yes.length > 0) ? yes : all;
      const cols = Math.max(1, shown.length);

      return `
        <div class="tpm-tts-grid" style="--tts-cols:${cols}">
          ${shown.map(x => `
            <div class="tpm-tts-item" title="${x.ui.detail ? esc(plain(x.ui.detail)) : ''}">
              <div class="tpm-tts-head">
                <div class="tpm-tts-name">${esc(x.name)}</div>
                <div class="tpm-tts-pick">${x.ui.pick}</div>
              </div>
            </div>
          `).join('')}
        </div>
      `;
    })()}
  </td>

  <td>
    ${(() => {
      const all = [
        {
          pred: String(tts?.home?.prediction || '').toUpperCase().trim(),
          odd: ttsOddHome,
        },
        {
          pred: String(tts?.away?.prediction || '').toUpperCase().trim(),
          odd: ttsOddAway,
        }
      ];
      const yes = all.filter(x => x.pred === 'OUI' || x.pred === 'YES');
      const shown = (yes.length > 0) ? yes : all;
      const cols = Math.max(1, shown.length);

      return `
        <div class="tpm-tts-grid" style="--tts-cols:${cols}">
          ${shown.map(x => `
            <div class="tpm-tts-item tpm-tts-confitem">${oddPill(x.odd)}</div>
          `).join('')}
        </div>
      `;
    })()}
  </td>

  <td>
    ${(() => {
      const all = [
        {
          pred: String(tts?.home?.prediction || '').toUpperCase().trim(),
          conf: ttsHome.conf,
          lvl:  tts?.home?.confidence_level || '',
        },
        {
          pred: String(tts?.away?.prediction || '').toUpperCase().trim(),
          conf: ttsAway.conf,
          lvl:  tts?.away?.confidence_level || '',
        }
      ];
      const yes = all.filter(x => x.pred === 'OUI' || x.pred === 'YES');
      const shown = (yes.length > 0) ? yes : all;
      const cols = Math.max(1, shown.length);

      return `
        <div class="tpm-tts-grid" style="--tts-cols:${cols}">
          ${shown.map(x => `
            <div class="tpm-tts-item tpm-tts-confitem">
              ${x.conf}
              ${x.lvl ? `<div style="margin-top:6px"><small class="muted">${esc(x.lvl)}</small></div>` : ''}
            </div>
          `).join('')}
        </div>
      `;
    })()}
  </td>
</tr>


       <tr>
  <th>Combiné SAFE 3% BK</th>
  <td title="${comboDetail ? esc(plain(comboDetail)) : ''}">${comboPick}</td>
  <td>${comboOdd ? oddPill(comboOdd) : pill('—','tpm-gray')}</td>
  <td>${comboConf}${comboLevel ? `<div style="margin-top:6px"><small class="muted">${esc(comboLevel)}</small></div>` : ''}</td>
</tr>

<tr>
  <th>Combiné FUN 1% BK</th>
  <td title="${funDetail ? esc(plain(funDetail)) : ''}">${funPick}</td>
  <td>${funOdd ? oddPill(funOdd) : pill('—','tpm-gray')}</td>
  <td>${funConf}${funLevel ? `<div style="margin-top:6px"><small class="muted">${esc(funLevel)}</small></div>` : ''}</td>
</tr>

          <tr>
            <th>Score exact 0.1% BK</th>
            <td title="${esDetail ? esc(plain(esDetail)) : ''}">${esPick}</td>
            <td>${oddPill(esOdd)}</td>
            <td>${esConf}</td>
          </tr>

${showMSC ? `
<tr>
  <th>Score multichance</th>
  <td title="${msDetail ? esc(plain(msDetail)) : ''}">${msPick}</td>
  <td>${pill('—','tpm-gray')}</td>
  <td>${msConf}${msc?.confidence_level ? `<div style="margin-top:6px"><small class="muted">${esc(msc.confidence_level)}</small></div>` : ''}</td>
</tr>
` : ``}
        </tbody>
      </table>
    </div>`;
  };
}

  function pctBadge(p){
    const n = Number(p||0);
    const cls = n>=70 ? 'tpm-green' : (n<=35 ? 'tpm-red' : '');
    return `<span class="tpm-pill2 ${cls}">${esc(n)}%</span>`;
  }
  function gmRow(label, key, gm){
    const h = gm?.home?.[key] ?? 0;
    const a = gm?.away?.[key] ?? 0;
    return `<tr><th>${esc(label)}</th><td>${pctBadge(h)}</td><td>${pctBadge(a)}</td></tr>`;
  }

  /* ============================================================
     ✅ Goal Matrix en ACCORDION (FERMÉ PAR DÉFAUT)
     ============================================================ */
  function goalsMatrixAccordion(d){
    const gm = d?.goals_matrix;
    const ms = d?.match_stats || {};
    const hName = ms?.home?.name || 'Domicile';
    const aName = ms?.away?.name || 'Extérieur';
    const fx = d?.meta?.fixture_id ?? 'x';

    if(!gm || !gm.home || !gm.away){
      return `<div class="tpm-empty">Goal Matrix indisponible.</div>`;
    }

    const playedH = Number(gm?.home?.played ?? 0);
    const playedA = Number(gm?.away?.played ?? 0);
    const req = gm?.n_requested ?? '';

    if(playedH === 0 || playedA === 0){
      return `<div class="tpm-empty">Goal Matrix : 0 match terminé trouvé (home=${esc(playedH)}, away=${esc(playedA)}).</div>`;
    }

    const accId = `gm-${fx}`;

    return `
      <div class="tpm-stats tpm-acc" data-acc="${esc(accId)}" style="margin-top:12px">
        <div class="tpm-stats-head">
          <div class="tpm-stats-title">
            <div>Buts / Match & Mi-temps</div>
            <button class="tpm-mini tpm-acc-toggle" type="button" data-acc-toggle="${esc(accId)}" aria-expanded="false">Afficher ▾</button>
          </div>
          <div style="padding:0 12px 10px;color:rgba(234,255,242,.70);font-weight:900;font-size:11px">
            N demandé: ${esc(req)} · ${esc(hName)} (${esc(playedH)}) · ${esc(aName)} (${esc(playedA)})
          </div>
        </div>

        <div class="tpm-acc-body">
          <table>
            <thead>
              <tr>
                <th style="width:280px;text-align:left">Stat</th>
                <th style="text-align:left">${esc(hName)}</th>
                <th style="text-align:left">${esc(aName)}</th>
              </tr>
            </thead>
            <tbody>
              ${gmRow('Plus de 0.5 but / match',  'over_0_5', gm)}
              ${gmRow('Plus de 1.5 buts / match', 'over_1_5', gm)}
              ${gmRow('Plus de 2.5 buts / match', 'over_2_5', gm)}
              ${gmRow('Moins de 3.5 buts / match','under_3_5', gm)}
              ${gmRow('Moins de 4.5 buts / match','under_4_5', gm)}
              ${gmRow('Moins de 5.5 buts / match','under_5_5', gm)}
              ${gmRow('Les 2 équipes marquent',    'btts', gm)}
              ${gmRow('Plus de 1.5 buts à la mi-temps', 'ht_over_1_5', gm)}
              ${gmRow('Moins de 1.5 buts à la mi-temps','ht_under_1_5', gm)}
              ${gmRow('Marque (1ère mi-temps)',    'score_ht', gm)}
              ${gmRow('Encaisse (1ère mi-temps)',  'concede_ht', gm)}
              ${gmRow('Marque (2ème mi-temps)',    'score_2h', gm)}
              ${gmRow('Encaisse (2ème mi-temps)',  'concede_2h', gm)}
            </tbody>
          </table>
        </div>
      </div>
    `;
  }

  function cmp(a,b,higherBetter){
    const na = Number(a), nb = Number(b);
    if (isNaN(na) || isNaN(nb) || na===nb) return '';
    if (higherBetter) return na>nb ? 'tpm-green' : 'tpm-red';
    return na<nb ? 'tpm-green' : 'tpm-red';
  }

  function row(label, key, higherBetter, homeTeam, awayTeam){
    const hs = homeTeam?.stats || {}, as = awayTeam?.stats || {};
    const hSeason = hs.season?.[key] ?? 0, aSeason = as.season?.[key] ?? 0;
    const hDomExt = hs.home?.[key] ?? 0;
    const aDomExt = as.away?.[key] ?? 0;
    const hLast5  = hs.last5?.[key] ?? 0,  aLast5  = as.last5?.[key] ?? 0;
    const isPct = key.endsWith('pct');

    const hS = isPct ? (hSeason+'%') : hSeason;
    const aS = isPct ? (aSeason+'%') : aSeason;
    const hD = isPct ? (hDomExt+'%') : hDomExt;
    const aD = isPct ? (aDomExt+'%') : aDomExt;
    const hL = isPct ? (hLast5+'%') : hLast5;
    const aL = isPct ? (aLast5+'%') : aLast5;

    return `
      <tr>
        <th>${esc(label)}<span class="tpm-rowhint">Saison / Domicile-Extérieur / 5 derniers</span></th>
        <td><div class="tpm-cells">
          <span class="tpm-tag">Saison</span>${pill(hS, cmp(hSeason,aSeason,higherBetter))}
          <span class="tpm-tag">Dom/Ext</span>${pill(hD, cmp(hDomExt,aDomExt,higherBetter))}
          <span class="tpm-tag">5 derniers</span>${pill(hL, cmp(hLast5,aLast5,higherBetter))}
        </div></td>
        <td><div class="tpm-cells">
          <span class="tpm-tag">Saison</span>${pill(aS, cmp(aSeason,hSeason,higherBetter))}
          <span class="tpm-tag">Dom/Ext</span>${pill(aD, cmp(aDomExt,hDomExt,higherBetter))}
          <span class="tpm-tag">5 derniers</span>${pill(aL, cmp(aLast5,hLast5,higherBetter))}
        </div></td>
      </tr>
    `;
  }

  function statsTable(d){
    const ms = d?.match_stats || {};
    const meta = d?.meta || {};
    const h = ms.home || {}, a = ms.away || {};
    const fx = meta?.fixture_id ?? '';

    return `
      <div class="tpm-stats" data-fx="${esc(fx)}">
        <div class="tpm-stats-head">
          <div class="tpm-stats-title">
            <div>STATISTIQUES DU MATCH</div>
            <div class="tpm-meta-mini">Saison ${esc(meta?.season ?? '')} · Ligue ${esc(meta?.league_id ?? '')} · ${esc(meta?.time_used_sec ?? '')}s</div>
          </div>

       <div class="tpm-teamcol">
  <button class="tpm-teambtn" type="button" data-modal="injuries" data-side="home" data-fx="${esc(fx)}" title="Voir les blessés">
    <div class="tpm-teambox">
      ${h.logo ? `<img class="lg" src="${esc(h.logo)}" alt="">` : ''}
      <div class="stack">
        <div class="nm">${esc(h.name||'Domicile')}</div>
        <div class="sub">Clique : Blessés · Vue : Saison / Domicile / 5 derniers</div>
      </div>
    </div>
  </button>

  <button class="tpm-mini tpm-l5btn" type="button" data-modal="l5" data-side="home" data-fx="${esc(fx)}">
    L5 — 5 derniers
  </button>
</div>

            <div class="tpm-midbtns">
              <button class="tpm-mini" type="button" data-modal="standings" data-fx="${esc(fx)}">Classement</button>
              <button class="tpm-mini" type="button" data-modal="h2h" data-fx="${esc(fx)}">H2H</button>
              <button class="tpm-mini" type="button" data-modal="lineups" data-fx="${esc(fx)}">Compos</button>
            </div>

<div class="tpm-teamcol right">
  <button class="tpm-teambtn" type="button" data-modal="injuries" data-side="away" data-fx="${esc(fx)}" title="Voir les blessés">
    <div class="tpm-teambox right">
      ${a.logo ? `<img class="lg" src="${esc(a.logo)}" alt="">` : ''}
      <div class="stack">
        <div class="nm">${esc(a.name||'Extérieur')}</div>
        <div class="sub">Clique : Blessés · Vue : Saison / Extérieur / 5 derniers</div>
      </div>
    </div>
  </button>

  <button class="tpm-mini tpm-l5btn" type="button" data-modal="l5" data-side="away" data-fx="${esc(fx)}">
    L5 — 5 derniers
  </button>
</div>

        </div>

        <table><tbody>
          ${row('Points par match', 'ppm', true,  h, a)}
          ${row('Buts marqués / match', 'gfpm', true, h, a)}
          ${row('Buts encaissés / match', 'gapm', false, h, a)}
          ${row('Marque au moins 1 but', 'score1pct', true, h, a)}
          ${row('Encaisse au moins 1 but', 'concede1pct', false, h, a)}
        </tbody></table>
      </div>
    `;
  }

  async function loadDetails(btn){
    const panel = document.getElementById(btn.getAttribute('data-target'));
    if(!panel) return;

    const open = panel.style.display === 'block';
    panel.style.display = open ? 'none' : 'block';
    btn.textContent = open ? 'Ouvrir l’analyse' : 'Fermer l’analyse';
    if(open) return;

    const fxid = btn.getAttribute('data-fixture');
    if(panel.getAttribute('data-loaded') === '1' && store.has(fxid)) return;

    panel.innerHTML = '<div class="tpm-loading">Chargement des stats…</div>';

    const fd = new FormData();
    fd.append('action', 'tp_match_analyzer_details');
    fd.append('nonce', nonce);
    fd.append('fixture_id', fxid);
    fd.append('league_id', btn.getAttribute('data-league'));
    fd.append('home_id', btn.getAttribute('data-home'));
    fd.append('away_id', btn.getAttribute('data-away'));
    fd.append('home_name', btn.getAttribute('data-home-name')||'');
    fd.append('away_name', btn.getAttribute('data-away-name')||'');
    fd.append('home_logo', btn.getAttribute('data-home-logo')||'');
    fd.append('away_logo', btn.getAttribute('data-away-logo')||'');

    try{
      const res = await fetch(ajaxUrl, { method:'POST', body: fd, credentials:'same-origin' });
      const json = await res.json();
      if(!json || !json.success){
        panel.innerHTML = '<div class="tpm-empty">Erreur : '+esc(json?.data?.message ?? 'inconnue')+'</div>';
        return;
      }

      const d = json.data || {};
      store.set(String(d?.meta?.fixture_id ?? fxid), d);
		
		// ✅ FEU
updateFireBadges(fxid, d);

      // ✅ PRÉDICTIONS (inclut Victoire sec) + STATS + Goal Matrix (fermé)
      panel.innerHTML = predictionsUnifiedTable(d) + statsTable(d) + goalsMatrixAccordion(d);
      panel.setAttribute('data-loaded','1');
    } catch(e){
      panel.innerHTML = '<div class="tpm-empty">Erreur réseau : '+esc(e?.message ?? e)+'</div>';
    }
  }

  function standingsModal(d){
    const st = d?.standings;
    const ms = d?.match_stats || {};
    const hName = ms?.home?.name || '', aName = ms?.away?.name || '';

    if(!Array.isArray(st) || !st.length){
      return '<div class="muted">Classement non disponible.</div>';
    }

    const rows = st.slice(0, 20).map(r=>{
      const rk = r.rank ?? '';
      const tn = r.team?.name ?? '';
      const tl = r.team?.logo ?? '';
      const pts = r.points ?? '';
      const pl = r.all?.played ?? '';
      const gd = r.goalsDiff ?? r.goals?.diff ?? '';
      const w = r.all?.win ?? '';
      const dr = r.all?.draw ?? '';
      const l = r.all?.lose ?? '';

      const isH = tn && hName && tn.toLowerCase() === hName.toLowerCase();
      const isA = tn && aName && tn.toLowerCase() === aName.toLowerCase();
      const badge = isH ? ' <span class="tpm-badge">🏠 Domicile</span>' : (isA ? ' <span class="tpm-badge">✈️ Extérieur</span>' : '');

      return `
        <tr>
          <td class="tpm-rank">${esc(rk)}</td>
          <td>
            <div class="tpm-teamcell">
              ${tl ? `<img src="${esc(tl)}" alt="">` : ''}
              <span title="${esc(tn)}">${esc(tn)}</span>${badge}
            </div>
          </td>
          <td><b>${esc(pts)}</b></td>
          <td>${esc(pl)}</td>
          <td>${esc(gd)}</td>
          <td>${esc(w)}-${esc(dr)}-${esc(l)}</td>
        </tr>
      `;
    }).join('');

    return `
      <div class="tpm-kpis">
        <span class="tpm-badge">Top 20</span>
        <span class="tpm-badge">Saison ${esc(d?.meta?.season ?? '')}</span>
        <span class="tpm-badge">Ligue ${esc(d?.meta?.league_id ?? '')}</span>
      </div>
      <table class="tpm-mtbl">
        <thead><tr><th>#</th><th>Équipe</th><th>Pts</th><th>J</th><th>Diff</th><th>W-D-L</th></tr></thead>
        <tbody>${rows}</tbody>
      </table>
    `;
  }

  function h2hModal(d){
    const list = d?.h2h;
    const ms = d?.match_stats || {};
    const hName = ms?.home?.name || 'Domicile';
    const aName = ms?.away?.name || 'Extérieur';
    const hid = ms?.home?.id ?? 0;
    const aid = ms?.away?.id ?? 0;

    if(!Array.isArray(list) || !list.length){
      return '<div class="muted">Aucune confrontation renvoyée (saisons filtrées).</div>';
    }

    let hw=0, dr=0, aw=0;
    const items = list.slice(0, 12).map(fx=>{
      const hs = fx?.goals?.home, as = fx?.goals?.away;
      const homeId = fx?.teams?.home?.id ?? 0;
      const awayId = fx?.teams?.away?.id ?? 0;

      if (hs !== null && as !== null && hs !== undefined && as !== undefined) {
        if (hs === as) dr++;
        else {
          const homeWon = (fx?.teams?.home?.winner === true);
          const awayWon = (fx?.teams?.away?.winner === true);
          if ((homeWon && homeId===hid) || (awayWon && awayId===hid)) hw++;
          else if ((homeWon && homeId===aid) || (awayWon && awayId===aid)) aw++;
        }
      }

      const dateIso = fx?.fixture?.date ?? '';
      const league = fx?.league?.name ?? '';
      const homeN = fx?.teams?.home?.name ?? 'Home';
      const awayN = fx?.teams?.away?.name ?? 'Away';
      const score = (hs !== null && as !== null && hs !== undefined && as !== undefined) ? `${hs} - ${as}` : '—';

      return `
        <div class="tpm-h2h-item">
          <div>
            <div class="tpm-h2h-date">${esc(fmtDate(dateIso))}</div>
            <div class="muted">${esc(league)}</div>
          </div>
          <div class="tpm-h2h-match" title="${esc(homeN+' vs '+awayN)}">${esc(homeN)} <span class="muted">vs</span> ${esc(awayN)}</div>
          <div class="tpm-h2h-score">${esc(score)}</div>
        </div>
      `;
    }).join('');

    return `
      <div class="tpm-kpis">
        <span class="tpm-badge">🏠 ${esc(hName)} : ${hw}V</span>
        <span class="tpm-badge">🤝 ${dr}N</span>
        <span class="tpm-badge">✈️ ${esc(aName)} : ${aw}V</span>
      </div>
      ${items}
    `;
  }

  function pickInjTeamId(inj){
    return inj?.team?.id ?? inj?.teamId ?? inj?.team_id ?? inj?.team?.team_id ?? 0;
  }

  function injuriesModal(d, teamSide){
    const ms = d?.match_stats || {};
    const team = teamSide === 'away' ? ms.away : ms.home;
    const tid = team?.id ?? 0;
    const tname = team?.name ?? (teamSide==='away'?'Extérieur':'Domicile');
    const tlogo = team?.logo ?? '';

    const inj = Array.isArray(d?.injuries) ? d.injuries : [];
    const mine = inj.filter(x => Number(pickInjTeamId(x)) === Number(tid));

    if(!mine.length){
      return `<div class="muted">Aucun blessé/absent signalé pour ${esc(tname)}.</div>`;
    }

    const items = mine.map(x=>{
      const p = x?.player || x?.players || {};
      const name = p?.name ?? x?.name ?? 'Joueur';
      const photo = p?.photo ?? p?.image ?? '';
      const type = x?.type ?? x?.reason ?? x?.injury ?? p?.type ?? 'Indispo';
      const reason = x?.reason ?? x?.detail ?? x?.description ?? '';
      const date = x?.fixture?.date ?? x?.date ?? '';
      const susp = x?.suspension_active ? (x?.suspension?.type ? ` · <b>${esc(x.suspension.type)}</b>` : ' · <b>Suspension</b>') : '';

      return `
        <div class="tpm-inj">
          ${photo ? `<img src="${esc(photo)}" alt="">` : `<img src="data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="rgba(255,255,255,0.06)"/><text x="50%" y="54%" text-anchor="middle" fill="rgba(255,255,255,0.55)" font-family="Arial" font-size="14">?</text></svg>')}" alt="">`}
          <div>
            <div class="nm">${esc(name)}</div>
            <div class="sub">${esc(type)}${reason ? ' · '+esc(reason) : ''}${susp}${date ? ' · '+esc(fmtDate(date)) : ''}</div>
          </div>
        </div>
      `;
    }).join('');

    return `
      <div class="tpm-kpis">
        ${tlogo ? `<span class="tpm-badge"><img src="${esc(tlogo)}" style="width:16px;height:16px;object-fit:contain"> ${esc(tname)}</span>` : `<span class="tpm-badge">${esc(tname)}</span>`}
        <span class="tpm-badge">${mine.length} joueur(s)</span>
      </div>
      <div class="tpm-inj-list">${items}</div>
    `;
  }

  function lineupsModal(d){
    const ms = d?.match_stats || {};
    const list = Array.isArray(d?.lineups) ? d.lineups : [];

    if(!list.length){
      return '<div class="muted">Compositions non disponibles (0).</div>';
    }

    function teamBox(t){
      const teamName = t?.team?.name ?? 'Équipe';
      const teamLogo = t?.team?.logo ?? '';
      const form = t?.formation ?? '';
      const coach = t?.coach?.name ?? '';

      const startXI = (t?.startXI || []).map(x => x?.player?.name).filter(Boolean);
      const subs = (t?.substitutes || []).map(x => x?.player?.name).filter(Boolean);

      const s1 = startXI.length ? `<ul class="tpm-lul">${startXI.map(n=>`<li>${esc(n)}</li>`).join('')}</ul>` : `<div class="muted">Non disponible</div>`;
      const s2 = subs.length ? `<ul class="tpm-lul">${subs.map(n=>`<li>${esc(n)}</li>`).join('')}</ul>` : `<div class="muted">Non disponible</div>`;

      return `
        <div class="tpm-lbox">
          <div class="h">
            <div style="display:flex;align-items:center;gap:10px;min-width:0">
              ${teamLogo ? `<img src="${esc(teamLogo)}" style="width:22px;height:22px;object-fit:contain" alt="">` : ''}
              <span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(teamName)}</span>
            </div>
            <div class="muted">${esc(form)}</div>
          </div>
          <div class="b">
            ${coach ? `<div class="muted">Coach : <b style="color:#e5e7eb">${esc(coach)}</b></div>` : ''}
            <div class="tpm-lsec">
              <div class="t">Titularisés</div>
              ${s1}
            </div>
            <div class="tpm-lsec">
              <div class="t">Remplaçants</div>
              ${s2}
            </div>
          </div>
        </div>
      `;
    }

    const hid = ms?.home?.id ?? 0;
    const aid = ms?.away?.id ?? 0;
    const homeObj = list.find(x => (x?.team?.id ?? 0) === hid) || list[0];
    const awayObj = list.find(x => (x?.team?.id ?? 0) === aid) || list[1];

    const boxes = [homeObj, awayObj].filter(Boolean).map(teamBox).join('');
    return `<div class="tpm-lineups">${boxes}</div>`;
  }
function resChip(r){
  if(r === 'W') return `<span class="tpm-pill2 tpm-green">V</span>`;
  if(r === 'D') return `<span class="tpm-pill2 tpm-gray">N</span>`;
  if(r === 'L') return `<span class="tpm-pill2 tpm-red">D</span>`;
  return `<span class="tpm-pill2 tpm-gray">—</span>`;
}

function l5Modal(out, teamId){
  const list = Array.isArray(out?.fixtures) ? out.fixtures : [];
  if(!list.length){
    return `<div class="muted">Aucun match renvoyé.</div>`;
  }

  let w=0,d=0,l=0;

  const items = list.map(fx=>{
    const h = fx?.teams?.home || {};
    const a = fx?.teams?.away || {};
    const gh = fx?.goals?.home, ga = fx?.goals?.away;

    const score = (gh!==null && gh!==undefined && ga!==null && ga!==undefined) ? `${gh} - ${ga}` : '—';
    const date = fx?.date ? fmtDate(fx.date) : '';
    const lg   = fx?.league?.name || '';
    const rd   = fx?.league?.round || '';

    // result relatif à teamId
    let r = '';
    const isHome = Number(h.id) === Number(teamId);
    const isAway = Number(a.id) === Number(teamId);
    if(score !== '—' && (isHome || isAway)){
      if(Number(gh) === Number(ga)) r='D';
      else {
        const teamWon = isHome ? (Number(gh) > Number(ga)) : (Number(ga) > Number(gh));
        r = teamWon ? 'W' : 'L';
      }
    }
    if(r==='W') w++; else if(r==='D') d++; else if(r==='L') l++;

    return `
      <div class="tpm-h2h-item">
        <div>
          <div class="tpm-h2h-date">${esc(date)}</div>
          <div class="muted">${esc(lg)}${rd ? ' · '+esc(rd) : ''}</div>
        </div>
        <div class="tpm-h2h-match" title="${esc((h.name||'')+' vs '+(a.name||''))}">
          ${esc(h.name||'Home')} <span class="muted">vs</span> ${esc(a.name||'Away')}
        </div>
        <div class="tpm-h2h-score" style="display:flex;gap:8px;align-items:center;justify-content:flex-end">
          ${resChip(r)} <span>${esc(score)}</span>
        </div>
      </div>
    `;
  }).join('');

  return `
    <div class="tpm-kpis">
      <span class="tpm-badge">5 derniers</span>
      <span class="tpm-badge">Forme: ${w}V · ${d}N · ${l}D</span>
    </div>
    ${items}
  `;
}

async function fetchTeamL5(teamId, season){
  const fd = new FormData();
  fd.append('action', 'tp_match_analyzer_team_l5');
  fd.append('nonce', nonce);
  fd.append('team_id', String(teamId));
  fd.append('season', String(season));

  const res = await fetch(ajaxUrl, { method:'POST', body: fd, credentials:'same-origin' });
  const json = await res.json();
  if(!json || !json.success) throw new Error(json?.data?.message ?? 'Erreur L5');
  return json.data || {};
}

root.addEventListener('click', async (e) => {
  // 1) Ouvrir/Fermer panel match
  const btn = e.target.closest('.tpm-btn');
  if(btn){
    e.preventDefault();
    await loadDetails(btn);
    return;
  }

  // 2) Toggle accordion goal matrix
  const accBtn = e.target.closest('[data-acc-toggle]');
  if(accBtn){
    const id = accBtn.getAttribute('data-acc-toggle');
    const box = root.querySelector(`.tpm-acc[data-acc="${CSS.escape(id)}"]`) || document.querySelector(`.tpm-acc[data-acc="${CSS.escape(id)}"]`);
    if(!box) return;
    const open = box.classList.toggle('is-open');
    accBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
    accBtn.textContent = open ? 'Masquer ▴' : 'Afficher ▾';
    return;
  }

  // 3) Modals (standings / h2h / lineups / injuries / l5)
  const modalBtn = e.target.closest('[data-modal]');
  if(!modalBtn) return;

  const type = modalBtn.getAttribute('data-modal') || '';
  const fx   = modalBtn.getAttribute('data-fx') || '';
  const d    = store.get(String(fx));

  if(!d){
    openModal('Erreur', '<div class="muted">Données non chargées. Ouvre d’abord l’analyse.</div>');
    return;
  }

  // ✅ L5
  if(type === 'l5'){
    const side = modalBtn.getAttribute('data-side') || 'home';
    const team = (side === 'away') ? d?.match_stats?.away : d?.match_stats?.home;
    const teamId = team?.id || 0;
    const teamName = team?.name || (side === 'away' ? 'Extérieur' : 'Domicile');
    const season = d?.meta?.season || <?php echo (int)CORE_BASE_FOOT_SEASON; ?>;

    if(!teamId){
      openModal('L5 — ' + teamName, '<div class="muted">Team ID introuvable.</div>');
      return;
    }

    openModal('L5 — ' + teamName, '<div class="tpm-loading">Chargement…</div>');

    try{
      const out = await fetchTeamL5(teamId, season);
      modalBody.innerHTML = l5Modal(out, teamId); // ✅ on utilise TA modalBody déjà définie (#tpm-modal-body)
    }catch(err){
      modalBody.innerHTML = `<div class="muted">Erreur L5 : ${esc(err?.message || err)}</div>`;
    }
    return;
  }

  if(type === 'standings') openModal('Classement', standingsModal(d));
  else if(type === 'h2h') openModal('H2H — Confrontations', h2hModal(d));
  else if(type === 'lineups') openModal('Compositions', lineupsModal(d));
  else if(type === 'injuries'){
    const side = modalBtn.getAttribute('data-side') || 'home';
    const teamName = side === 'away'
      ? (d?.match_stats?.away?.name || 'Extérieur')
      : (d?.match_stats?.home?.name || 'Domicile');
    openModal('Blessés — ' + teamName, injuriesModal(d, side));
  }
});

})();
</script>
    <?php
    return ob_get_clean();
  }
}
add_shortcode('tp_match_analyzer', 'tp_match_analyzer_shortcode');

Plongez dans les analyses et statistiques des matchs de football !

Explorez l’univers fascinant des analyses et statistiques des matchs de football avec notre contenu exclusif. Plongez au cœur de l’action et découvrez des informations approfondies qui vous permettront de prendre des décisions éclairées pour vos paris sportifs.

Analysez chaque rencontre en détail

Décortiquez chaque rencontre avec nos analyses pointues qui examinent les forces et les faiblesses de chaque équipe, les tactiques de jeu, les statistiques individuelles des joueurs et bien plus encore. Obtenez un aperçu complet de chaque match pour affiner vos pronostics.

Explorez les tendances et les performances

Plongez dans les tendances du moment, les performances récentes des équipes, les confrontations directes passées et les évolutions tactiques pour anticiper les résultats futurs. Notre expertise vous permet de saisir les subtilités de chaque rencontre et d’identifier les opportunités de paris.

Optimisez vos paris avec nos conseils avisés

Profitez de nos conseils avisés pour optimiser vos paris. Que vous soyez un parieur débutant ou expérimenté, nos analyses détaillées vous offrent un avantage sur les bookmakers et vous aident à maximiser vos gains.

Accédez à des données exclusives

Explorez des données exclusives, des graphiques et des tableaux statistiques qui vous offrent un aperçu approfondi de chaque match. Grâce à notre expertise, prenez des décisions éclairées basées sur des informations fiables et actualisées.

Plongez dans le monde des Paris Sportifs !

Au cœur de l’excitation des paris sportifs en ligne, notre site vous offre une expérience unique. Découvrez un univers riche en possibilités où chaque match devient une opportunité de vibrer et de gagner gros !

Explorez une diversité de sports

Des terrains de football aux courts de tennis, en passant par les arènes de basketball et les greens de golf, plongez dans un univers où chaque discipline sportive offre son lot d’émotions et de paris palpitants.

Maîtrisez les cotes et les stratégies

Apprenez à décrypter les cotes, à comprendre les différents types de paris et à développer des stratégies gagnantes pour maximiser vos gains. Notre expertise vous accompagne à chaque étape de votre parcours de parieur.

Contenus exclusifs et analyses pointues

Profitez de nos analyses détaillées, de nos pronostics éclairés et de nos conseils avisés pour affiner vos choix de paris. Plongez au cœur de l’action avec des contenus exclusifs qui vous donnent un avantage sur les bookmakers.

Optimisez votre expérience de jeu en ligne

Explorez nos conseils pour optimiser votre expérience de jeu en ligne, que ce soit à travers les bonus alléchants, les paris en direct, ou les paris combinés. Faites de chaque pari une opportunité de gagner gros !

Plongez dans l’univers envoûtant des analyses et statistiques des matchs de football avec Tedam’s Prono. Explorez chaque rencontre en détail grâce à nos analyses pointues, élaborées par notre équipe d’experts passionnés. Découvrez les forces et les faiblesses de chaque équipe, les tactiques de jeu et les performances individuelles des joueurs, le tout présenté de manière claire et concise pour vous aider à prendre des décisions éclairées pour vos paris sportifs.

Grâce à l’expertise de Tedam’s Prono, plongez dans les tendances du moment, les performances récentes des équipes et les confrontations directes passées pour anticiper les résultats futurs avec précision. Nos analyses approfondies vous permettent de saisir les subtilités de chaque rencontre et d’identifier les opportunités de paris les plus prometteuses.

Optimisez vos paris en suivant les conseils avisés de Tedam’s Prono. Que vous soyez un parieur débutant ou expérimenté, nos analyses détaillées vous offrent un avantage incontestable sur les bookmakers et vous aident à maximiser vos gains. Accédez à des données exclusives, des graphiques et des tableaux statistiques qui vous offrent un aperçu complet de chaque match, le tout élaboré par l’équipe de Tedam’s Prono pour vous offrir des informations fiables et actualisées.

Plongez dans le monde passionnant des paris sportifs en ligne avec Tedam’s Prono. Explorez une diversité de sports, des terrains de football aux courts de tennis, en passant par les arènes de basketball et le gazon de Wimbledon. Chaque discipline sportive offre son lot d’émotions et de paris palpitants, et Tedam’s Prono est là pour vous guider à chaque étape de votre parcours de parieur.

Maîtrisez les cotes et les stratégies avec l’aide de Tedam’s Prono. Apprenez à décrypter les cotes, à comprendre les différents types de paris et à développer des stratégies gagnantes pour maximiser vos gains. Profitez de nos analyses détaillées, de nos pronostics éclairés et de nos conseils avisés pour affiner vos choix de paris et faire de chaque match une opportunité de gagner gros.

Optimisez votre expérience de jeu en ligne avec Tedam’s Prono. Explorez les bonus alléchants, les paris en direct et les paris combinés pour augmenter vos chances de succès et transformer chaque match en une chance de vibrer et de remporter des gains exceptionnels. Rejoignez Tedam’s Prono dès maintenant et faites de vos paris sportifs une expérience inoubliable !

Découvrez nos pronostics quotidiens remplis d’analyses approfondies et de prédictions précises pour les matchs de sport du jour. Nos experts en paris sportifs vous offrent des conseils éclairés pour vous aider à prendre les meilleures décisions de paris, accompagnés de statistiques détaillées et de tendances pour chaque match. Ne manquez pas nos pronostics quotidiens pour rester informé et augmenter vos chances de gagner gros. Rejoignez-nous dès maintenant pour une expérience de pari sportif passionnante et lucrative.

  Pronos du jour

Comment bien débuter dans les Paris Sportifs ?


  

Pourquoi avoir plusieurs Bookmaker

Comment gérer sa bankroll aux paris sportifs ?

Qu’est-ce que le ROI dans les paris sportifs ?

C’est quoi une value bet ?

Qu’est-ce qu’un pari double chance ou les 2 équipes marquent ?

Comment bien miser en paris sportifs ?

 

Statistiques du match Universitatea Cluj Dinamo Bucuresti  
Santé Saison (Points par match) 2.14 1.71  
Domicile / Extérieur (Points par match) 3 0.75  
5 derniers matchs (Points par match) 2 1.6  
Attaque Saison A (Buts par match) 1.71 2.29  
Domicile Extérieur A (Buts par match) 3 1.5  
Ratio (Buts marqués – 5 derniers matchs) 1.2 1.2  
Défense Saison B (Buts encaissés par match) 0.57 1.43  
Domicile Extérieur B (Buts encaissés par match) 0.5 1.75  
Ratio (Buts Encaissés – 5 derniers matchs) 0.2 1  
Marque au moins 1 but SAISON (%) 71.43% 100%  
Marque au moins 1 but DOM / EXT (%) 100% 100%  
Marque au moins 1 but 5 derniers (%) 60% 80%  
Encaisse au moins 1 but SAISON (%) 42.86% 100%  
Encaisse au moins 1 but DOM / EXT (%) 50% 100%  
Encaisse au moins 1 but 5 derniers (%) 20% 80%  
Tirs Cadrés (total) 30 26  
Corners (total) 28 28  
Catégorie 1 2  
Nombre de buts 1.68 01.04  
Nombre de buts Catégorie 1.85 0.94  
Prédictions Universitatea Cluj Dinamo Bucuresti  
Prédiction Nombre de buts Moins 4.5 buts  
Est-ce que l’équipe marque ? OUI NON  
Prédiction Les 2 équipes marquent ? NON  
Prédiction Safe Universitatea Cluj ou Nul  
Prédiction Combiné Safe Universitatea Cluj ou Nul et Moins 4.5 buts  
Prédiction Victoire Universitatea Cluj  
Prédiction Fun à tenter Universitatea Cluj ou Nul et Moins 4.5 buts et NON  
Prédiction Tirs cadres Plus de 6.5 Tirs cadrés  
Prédiction Corners Plus de 6.5 Corners  
Score Prédit 2 1  
Score 2 2 0  
Score 3 2 0  
Buteurs potentiels Universitatea Cluj V. Blănuță (4 buts)
D. Nistor (4 buts)
Dinamo Bucuresti A. Selmani (4 buts)
D. Politic (3 buts)

Êtes-vous satisfait des analyses ?

Cliquez sur une étoile pour la noter !

Note Moyenne 5 / 5. Vote 1

Aucun vote pour l'instant ! Soyez le premier à noter cet article.