// fp-apis.jsx — Live API Integrations + 30min Cache
// ✅ Open-Meteo   : Echtes Wetter, kostenlos, kein Key
// ✅ OpenLigaDB   : Bundesliga live, kostenlos, kein Key
// ⚡ ESPN          : Soccer Scores, inoffizielle API, kein Key
// 🔑 API-Sport.io : Alle Ligen, RapidAPI Key nötig

// ─── 30min LocalStorage Cache ─────────────────────────────────
const apiCache = {
  PREFIX: 'fai_cache_',
  TTL: 30 * 60 * 1000, // 30 minutes default

  set(key, data, ttl = this.TTL) {
    try {
      const entry = { data, expires: Date.now() + ttl };
      localStorage.setItem(this.PREFIX + key, JSON.stringify(entry));
    } catch(e) { /* localStorage full — skip caching */ }
  },

  get(key) {
    try {
      const raw = localStorage.getItem(this.PREFIX + key);
      if (!raw) return null;
      const entry = JSON.parse(raw);
      if (Date.now() > entry.expires) {
        localStorage.removeItem(this.PREFIX + key);
        return null;
      }
      return entry.data;
    } catch { return null; }
  },

  clear(prefix = '') {
    const keys = Object.keys(localStorage).filter(k => k.startsWith(this.PREFIX + prefix));
    keys.forEach(k => localStorage.removeItem(k));
    return keys.length;
  },

  stats() {
    const keys = Object.keys(localStorage).filter(k => k.startsWith(this.PREFIX));
    const now = Date.now();
    let valid = 0, expired = 0, totalBytes = 0;
    keys.forEach(k => {
      try {
        const raw = localStorage.getItem(k);
        totalBytes += raw?.length || 0;
        const entry = JSON.parse(raw);
        if (now < entry.expires) valid++; else expired++;
      } catch { expired++; }
    });
    return { keys: keys.length, valid, expired, sizeKB: Math.round(totalBytes / 1024) };
  },
};

// ─── VENUE KOORDINATEN ────────────────────────────────────────
const VENUE_COORDS = {
  m01: { city:'Frankfurt',   lat:50.0687, lon:8.6454,  venue:'Deutsche Bank Park' },
  m02: { city:'Leverkusen',  lat:51.0380, lon:7.0022,  venue:'BayArena' },
  m03: { city:'London',      lat:51.4816, lon:-0.1910, venue:'Stamford Bridge' },
  m04: { city:'München',     lat:48.2188, lon:11.6247, venue:'Allianz Arena' },
  m05: { city:'Dortmund',    lat:51.4926, lon:7.4519,  venue:'Signal Iduna Park' },
  m06: { city:'London',      lat:51.5549, lon:-0.1084, venue:'Emirates Stadium' },
  m07: { city:'Liverpool',   lat:53.4308, lon:-2.9608, venue:'Anfield' },
  m08: { city:'Madrid',      lat:40.4531, lon:-3.6883, venue:'Bernabéu' },
  m09: { city:'Barcelona',   lat:41.3809, lon:2.1228,  venue:'Estadi Olímpic' },
  m10: { city:'Milano',      lat:45.4781, lon:9.1240,  venue:'San Siro' },
  m11: { city:'Milano',      lat:45.4781, lon:9.1240,  venue:'San Siro' },
  m12: { city:'Paris',       lat:48.8414, lon:2.2530,  venue:'Parc des Princes' },
  m13: { city:'Marseille',   lat:43.2696, lon:5.3961,  venue:'Vélodrome' },
  m14: { city:'Amsterdam',   lat:52.3144, lon:4.9421,  venue:'Johan Cruyff ArenA' },
  m15: { city:'Brugge',      lat:51.1877, lon:3.2030,  venue:'Jan Breydel Stadion' },
  m16: { city:'Istanbul',    lat:41.0765, lon:28.7323, venue:'Rams Park' },
  m17: { city:'Lisboa',      lat:38.7522, lon:-9.1848, venue:'Estádio da Luz' },
  m18: { city:'Basel',       lat:47.5416, lon:7.6202,  venue:'St. Jakob-Park' },
};

// ─── WMO WEATHER CODE → App-Format ───────────────────────────
function wmoToCondition(code, windspeed, rain) {
  let label, impact, emoji;
  if (code === 0)              { label='Klar';      emoji='☀️';  impact='NEUTRAL'; }
  else if (code <= 2)          { label='Heiter';    emoji='⛅';  impact='NEUTRAL'; }
  else if (code <= 3)          { label='Bewölkt';   emoji='☁️';  impact='NEUTRAL'; }
  else if (code <= 49)         { label='Neblig';    emoji='🌫️'; impact='SLOWER_PACE'; }
  else if (code <= 67)         { label='Regen';     emoji='🌧️'; impact='LOW_SCORING'; }
  else if (code <= 77)         { label='Schnee';    emoji='❄️';  impact='CHAOTIC'; }
  else if (code <= 82)         { label='Schauer';   emoji='🌦️'; impact='LOW_SCORING'; }
  else                         { label='Gewitter';  emoji='⛈️'; impact='CHAOTIC'; }
  if (windspeed > 30)  impact = 'CHAOTIC';
  else if (windspeed > 20 && impact === 'NEUTRAL') impact = 'LOW_SCORING';
  return { label:`${label} ${emoji}`, wind:Math.round(windspeed), rain:Math.round(rain||0), impact, code };
}

// ─── 1. OPEN-METEO (kostenlos, kein Key) ─────────────────────
const openMeteoAPI = {
  cache: {},
  async getWeather(matchId) {
    const venue = VENUE_COORDS[matchId];
    if (!venue) return null;
    const cacheKey = `weather_${venue.lat}_${venue.lon}`;
    const cached = apiCache.get(cacheKey);
    if (cached) return cached;
    try {
      const url = `https://api.open-meteo.com/v1/forecast?latitude=${venue.lat}&longitude=${venue.lon}&current=temperature_2m,precipitation,windspeed_10m,weathercode,relativehumidity_2m&timezone=auto&forecast_days=1`;
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      const cur = data.current;
      const condition = wmoToCondition(cur.weathercode, cur.windspeed_10m, cur.precipitation);
      const result = {
        ...condition,
        temp: Math.round(cur.temperature_2m),
        humidity: cur.relativehumidity_2m,
        venue: venue.venue,
        city: venue.city,
        source: 'open-meteo',
        freshAt: new Date().toISOString(),
      };
      apiCache.set(cacheKey, result, 30 * 60 * 1000); // 30min
      return result;
    } catch(e) {
      console.warn('Open-Meteo:', e.message);
      return null;
    }
  },
  async getAllMatchWeather(matchIds) {
    const results = {};
    // Batch deduplicated by city (max ~8 unique cities)
    const byCity = {};
    matchIds.forEach(id => {
      const v = VENUE_COORDS[id];
      if (v) { const key = `${v.lat}_${v.lon}`; if (!byCity[key]) byCity[key] = []; byCity[key].push(id); }
    });
    await Promise.all(Object.keys(byCity).map(async key => {
      const ids = byCity[key];
      const weather = await this.getWeather(ids[0]);
      ids.forEach(id => { results[id] = weather; });
    }));
    return results;
  }
};

// ─── 2. OPENLIGADB (Bundesliga, kostenlos, kein Key) ─────────
const openLigaDB = {
  BASE: 'https://api.openligadb.de',
  cache: {},
  async getCurrentMatchday() {
    const cacheKey = 'oldb_matchday';
    const cached = apiCache.get(cacheKey);
    if (cached) return cached;
    try {
      const res = await fetch(`${this.BASE}/getcurrentgroup/bl1`);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      apiCache.set(cacheKey, data, 60 * 60 * 1000); // 1h
      return data;
    } catch(e) { console.warn('OpenLigaDB matchday:', e.message); return null; }
  },
  async getMatches(league = 'bl1', season = '2024') {
    const cacheKey = `oldb_${league}_${season}`;
    const cached = apiCache.get(cacheKey);
    if (cached) return cached;
    try {
      const res = await fetch(`${this.BASE}/getmatchdata/${league}/${season}`);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      const today = new Date().toDateString();
      const todayMatches = data.filter(m => new Date(m.matchDateTime).toDateString() === today);
      const result = { all: data.slice(0, 20), today: todayMatches };
      apiCache.set(cacheKey, result, 15 * 60 * 1000); // 15min
      return result;
    } catch(e) { console.warn('OpenLigaDB matches:', e.message); return null; }
  },
  async getLiveScores() {
    try {
      const res = await fetch(`${this.BASE}/getmatchdata/bl1`);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      const live = data.filter(m => m.matchIsFinished === false && m.leagueId);
      return live.map(m => ({
        id: `oldb_${m.matchId}`,
        home: m.team1?.teamName || '?',
        away: m.team2?.teamName || '?',
        scoreHome: m.matchResults?.[0]?.pointsTeam1 ?? '-',
        scoreAway: m.matchResults?.[0]?.pointsTeam2 ?? '-',
        minute: null,
        isLive: !m.matchIsFinished,
        league: 'BL',
        source: 'openligadb',
      }));
    } catch(e) { console.warn('OpenLigaDB live:', e.message); return []; }
  },
  // Parse OpenLigaDB match to our format
  toAppMatch(m, idx) {
    return {
      id: `oldb_${m.matchId}`,
      leagueKey: 'BL',
      home: m.team1?.teamName || 'Heim',
      away: m.team2?.teamName || 'Auswärts',
      time: new Date(m.matchDateTime).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }),
      importance: null,
      odds: { home: 2.10, draw: 3.40, away: 3.20 }, // No odds from OLDB, fallback
      btts: { yes: 1.80, no: 2.00 },
      overUnder: { o25: 1.85, u25: 1.95 },
      form: { home: ['W','D','W','L','W'], away: ['D','W','L','W','D'] },
      homePos: idx + 1, awayPos: idx + 3,
      h2h: '2W 1D 2L',
      xG: { home: 1.5, away: 1.3 },
      avgGoals: 2.5,
      possession: { home: 52, away: 48 },
      handicap: { line: 0, homeOdds: 2.20, awayOdds: 2.10 },
      lineMovement: { opening: { home: 2.15, draw: 3.35, away: 3.10 }, homeMove: -2.3, awayMove: 3.1, sharpMoney: 'NEUTRAL', steamMove: false },
      source: 'openligadb',
      liveScore: m.matchResults?.[0] ? `${m.matchResults[0].pointsTeam1}:${m.matchResults[0].pointsTeam2}` : null,
    };
  }
};

// ─── 3. ESPN (inoffizielle API, kein Key) ─────────────────────
const espnAPI = {
  LEAGUES: {
    BL:  'ger.1',
    PL:  'eng.1',
    LL:  'esp.1',
    SA:  'ita.1',
    L1:  'fra.1',
    UCL: 'uefa.champions',
    UEL: 'uefa.europa',
    ECL: 'uefa.europa.conf',
    ERE: 'ned.1',
    SUP: 'tur.1',
    PRI: 'por.1',
  },
  BASE: 'https://site.api.espn.com/apis/site/v2/sports/soccer',
  cache: {},
  async getScoreboard(leagueCode) {
    const espnLeague = this.LEAGUES[leagueCode];
    if (!espnLeague) return null;
    const cacheKey = `espn_${espnLeague}`;
    const cached = apiCache.get(cacheKey);
    if (cached) return cached;
    try {
      const res = await fetch(`${this.BASE}/${espnLeague}/scoreboard`);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      apiCache.set(cacheKey, data, 5 * 60 * 1000); // 5min for live scores
      return data;
    } catch(e) { console.warn(`ESPN ${leagueCode}:`, e.message); return null; }
  },
  async getLiveScores(leagueCodes = ['UCL','UEL','BL','PL','LL','SA','L1']) {
    const allScores = [];
    const results = await Promise.allSettled(leagueCodes.map(lc => this.getScoreboard(lc)));
    results.forEach((res, i) => {
      if (res.status !== 'fulfilled' || !res.value?.events) return;
      const lc = leagueCodes[i];
      res.value.events.forEach(ev => {
        const comp = ev.competitions?.[0];
        if (!comp) return;
        const home = comp.competitors?.find(c => c.homeAway === 'home');
        const away = comp.competitors?.find(c => c.homeAway === 'away');
        const status = ev.status?.type;
        allScores.push({
          id: `espn_${ev.id}`,
          home: home?.team?.displayName || '?',
          away: away?.team?.displayName || '?',
          homeScore: home?.score ?? null,
          awayScore: away?.score ?? null,
          homeLogo: home?.team?.logo || null,
          awayLogo: away?.team?.logo || null,
          minute: status?.detail || null,
          isLive: status?.state === 'in',
          isFinished: status?.completed || false,
          isPending: status?.state === 'pre',
          statusText: status?.shortDetail || '',
          league: lc,
          leagueFlag: LEAGUES[lc]?.flag || '⚽',
          date: ev.date,
          source: 'espn',
        });
      });
    });
    return allScores;
  },
  // American moneyline → decimal odds
  mlToDec(ml) {
    if (ml == null || ml === '') return null;
    const n = typeof ml === 'string' ? parseInt(ml.replace(/[^-\d]/g,''),10) : Number(ml);
    if (!Number.isFinite(n) || n === 0) return null;
    return n > 0 ? +(n/100 + 1).toFixed(2) : +(100/Math.abs(n) + 1).toFixed(2);
  },
  // Convert ESPN event → app match format (today's fixtures source)
  eventToAppMatch(ev, leagueCode) {
    const comp = ev.competitions?.[0]; if (!comp) return null;
    const home = comp.competitors?.find(c => c.homeAway === 'home');
    const away = comp.competitors?.find(c => c.homeAway === 'away');
    if (!home || !away) return null;
    const status = ev.status?.type || {};
    const isLive = status.state === 'in';
    const isFinished = status.completed || false;
    const odds = comp.odds?.[0] || {};
    const ml = odds.moneyline || {};
    let homeOdd = this.mlToDec(ml.home?.close?.odds || ml.home?.open?.odds);
    let awayOdd = this.mlToDec(ml.away?.close?.odds || ml.away?.open?.odds);
    let drawOdd = this.mlToDec(ml.draw?.close?.odds || ml.draw?.open?.odds || odds.drawOdds?.moneyLine);
    if (!homeOdd) homeOdd = this.mlToDec(odds.homeTeamOdds?.moneyLine) || 2.10;
    if (!awayOdd) awayOdd = this.mlToDec(odds.awayTeamOdds?.moneyLine) || 3.20;
    if (!drawOdd) drawOdd = 3.40;
    // Over/Under 2.5
    const totalLine = parseFloat(odds.overUnder) || 2.5;
    const t = odds.total || {};
    const overOdd = this.mlToDec(t.over?.close?.odds || t.over?.open?.odds) || 1.85;
    const underOdd = this.mlToDec(t.under?.close?.odds || t.under?.open?.odds) || 1.95;
    const oddsProvider = odds.provider?.displayName || odds.provider?.name || null;
    const date = new Date(ev.date);
    const leagueMeta = (typeof LEAGUES !== 'undefined' && LEAGUES[leagueCode]) || {};
    return {
      id: `espn_${ev.id}`,
      espnEventId: ev.id,
      homeTeamId: home.team?.id || null,
      awayTeamId: away.team?.id || null,
      leagueKey: leagueCode,
      leagueShort: leagueCode,
      leagueFlag: leagueMeta.flag || '⚽',
      leagueName: leagueMeta.name || leagueCode,
      home: home.team?.displayName || home.team?.name || '?',
      away: away.team?.displayName || away.team?.name || '?',
      homeLogo: home.team?.logo || null,
      awayLogo: away.team?.logo || null,
      time: date.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }),
      importance: null,
      odds: { home: +homeOdd.toFixed(2), draw: +drawOdd.toFixed(2), away: +awayOdd.toFixed(2) },
      btts: { yes: 1.80, no: 2.00 },
      overUnder: { o25: +overOdd.toFixed(2), u25: +underOdd.toFixed(2), o35: 2.90, u35: 1.42, line: totalLine },
      htOdds: { hh: 3.5, hd: 3.3, ha: 8.0, dh: 7.5, dd: 9.5, da: 12.0, ah: 11.0, ad: 11.5, aa: 5.5 },
      homePos: null, awayPos: null,
      h2h: '— ',
      xG: { home: 1.5, away: 1.3 },
      avgGoals: 2.5,
      possession: { home: 50, away: 50 },
      handicap: { line: -0.5, homeOdds: 2.50, awayOdds: 1.55 },
      lineMovement: { opening: { home: +homeOdd.toFixed(2), draw: +drawOdd.toFixed(2), away: +awayOdd.toFixed(2) }, homeMove: 0, awayMove: 0, sharpMoney: 'NEUTRAL', steamMove: false },
      oddsSource: oddsProvider ? `ESPN/${oddsProvider}` : null,
      form: { home: ['W','D','W','L','W'], away: ['W','D','L','W','D'] },
      liveScore: (isLive || isFinished) ? `${home.score ?? 0}:${away.score ?? 0}` : null,
      liveMinute: status.detail || null,
      isLive,
      isFinished,
      statusText: status.shortDetail || '',
      source: 'espn',
    };
  },
  async getAllTodayMatches(leagueCodes = ['UCL','UEL','ECL','BL','PL','LL','SA','L1','ERE','SUP','PRI']) {
    const today = new Date().toISOString().split('T')[0];
    const results = await Promise.allSettled(leagueCodes.map(lc => this.getScoreboard(lc)));
    const matches = [];
    results.forEach((res, i) => {
      if (res.status !== 'fulfilled' || !res.value?.events) return;
      const lc = leagueCodes[i];
      res.value.events.forEach(ev => {
        const evDate = new Date(ev.date).toISOString().split('T')[0];
        if (evDate !== today) return;
        const m = this.eventToAppMatch(ev, lc);
        if (m) matches.push({ ...m, bookmakerOdds: generateBookmakerOdds(m.odds) });
      });
    });
    return matches;
  },
  // Match app match ID to ESPN match by team names
  matchToESPN(appMatch, espnScores) {
    const hName = appMatch.home.toLowerCase();
    const aName = appMatch.away.toLowerCase();
    return espnScores.find(s =>
      (s.home.toLowerCase().includes(hName.split(' ')[0]) || hName.includes(s.home.toLowerCase().split(' ')[0])) &&
      (s.away.toLowerCase().includes(aName.split(' ')[0]) || aName.includes(s.away.toLowerCase().split(' ')[0]))
    ) || null;
  }
};

// ─── LIVE SCORE TICKER ────────────────────────────────────────
const liveScoreTicker = {
  scores: {},
  listeners: [],
  refreshInterval: null,

  subscribe(cb) { this.listeners.push(cb); },
  unsubscribe(cb) { this.listeners = this.listeners.filter(l => l !== cb); },
  notify() { this.listeners.forEach(cb => cb({...this.scores})); },

  async refresh() {
    try {
      const [espnScores, blScores] = await Promise.all([
        espnAPI.getLiveScores(),
        openLigaDB.getLiveScores(),
      ]);
      const all = [...espnScores, ...blScores];
      all.forEach(s => { this.scores[s.id] = s; });
      // Auto-resolve finished matches → tracker
      try {
        all.filter(s => s.isFinished).forEach(s => {
          if (s.homeScore != null && s.awayScore != null && window.tracker) {
            // Mappe ESPN-score auf MATCHES_TODAY-id
            const m = MATCHES_TODAY.find(mt =>
              mt.id === s.id || mt.id === `espn_${s.id?.replace?.('espn_','')}` ||
              ((mt.home||'').toLowerCase().split(' ')[0] === (s.home||'').toLowerCase().split(' ')[0] &&
               (mt.away||'').toLowerCase().split(' ')[0] === (s.away||'').toLowerCase().split(' ')[0])
            );
            if (m) {
              const resolved = window.tracker.resolveMatch(m.id, { home: +s.homeScore, away: +s.awayScore }, { home: m.homeTeamId, away: m.awayTeamId });
              if (resolved) console.log(`✓ Tracker: ${resolved} Pick(s) für ${m.home} vs ${m.away} aufgelöst (${s.homeScore}:${s.awayScore})`);
              // Auto-Bets ebenfalls auflösen
              try {
                const updated = window.autobetStore?.resolveMatchPicks?.(m.id, { home: +s.homeScore, away: +s.awayScore });
                if (updated) console.log(`✓ AutoBet: ${updated} Pick(s) für ${m.home} vs ${m.away} resolved`);
              } catch(e) {}
            }
          }
        });
        // Closing-Line Snapshot: Spiele die "in" (live) gehen → friere Quoten ein
        all.filter(s => s.isLive).forEach(s => {
          const m = MATCHES_TODAY.find(mt => (mt.id||'').includes(s.id?.replace?.('espn_','')) ||
            ((mt.home||'').toLowerCase().split(' ')[0] === (s.home||'').toLowerCase().split(' ')[0]));
          if (m && m.odds) window.tracker?.snapshotClosing?.(m.id, m.odds);
        });
      } catch(e) { console.warn('Tracker auto-resolve failed:', e.message); }
      this.notify();
      return this.scores;
    } catch(e) { console.warn('LiveTicker:', e.message); return {}; }
  },

  start(intervalMs = 60000) {
    this.refresh();
    this.refreshInterval = setInterval(() => this.refresh(), intervalMs);
  },
  stop() {
    if (this.refreshInterval) clearInterval(this.refreshInterval);
  }
};

// ─── API STATUS CHECKER ───────────────────────────────────────
async function checkAPIStatus() {
  const results = {};

  // Open-Meteo (always free)
  try {
    const r = await fetch('https://api.open-meteo.com/v1/forecast?latitude=52.5&longitude=13.4&current=temperature_2m&forecast_days=1');
    results.openMeteo = r.ok ? 'OK' : 'ERROR';
  } catch { results.openMeteo = 'ERROR'; }

  // OpenLigaDB (always free)
  try {
    const r = await fetch('https://api.openligadb.de/getavailableleagues');
    results.openLigaDB = r.ok ? 'OK' : 'ERROR';
  } catch { results.openLigaDB = 'ERROR'; }

  // ESPN (unofficial, may be blocked)
  try {
    const r = await fetch('https://site.api.espn.com/apis/site/v2/sports/soccer/ger.1/scoreboard');
    results.espn = r.ok ? 'OK' : 'ERROR';
  } catch { results.espn = 'ERROR'; }


  return results;
}

// ─── MAIN LOADER: fetch all live data on startup ──────────────
async function loadLiveData() {
  // 0. REAL fixtures from ESPN (free, 11 leagues) — primary source
  try {
    const espnMatches = await espnAPI.getAllTodayMatches();
    if (espnMatches.length > 0) {
      MATCHES_TODAY.length = 0;
      espnMatches.forEach(m => MATCHES_TODAY.push(m));
      console.log(`✅ ESPN: ${espnMatches.length} echte Spiele geladen`);
      if (window.elo) window.elo.seedFromMatches(MATCHES_TODAY);
    }
  } catch(e) { console.warn('ESPN fixtures load failed:', e.message); }


  const matchIds = MATCHES_TODAY.map(m => m.id);

  // 1. Real weather for all venues
  try {
    const weatherData = await openMeteoAPI.getAllMatchWeather(matchIds);
    Object.entries(weatherData).forEach(([id, w]) => {
      const match = MATCHES_TODAY.find(m => m.id === id);
      if (match && w) {
        match.weather = w;
        match.weatherSource = 'open-meteo-live';
      }
    });
    console.log('✅ Open-Meteo Wetterdaten geladen');
  } catch(e) { console.warn('Open-Meteo load failed:', e.message); }

  // 2. ESPN live scores
  try {
    const espnScores = await espnAPI.getLiveScores();
    espnScores.forEach(score => {
      const match = MATCHES_TODAY.find(m => espnAPI.matchToESPN(m, [score]));
      if (match) {
        match.espnData = score;
        if (score.isLive || score.isFinished) {
          match.liveScore = `${score.homeScore}:${score.awayScore}`;
          match.liveMinute = score.minute;
          match.isLive = score.isLive;
        }
      }
    });
    console.log(`✅ ESPN: ${espnScores.length} Spiele geladen`);
  } catch(e) { console.warn('ESPN load failed:', e.message); }

  // 3. OpenLigaDB Bundesliga
  try {
    const blData = await openLigaDB.getMatches('bl1', '2024');
    if (blData?.today?.length > 0) {
      blData.today.forEach(m => {
        const match = MATCHES_TODAY.find(a =>
          a.leagueKey === 'BL' &&
          (a.home.includes(m.team1?.teamName?.split(' ')[0] || '') ||
           a.away.includes(m.team2?.teamName?.split(' ')[0] || ''))
        );
        if (match) {
          match.openLigaData = m;
          const res = m.matchResults?.[0];
          if (res) match.liveScore = `${res.pointsTeam1}:${res.pointsTeam2}`;
        }
      });
      console.log(`✅ OpenLigaDB: ${blData.today.length} BL-Spiele heute`);
    } else {
      console.log('ℹ️ OpenLigaDB: Keine BL-Spiele heute');
    }
  } catch(e) { console.warn('OpenLigaDB load failed:', e.message); }

  return true;
}

// ─── REACT HOOK: useLiveData ──────────────────────────────────
function useLiveData() {
  const [weatherLoaded, setWeatherLoaded] = React.useState(false);
  const [liveScores, setLiveScores] = React.useState({});
  const [apiStatus, setApiStatus] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    (async () => {
      await loadLiveData();
      setWeatherLoaded(true);
      setLoading(false);

      // Check API status
      const status = await checkAPIStatus();
      setApiStatus(status);

      // Start live ticker (ESPN + OpenLigaDB, every 60s)
      liveScoreTicker.subscribe(scores => setLiveScores(scores));
      liveScoreTicker.start(60000);
    })();
    return () => liveScoreTicker.stop();
  }, []);

  return { weatherLoaded, liveScores, apiStatus, loading };
}

// ─── LIVE SCORE BADGE COMPONENT ──────────────────────────────
function LiveScoreBadge({ match }) {
  if (!match.liveScore && !match.isLive) return null;
  const isLive = match.isLive;
  const c = isLive ? '#22ff88' : 'rgba(255,255,255,0.5)';
  return (
    <div style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      padding: '3px 10px', borderRadius: 99,
      background: isLive ? 'rgba(34,255,136,0.12)' : 'rgba(255,255,255,0.06)',
      border: `1px solid ${c}30`,
    }}>
      {isLive && <div style={{ width: 6, height: 6, borderRadius: '50%', background: '#22ff88', animation: 'livePing 1.5s infinite' }} />}
      <span style={{ fontSize: 12, fontWeight: 800, fontFamily: 'JetBrains Mono', color: c, letterSpacing: '.05em' }}>{match.liveScore}</span>
      {match.liveMinute && <span style={{ fontSize: 9, color: c, fontFamily: 'JetBrains Mono' }}>{match.liveMinute}</span>}
    </div>
  );
}

// ─── API STATUS PANEL ─────────────────────────────────────────
function APIStatusPanel({ status }) {
  if (!status) return null;
  const items = [
    { key: 'openMeteo', label: 'Open-Meteo', icon: '🌦️', free: true },
    { key: 'openLigaDB', label: 'OpenLigaDB', icon: '🏟️', free: true },
    { key: 'espn', label: 'ESPN', icon: '📺', free: true },
  ];
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, marginBottom: 16 }}>
      {items.map(item => {
        const s = status[item.key];
        const ok = s === 'OK' || s === 'KEY_SET';
        const noKey = s === 'NO_KEY';
        const color = ok ? '#22ff88' : noKey ? '#ffb648' : '#ff5470';
        const statusLabel = ok ? 'ONLINE' : noKey ? 'KEY FEHLT' : 'OFFLINE';
        return (
          <div key={item.key} style={{ background: `${color}08`, border: `1px solid ${color}22`, borderRadius: 12, padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ fontSize: 18 }}>{item.icon}</span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 10, fontWeight: 700, color: 'rgba(255,255,255,0.8)', fontFamily: 'Space Grotesk' }}>{item.label}</div>
              <div style={{ fontSize: 8, color, fontFamily: 'JetBrains Mono', letterSpacing: '.06em' }}>{statusLabel} {item.free ? '· FREE' : '· API KEY'}</div>
            </div>
            <div style={{ width: 8, height: 8, borderRadius: '50%', background: color, boxShadow: `0 0 6px ${color}`, flexShrink: 0 }} />
          </div>
        );
      })}
    </div>
  );
}

// ─── WEATHER DETAIL CARD ──────────────────────────────────────
function WeatherDetailCard({ match }) {
  const w = match.weather;
  if (!w) return null;
  const c = getWeatherImpactColor(w.impact);
  const isLive = w.source === 'open-meteo-live';
  return (
    <div style={{
      padding: '10px 12px',
      background: `${c}08`, border: `1px solid ${c}20`,
      borderRadius: 14, marginBottom: 10,
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
        <span style={{ fontSize: 22 }}>{w.label.split(' ')[1] || '🌤️'}</span>
        <div style={{ flex: 1 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <span style={{ fontSize: 12, fontWeight: 700, color: c, fontFamily: 'Space Grotesk' }}>{w.label.split(' ')[0]}</span>
            {isLive && <span style={{ fontSize: 8, background: 'rgba(34,255,136,0.15)', color: '#22ff88', padding: '1px 6px', borderRadius: 99, fontFamily: 'JetBrains Mono' }}>LIVE</span>}
          </div>
          <div style={{ fontSize: 9, color: 'rgba(255,255,255,0.4)', fontFamily: 'JetBrains Mono' }}>{w.venue || match.home + ' Stadion'} · {w.city}</div>
        </div>
        <div style={{ textAlign: 'right' }}>
          <div style={{ fontSize: 18, fontWeight: 700, fontFamily: 'JetBrains Mono', color: 'rgba(255,255,255,0.85)' }}>{w.temp}°C</div>
        </div>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 5 }}>
        {[
          { l: 'WIND', v: `${w.wind}km/h` },
          { l: 'REGEN', v: `${w.rain}%` },
          { l: 'FEUCHTE', v: `${w.humidity || '?'}%` },
          { l: 'EINFLUSS', v: w.impact.replace('_', ' ') },
        ].map(s => (
          <div key={s.l} style={{ background: 'rgba(255,255,255,0.04)', borderRadius: 8, padding: '5px 4px', textAlign: 'center' }}>
            <div style={{ fontSize: 7, color: 'rgba(255,255,255,0.3)', fontFamily: 'JetBrains Mono', marginBottom: 2 }}>{s.l}</div>
            <div style={{ fontSize: 9, fontWeight: 700, fontFamily: 'JetBrains Mono', color: c }}>{s.v}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── LIVE TICKER SCREEN ───────────────────────────────────────
function LiveTickerScreen({ liveScores }) {
  const scores = Object.values(liveScores);
  const live    = scores.filter(s => s.isLive);
  const finished = scores.filter(s => s.isFinished);
  const pending  = scores.filter(s => s.isPending);

  if (scores.length === 0) return (
    <div style={{ padding: '32px 16px', textAlign: 'center' }}>
      <div style={{ fontSize: 36, marginBottom: 12, opacity: 0.4 }}>📺</div>
      <div style={{ fontSize: 14, color: 'rgba(255,255,255,0.4)', fontFamily: 'Space Grotesk', marginBottom: 6 }}>Keine Live-Spiele</div>
      <div style={{ fontSize: 11, color: 'rgba(255,255,255,0.25)', fontFamily: 'Inter', lineHeight: 1.6 }}>ESPN & OpenLigaDB werden alle 60s aktualisiert</div>
    </div>
  );

  const ScoreRow = ({ s }) => {
    const c = s.isLive ? '#22ff88' : s.isFinished ? 'rgba(255,255,255,0.4)' : '#ffb648';
    return (
      <div style={{ padding: '10px 12px', background: s.isLive ? 'rgba(34,255,136,0.06)' : 'rgba(255,255,255,0.025)', border: `1px solid ${c}20`, borderRadius: 12, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 10 }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 9, color: 'rgba(255,255,255,0.3)', fontFamily: 'JetBrains Mono', marginBottom: 3 }}>
            {s.leagueFlag} {s.league} {s.isLive && <span style={{ color: '#22ff88' }}>· LIVE {s.minute}</span>}
          </div>
          <div style={{ fontSize: 12, fontWeight: 700, fontFamily: 'Space Grotesk', color: 'rgba(255,255,255,0.9)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.home} vs {s.away}</div>
        </div>
        {(s.homeScore !== null) ? (
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
            <span style={{ fontSize: 20, fontWeight: 800, fontFamily: 'JetBrains Mono', color: c }}>{s.homeScore}</span>
            <span style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', fontFamily: 'JetBrains Mono' }}>:</span>
            <span style={{ fontSize: 20, fontWeight: 800, fontFamily: 'JetBrains Mono', color: c }}>{s.awayScore}</span>
          </div>
        ) : (
          <span style={{ fontSize: 10, color: '#ffb648', fontFamily: 'JetBrains Mono' }}>{s.statusText}</span>
        )}
      </div>
    );
  };

  return (
    <div style={{ height: '100%', overflowY: 'auto', padding: '14px 16px' }}>
      {live.length > 0 && (
        <>
          <div style={{ fontSize: 9, color: '#22ff88', fontFamily: 'JetBrains Mono', letterSpacing: '.14em', marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
            <div style={{ width: 6, height: 6, borderRadius: '50%', background: '#22ff88', animation: 'livePing 1.5s infinite' }} />LIVE JETZT ({live.length})
          </div>
          {live.map(s => <ScoreRow key={s.id} s={s} />)}
        </>
      )}
      {finished.length > 0 && (
        <>
          <div style={{ fontSize: 9, color: 'rgba(255,255,255,0.35)', fontFamily: 'JetBrains Mono', letterSpacing: '.14em', marginBottom: 10, marginTop: 12 }}>ABGEPFIFFEN ({finished.length})</div>
          {finished.map(s => <ScoreRow key={s.id} s={s} />)}
        </>
      )}
      {pending.length > 0 && (
        <>
          <div style={{ fontSize: 9, color: '#ffb648', fontFamily: 'JetBrains Mono', letterSpacing: '.14em', marginBottom: 10, marginTop: 12 }}>BALD ({pending.length})</div>
          {pending.slice(0, 8).map(s => <ScoreRow key={s.id} s={s} />)}
        </>
      )}
      <div style={{ height: 16 }} />
    </div>
  );
}

Object.assign(window, {
  openMeteoAPI, openLigaDB, espnAPI,
  liveScoreTicker, loadLiveData, checkAPIStatus,
  useLiveData, LiveScoreBadge, APIStatusPanel,
  WeatherDetailCard, LiveTickerScreen,
  VENUE_COORDS, wmoToCondition, apiCache,
});
