import httpx
import uvicorn
import os
import secrets
import time
import json
import psutil
import unicodedata
import re
from datetime import datetime
import pytz
from typing import List
from fastapi import FastAPI, HTTPException, Depends, Request
from fastapi.responses import RedirectResponse, HTMLResponse, PlainTextResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles
from apscheduler.schedulers.background import BackgroundScheduler
from pydantic import BaseModel

app = FastAPI()
security = HTTPBasic()

# --- PERMITIR CARPETA DE IMÁGENES ---
app.mount("/img", StaticFiles(directory=os.path.join(os.path.dirname(__file__), "img")), name="img")

# --- ARCHIVO DE SEGURIDAD Y CONFIGURACIÓN ---
SECURITY_FILE = os.path.join(os.path.dirname(__file__), "security.json")

def cargar_seguridad():
    if os.path.exists(SECURITY_FILE):
        try:
            with open(SECURITY_FILE, "r") as f:
                return json.load(f)
        except:
            pass
    return {"enabled": False, "ips": []}

def guardar_seguridad(data):
    with open(SECURITY_FILE, "w") as f:
        json.dump(data, f)

security_config = cargar_seguridad()

# Estado global para el origen de la NBA (Prime vs Sling)
nba_settings = {"source": "prime"} 

class SecurityModel(BaseModel):
    enabled: bool
    ips: List[str]

class NBASourceModel(BaseModel):
    source: str

# --- CREDENCIALES TELEGRAM ---
TELEGRAM_BOT_TOKEN = "8287241319:AAHrtt6ja1LbxkoB8HRznABECLnXHM7T0zY"
TELEGRAM_CHAT_ID = "1797735602"

def enviar_alerta_telegram(mensaje):
    try:
        url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
        payload = {
            'chat_id': TELEGRAM_CHAT_ID, 
            'text': f"📡 <b>Alerta del Router Maestro</b>\n\n{mensaje}", 
            'parse_mode': 'HTML'
        }
        httpx.post(url, data=payload, timeout=5.0)
    except Exception as e:
        print(f"[ERROR] No se pudo enviar Telegram: {e}")

def verificar_credenciales(credentials: HTTPBasicCredentials = Depends(security)):
    usuario_correcto = secrets.compare_digest(credentials.username, "Zeus")
    pass_correcto = secrets.compare_digest(credentials.password, "Laloekmj232023")
    if not (usuario_correcto and pass_correcto):
        raise HTTPException(
            status_code=401, 
            detail="Acceso denegado", 
            headers={"WWW-Authenticate": "Basic"}
        )
    return credentials.username

# --- VARIABLES GLOBALES DE RED ---
last_net_io = psutil.net_io_counters()
last_net_time = time.time()

# --- CONFIGURACIÓN MULTIPANEL ---
O11_PANELS = {
    "panel_1": {
        "host": "http://23.237.34.58:7070",
        "auth": "Basic YWRtaW46TVYzNTJFWlRCM0NP"
    },
    "panel_prime": {
        "host": "http://172.111.38.198:2020",
        "auth": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI3Mzc3MjI1NTAsInBhc3N3b3JkIjoiYjM1M2U2NjA4ZjkzZTQ0MTA4NmY1MjA2NzRkNDcyMTlhNjQ4Mjk1MGFmNDc3YzRkZDlmYzU2ZDMyMTZhMmRjNyIsInVzZXJuYW1lIjoiYWRtaW4ifQ.E2PMNGEBMdhE1NwBD7ewhgUAdrYDkX-FA1udOC0rfMc"
    },
    "panel_triller": {
        "host": "http://51.81.107.170:7070",
        "auth": "Basic YWRtaW46TzFDNFdUTEYyR1lY"
    }
}

# --- CANALES MAESTROS NACIONALES E INTERNACIONALES ---
ESPN_STREAM = "http://51.81.107.170:7070/stream/hulumiami/espn/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f"
ABC_STREAM = "http://15.204.107.193:7070/stream/hulumiami/wsvn/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"
NBC_STREAM = "http://15.204.107.193:7070/stream/hulumiami/wtvj/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"
NFL_NETWORK_STREAM = "http://15.204.107.193:7070/stream/huluatlanta/nflnetwork/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"
TBS_STREAM = "http://51.81.107.170:7070/stream/hulumiami/tbseast/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f"
FOX_MLB_STREAM = "http://15.204.107.193:7070/stream/huluatlanta/waga/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"
FS1_STREAM = "http://51.81.107.170:7070/stream/hulumiami/fs1/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f"
NBA_TV_STREAM = "http://51.81.107.170:7070/stream/sling/nbatv/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f"
TNT_STREAM = "http://15.204.107.193:7070/stream/hulumiami/tnteast/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"

def sling(team):
    return f"http://15.204.47.5:1234/stream/sling/{team}/master.m3u8?u=admin&p=8366d2f479ee25eaecd01b42960c3fe667ac6f49f2fb41d7c236fde115bd64de"

def nba_sling(team):
    return f"http://51.81.107.170:7070/stream/sling/{team}/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f"

# --- ESTRUCTURA UNIFICADA DE LIGAS ---
LEAGUES = ["nfl", "mlb", "nba", "nhl", "mls", "ppv", "lmp", "triller"]

current_game_info = {league: {} for league in LEAGUES}
manual_overrides = {league: {} for league in LEAGUES}
active_viewers = {league: {} for league in LEAGUES}

STREAM_DATABASE = {
    "nfl": {
        "cardinals": {"abbr": "ari", "CBS": "http://15.204.107.193:7070/stream/huluphoenix/kpho/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://23.237.34.58:7070/stream/huluphoenix/ksaz/master.m3u8?u=admin&p=99dfdd1ac82f455c4de1572a9eca98c29392199d1cd5a912b6cc36f53de8848b"},
        "falcons": {"abbr": "atl", "CBS": "http://15.204.107.193:7070/stream/huluatlanta/wupa/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/huluatlanta/waga/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "ravens": {"abbr": "bal", "CBS": "http://15.204.107.193:7070/stream/hulubaltimore/wjz/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/hulubaltimore/wbff/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "bills": {"abbr": "buf", "CBS": "http://15.204.107.193:7070/stream/hulubuffalo/wivb/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/hulubuffalo/wutv/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "panthers": {"abbr": "car", "CBS": "http://15.204.107.193:7070/stream/hulucharlotte/wbtv/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/hulucharlotte/wjzy/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "bears": {"abbr": "chi", "CBS": "http://51.81.107.170:7070/stream/huluchicago/wbbm/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f", "FOX": "http://51.81.107.170:7070/stream/huluchicago/wfld/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f"},
        "bengals": {"abbr": "cin", "CBS": "http://15.204.107.193:7070/stream/hulucincinnati/wkrc/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/hulucincinnati/wxix/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "browns": {"abbr": "cle", "CBS": "http://15.204.107.193:7070/stream/hulucleveland/woio/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/hulucleveland/wjw/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "cowboys": {"abbr": "dal", "CBS": "http://23.237.34.58:7070/stream/huludallas/ktvt/master.m3u8?u=admin&p=99dfdd1ac82f455c4de1572a9eca98c29392199d1cd5a912b6cc36f53de8848b", "FOX": "http://147.135.104.121:1234/stream/huludallas/kdfw/master.m3u8?u=admin&p=8366d2f479ee25eaecd01b42960c3fe667ac6f49f2fb41d7c236fde115bd64de"},
        "broncos": {"abbr": "den", "CBS": "http://15.204.107.193:7070/stream/huludenver/kcnc/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/huludenver/kdvr/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "lions": {"abbr": "det", "CBS": "http://15.204.107.193:7070/stream/huludetroit/wwj/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/huludetroit/wjbk/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "packers": {"abbr": "gb", "CBS": "http://15.204.107.193:7070/stream/hulugreenbay/wfrv/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/hulugreenbay/wfrv/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "texans": {"abbr": "hou", "CBS": "http://15.204.107.193:7070/stream/huluhouston/khou/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/huluhouston/kriv/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "colts": {"abbr": "ind", "CBS": "http://15.204.107.193:7070/stream/huluindianapolis/wttv/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/huluindianapolis/wxin/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "jaguars": {"abbr": "jax", "CBS": "http://15.204.107.193:7070/stream/hulujacksonville/wjax/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/hulujacksonville/wfox/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "chiefs": {"abbr": "kc", "CBS": "http://15.204.107.193:7070/stream/huluwichita/kwch/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/huluwichita/ksas/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "raiders": {"abbr": "lv", "CBS": "http://147.135.104.121:1234/stream/hululasvegas/klas/master.m3u8?u=admin&p=8366d2f479ee25eaecd01b42960c3fe667ac6f49f2fb41d7c236fde115bd64de", "FOX": "http://51.81.107.170:7070/stream/hululasvegas/kvvu/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f"},
        "rams": {"abbr": "lar", "CBS": "http://15.204.107.193:7070/stream/hulula/kcbs/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://23.237.34.58:7070/stream/hulula/kttv/master.m3u8?u=admin&p=99dfdd1ac82f455c4de1572a9eca98c29392199d1cd5a912b6cc36f53de8848b"},
        "chargers": {"abbr": "lac", "CBS": "http://15.204.107.193:7070/stream/hulula/kcbs/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://23.237.34.58:7070/stream/hulula/kttv/master.m3u8?u=admin&p=99dfdd1ac82f455c4de1572a9eca98c29392199d1cd5a912b6cc36f53de8848b"},
        "dolphins": {"abbr": "mia", "CBS": "http://23.237.34.58:7070/stream/hulumiami/wfor/master.m3u8?u=admin&p=99dfdd1ac82f455c4de1572a9eca98c29392199d1cd5a912b6cc36f53de8848b", "FOX": "http://147.135.104.121:1234/stream/hulumiami/wsvn/master.m3u8?u=admin&p=8366d2f479ee25eaecd01b42960c3fe667ac6f49f2fb41d7c236fde115bd64de"},
        "vikings": {"abbr": "min", "CBS": "http://15.204.107.193:7070/stream/huluminneapolis/wcco/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/huluminneapolis/kmsp/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "titans": {"abbr": "ten", "CBS": "http://15.204.107.193:7070/stream/hulunashville/wtvf/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/hulunashville/wztv/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "patriots": {"abbr": "ne", "CBS": "http://15.204.107.193:7070/stream/huluboston/wbz/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://51.81.107.170:7070/stream/huluboston/wfxt/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f"},
        "saints": {"abbr": "no", "CBS": "http://15.204.107.193:7070/stream/huluneworleans/wwl/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://51.81.107.170:7070/stream/huluneworleans/wvue/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f"},
        "giants": {"abbr": "nyg", "CBS": "http://15.204.107.193:7070/stream/hulunuevayork/wcbs/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://51.81.107.170:7070/stream/hulunuevayork/wnyw/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f"},
        "jets": {"abbr": "nyj", "CBS": "http://15.204.107.193:7070/stream/hulunuevayork/wcbs/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://51.81.107.170:7070/stream/hulunuevayork/wnyw/master.m3u8?u=admin&p=9b70e03097fa174868df37b951b40959fe67e648dcd5f9c5c346e9873b48597f"},
        "eagles": {"abbr": "phi", "CBS": "http://15.204.107.193:7070/stream/huluphili/kyw/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://147.135.104.121:1234/stream/huluphili/wtxf/master.m3u8?u=admin&p=8366d2f479ee25eaecd01b42960c3fe667ac6f49f2fb41d7c236fde115bd64de"},
        "steelers": {"abbr": "pit", "CBS": "http://15.204.107.193:7070/stream/hulupittsburgh/kdka/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/hulupittsburgh/wpgh/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "49ers": {"abbr": "sf", "CBS": "http://15.204.107.193:7070/stream/hulusanfrancisco/kpix/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/hulusanfrancisco/ktvu/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "seahawks": {"abbr": "sea", "CBS": "http://15.204.107.193:7070/stream/huluseattle/kiro/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://15.204.107.193:7070/stream/huluseattle/kcpq/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd"},
        "buccaneers": {"abbr": "tb", "CBS": "http://15.204.107.193:7070/stream/hulutampa/wtsp/master.m3u8?u=admin&p=5f797969be6d73acfc4b2456701fe5269ac6b62ed7745a0890a2397e0d1c6ddd", "FOX": "http://23.237.34.58:7070/stream/hulutampa/wtvt/master.m3u8?u=admin&p=99dfdd1ac82f455c4de1572a9eca98c29392199d1cd5a912b6cc36f53de8848b"},
        "commanders": {"abbr": "was", "CBS": "http://147.135.104.121:1234/stream/huluwashington/wusa/master.m3u8?u=admin&p=8366d2f479ee25eaecd01b42960c3fe667ac6f49f2fb41d7c236fde115bd64de", "FOX": "http://147.135.104.121:1234/stream/huluwashington/wttg/master.m3u8?u=admin&p=8366d2f479ee25eaecd01b42960c3fe667ac6f49f2fb41d7c236fde115bd64de"}
    },
    "mlb": {
        "diamondbacks": {"abbr": "ari", "regional": sling("arizonadiamondbacks")},
        "braves": {"abbr": "atl", "regional": sling("atlantabraves")},
        "orioles": {"abbr": "bal", "regional": sling("baltimoreorioles")},
        "redsox": {"abbr": "bos", "regional": sling("bostonredsox")},
        "cubs": {"abbr": "chc", "regional": sling("chicagocubs")},
        "whitesox": {"abbr": "chw", "regional": sling("chicagowhitesox")},
        "reds": {"abbr": "cin", "regional": sling("cincinnatireds")},
        "guardians": {"abbr": "cle", "regional": sling("clevelandguardians")},
        "rockies": {"abbr": "col", "regional": sling("coloradorockies")},
        "tigers": {"abbr": "det", "regional": sling("detroittigers")},
        "astros": {"abbr": "hou", "regional": sling("houstonastros")},
        "royals": {"abbr": "kc", "regional": sling("kansascityroyals")},
        "angels": {"abbr": "laa", "regional": sling("losangelesangels")},
        "dodgers": {"abbr": "lad", "regional": sling("losangelesdodgers")},
        "marlins": {"abbr": "mia", "regional": sling("miamimarlins")},
        "brewers": {"abbr": "mil", "regional": sling("milwaukeebrewers")},
        "twins": {"abbr": "min", "regional": sling("minnesotatwins")},
        "mets": {"abbr": "nym", "regional": sling("newyorkmets")},
        "yankees": {"abbr": "nyy", "regional": sling("newyorkyankees")},
        "athletics": {"abbr": "oak", "regional": sling("oaklandathletics")},
        "phillies": {"abbr": "phi", "regional": sling("philadelphiaphillies")},
        "pirates": {"abbr": "pit", "regional": sling("pittsburghpirates")},
        "padres": {"abbr": "sd", "regional": sling("sandiegopadres")},
        "giants": {"abbr": "sf", "regional": sling("sanfranciscogiants")},
        "mariners": {"abbr": "sea", "regional": sling("seattlemariners")},
        "cardinals": {"abbr": "stl", "regional": sling("stlouiscardinals")},
        "rays": {"abbr": "tb", "regional": sling("tampabayrays")},
        "rangers": {"abbr": "tex", "regional": sling("texasrangers")},
        "bluejays": {"abbr": "tor", "regional": sling("torontobluejays")},
        "nationals": {"abbr": "wsh", "regional": sling("washingtonnationals")}
    },
    "nba": {
        "hawks": {"abbr": "atl", "regional": "", "sling": nba_sling("atlantahawks")},
        "celtics": {"abbr": "bos", "regional": "", "sling": nba_sling("bostonceltics")},
        "nets": {"abbr": "bkn", "regional": "", "sling": nba_sling("brooklynnets")},
        "hornets": {"abbr": "cha", "regional": "", "sling": nba_sling("charlottehornets")},
        "bulls": {"abbr": "chi", "regional": "", "sling": nba_sling("chicagobulls")},
        "cavaliers": {"abbr": "cle", "regional": "", "sling": nba_sling("clevelandcavaliers")},
        "mavericks": {"abbr": "dal", "regional": "", "sling": nba_sling("dallasmavericks")},
        "nuggets": {"abbr": "den", "regional": "", "sling": nba_sling("denvernuggets")},
        "pistons": {"abbr": "det", "regional": "", "sling": nba_sling("detroitpistons")},
        "warriors": {"abbr": "gsw", "regional": "", "sling": nba_sling("goldenstatewarriors")},
        "rockets": {"abbr": "hou", "regional": "", "sling": nba_sling("houstonrockets")},
        "pacers": {"abbr": "ind", "regional": "", "sling": nba_sling("indianapacers")},
        "clippers": {"abbr": "lac", "regional": "", "sling": nba_sling("losangelesclippers")},
        "lakers": {"abbr": "lal", "regional": "", "sling": nba_sling("losangeleslakers")},
        "grizzlies": {"abbr": "mem", "regional": "", "sling": nba_sling("memphisgrizzlies")},
        "heat": {"abbr": "mia", "regional": "", "sling": nba_sling("miamiheat")},
        "bucks": {"abbr": "mil", "regional": "", "sling": nba_sling("milwaukeebucks")},
        "timberwolves": {"abbr": "min", "regional": "", "sling": nba_sling("minnesotatimberwolves")},
        "pelicans": {"abbr": "nop", "regional": "", "sling": nba_sling("neworleanspelicans")},
        "knicks": {"abbr": "nyk", "regional": "", "sling": nba_sling("newyorkknicks")},
        "thunder": {"abbr": "okc", "regional": "", "sling": nba_sling("oklahomacitythunder")},
        "magic": {"abbr": "orl", "regional": "", "sling": nba_sling("orlandomagic")},
        "76ers": {"abbr": "phi", "regional": "", "sling": nba_sling("philadelphia76ers")},
        "suns": {"abbr": "phx", "regional": "", "sling": nba_sling("phoenixsuns")},
        "trailblazers": {"abbr": "por", "regional": "", "sling": nba_sling("portlandtrailblazers")},
        "kings": {"abbr": "sac", "regional": "", "sling": nba_sling("sacramentokings")},
        "spurs": {"abbr": "sas", "regional": "", "sling": nba_sling("sanantoniospurs")},
        "raptors": {"abbr": "tor", "regional": "", "sling": nba_sling("torontoraptors")},
        "jazz": {"abbr": "utah", "regional": "", "sling": nba_sling("utahjazz")},
        "wizards": {"abbr": "was", "regional": "", "sling": nba_sling("washingtonwizards")}
    },
    "nhl": {},
    "mls": {
        "mls360": {"abbr": "mls", "o11_name": "MLS 360", "regional": "", "is_es": False, "base_slug": "mls360"},
        "mls360_es": {"abbr": "mls", "o11_name": "MLS 360", "regional": "", "is_es": True, "base_slug": "mls360"},
        "atlantaunited": {"abbr": "atl", "o11_name": "Atlanta United", "regional": "", "is_es": False, "base_slug": "atlantaunited"},
        "atlantaunited_es": {"abbr": "atl", "o11_name": "Atlanta United", "regional": "", "is_es": True, "base_slug": "atlantaunited"},
        "austinfc": {"abbr": "atx", "o11_name": "Austin FC", "regional": "", "is_es": False, "base_slug": "austinfc"},
        "austinfc_es": {"abbr": "atx", "o11_name": "Austin FC", "regional": "", "is_es": True, "base_slug": "austinfc"},
        "charlottefc": {"abbr": "clt", "o11_name": "Charlotte FC", "regional": "", "is_es": False, "base_slug": "charlottefc"},
        "charlottefc_es": {"abbr": "clt", "o11_name": "Charlotte FC", "regional": "", "is_es": True, "base_slug": "charlottefc"},
        "chicagofire": {"abbr": "chi", "o11_name": "Chicago Fire FC", "regional": "", "is_es": False, "base_slug": "chicagofire"},
        "chicagofire_es": {"abbr": "chi", "o11_name": "Chicago Fire FC", "regional": "", "is_es": True, "base_slug": "chicagofire"},
        "fccincinnati": {"abbr": "cin", "o11_name": "FC Cincinnati", "regional": "", "is_es": False, "base_slug": "fccincinnati"},
        "fccincinnati_es": {"abbr": "cin", "o11_name": "FC Cincinnati", "regional": "", "is_es": True, "base_slug": "fccincinnati"},
        "coloradorapids": {"abbr": "col", "o11_name": "Colorado Rapids", "regional": "", "is_es": False, "base_slug": "coloradorapids"},
        "coloradorapids_es": {"abbr": "col", "o11_name": "Colorado Rapids", "regional": "", "is_es": True, "base_slug": "coloradorapids"},
        "columbuscrew": {"abbr": "clb", "o11_name": "Columbus Crew", "regional": "", "is_es": False, "base_slug": "columbuscrew"},
        "columbuscrew_es": {"abbr": "clb", "o11_name": "Columbus Crew", "regional": "", "is_es": True, "base_slug": "columbuscrew"},
        "fcdallas": {"abbr": "dal", "o11_name": "FC Dallas", "regional": "", "is_es": False, "base_slug": "fcdallas"},
        "fcdallas_es": {"abbr": "dal", "o11_name": "FC Dallas", "regional": "", "is_es": True, "base_slug": "fcdallas"},
        "dcunited": {"abbr": "dc", "o11_name": "D.C. United", "regional": "", "is_es": False, "base_slug": "dcunited"},
        "dcunited_es": {"abbr": "dc", "o11_name": "D.C. United", "regional": "", "is_es": True, "base_slug": "dcunited"},
        "houstondynamo": {"abbr": "hou", "o11_name": "Houston Dynamo FC", "regional": "", "is_es": False, "base_slug": "houstondynamo"},
        "houstondynamo_es": {"abbr": "hou", "o11_name": "Houston Dynamo FC", "regional": "", "is_es": True, "base_slug": "houstondynamo"},
        "sportingkc": {"abbr": "skc", "o11_name": "Sporting Kansas City", "regional": "", "is_es": False, "base_slug": "sportingkc"},
        "sportingkc_es": {"abbr": "skc", "o11_name": "Sporting Kansas City", "regional": "", "is_es": True, "base_slug": "sportingkc"},
        "lagalaxy": {"abbr": "la", "o11_name": "LA Galaxy", "regional": "", "is_es": False, "base_slug": "lagalaxy"},
        "lagalaxy_es": {"abbr": "la", "o11_name": "LA Galaxy", "regional": "", "is_es": True, "base_slug": "lagalaxy"},
        "lafc": {"abbr": "lafc", "o11_name": "Los Angeles Football Club", "regional": "", "is_es": False, "base_slug": "lafc"},
        "lafc_es": {"abbr": "lafc", "o11_name": "Los Angeles Football Club", "regional": "", "is_es": True, "base_slug": "lafc"},
        "intermiami": {"abbr": "mia", "o11_name": "Inter Miami CF", "regional": "", "is_es": False, "base_slug": "intermiami"},
        "intermiami_es": {"abbr": "mia", "o11_name": "Inter Miami CF", "regional": "", "is_es": True, "base_slug": "intermiami"},
        "minnesotaunited": {"abbr": "min", "o11_name": "Minnesota United FC", "regional": "", "is_es": False, "base_slug": "minnesotaunited"},
        "minnesotaunited_es": {"abbr": "min", "o11_name": "Minnesota United FC", "regional": "", "is_es": True, "base_slug": "minnesotaunited"},
        "cfmontreal": {"abbr": "mtl", "o11_name": "CF Montréal", "regional": "", "is_es": False, "base_slug": "cfmontreal"},
        "cfmontreal_es": {"abbr": "mtl", "o11_name": "CF Montréal", "regional": "", "is_es": True, "base_slug": "cfmontreal"},
        "nashvillesc": {"abbr": "nsh", "o11_name": "Nashville SC", "regional": "", "is_es": False, "base_slug": "nashvillesc"},
        "nashvillesc_es": {"abbr": "nsh", "o11_name": "Nashville SC", "regional": "", "is_es": True, "base_slug": "nashvillesc"},
        "newenglandrevolution": {"abbr": "ne", "o11_name": "New England Revolution", "regional": "", "is_es": False, "base_slug": "newenglandrevolution"},
        "newenglandrevolution_es": {"abbr": "ne", "o11_name": "New England Revolution", "regional": "", "is_es": True, "base_slug": "newenglandrevolution"},
        "nycfc": {"abbr": "nyc", "o11_name": "New York City Football Club", "regional": "", "is_es": False, "base_slug": "nycfc"},
        "nycfc_es": {"abbr": "nyc", "o11_name": "New York City Football Club", "regional": "", "is_es": True, "base_slug": "nycfc"},
        "newyorkredbulls": {"abbr": "ny", "o11_name": "Red Bull New York", "regional": "", "is_es": False, "base_slug": "newyorkredbulls"},
        "newyorkredbulls_es": {"abbr": "ny", "o11_name": "Red Bull New York", "regional": "", "is_es": True, "base_slug": "newyorkredbulls"},
        "orlandocity": {"abbr": "orl", "o11_name": "Orlando City", "regional": "", "is_es": False, "base_slug": "orlandocity"},
        "orlandocity_es": {"abbr": "orl", "o11_name": "Orlando City", "regional": "", "is_es": True, "base_slug": "orlandocity"},
        "philadelphiaunion": {"abbr": "phi", "o11_name": "Philadelphia Union", "regional": "", "is_es": False, "base_slug": "philadelphiaunion"},
        "philadelphiaunion_es": {"abbr": "phi", "o11_name": "Philadelphia Union", "regional": "", "is_es": True, "base_slug": "philadelphiaunion"},
        "portlandtimbers": {"abbr": "por", "o11_name": "Portland Timbers", "regional": "", "is_es": False, "base_slug": "portlandtimbers"},
        "portlandtimbers_es": {"abbr": "por", "o11_name": "Portland Timbers", "regional": "", "is_es": True, "base_slug": "portlandtimbers"},
        "realsaltlake": {"abbr": "rsl", "o11_name": "Real Salt Lake", "regional": "", "is_es": False, "base_slug": "realsaltlake"},
        "realsaltlake_es": {"abbr": "rsl", "o11_name": "Real Salt Lake", "regional": "", "is_es": True, "base_slug": "realsaltlake"},
        "sandiegofc": {"abbr": "sd", "o11_name": "San Diego FC", "regional": "", "is_es": False, "base_slug": "sandiegofc"},
        "sandiegofc_es": {"abbr": "sd", "o11_name": "San Diego FC", "regional": "", "is_es": True, "base_slug": "sandiegofc"},
        "sanjoseearthquakes": {"abbr": "sj", "o11_name": "San Jose Earthquakes", "regional": "", "is_es": False, "base_slug": "sanjoseearthquakes"},
        "sanjoseearthquakes_es": {"abbr": "sj", "o11_name": "San Jose Earthquakes", "regional": "", "is_es": True, "base_slug": "sanjoseearthquakes"},
        "seattlesounders": {"abbr": "sea", "o11_name": "Seattle Sounders FC", "regional": "", "is_es": False, "base_slug": "seattlesounders"},
        "seattlesounders_es": {"abbr": "sea", "o11_name": "Seattle Sounders FC", "regional": "", "is_es": True, "base_slug": "seattlesounders"},
        "stlouiscitysc": {"abbr": "stl", "o11_name": "St. Louis CITY SC", "regional": "", "is_es": False, "base_slug": "stlouiscitysc"},
        "stlouiscitysc_es": {"abbr": "stl", "o11_name": "St. Louis CITY SC", "regional": "", "is_es": True, "base_slug": "stlouiscitysc"},
        "torontofc": {"abbr": "tor", "o11_name": "Toronto FC", "regional": "", "is_es": False, "base_slug": "torontofc"},
        "torontofc_es": {"abbr": "tor", "o11_name": "Toronto FC", "regional": "", "is_es": True, "base_slug": "torontofc"},
        "vancouverwhitecaps": {"abbr": "van", "o11_name": "Vancouver Whitecaps FC", "regional": "", "is_es": False, "base_slug": "vancouverwhitecaps"},
        "vancouverwhitecaps_es": {"abbr": "van", "o11_name": "Vancouver Whitecaps FC", "regional": "", "is_es": True, "base_slug": "vancouverwhitecaps"}
    },
    "ppv": {
        "lagranja1": {
            "abbr": "lagranja1", 
            "o11_name": "la granja vip: acceso total 24/7 - 1", 
            "regional": "", 
            "custom_logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/77/Disney_Plus_logo.svg/500px-Disney_Plus_logo.svg.png", 
            "dynamic_logo": "", 
            "provider": "espnmx"
        },
        "lagranja2": {
            "abbr": "lagranja2", 
            "o11_name": "la granja vip: acceso total 24/7 - 2", 
            "regional": "", 
            "custom_logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/77/Disney_Plus_logo.svg/500px-Disney_Plus_logo.svg.png", 
            "dynamic_logo": "", 
            "provider": "espnmx"
        },
        "danawhite": {
            "abbr": "danawhite", 
            "o11_name": "dana white", 
            "regional": "", 
            "custom_logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/11/Amazon_Prime_Video_logo.svg/500px-Amazon_Prime_Video_logo.svg.png", 
            "dynamic_logo": "", 
            "provider": "primemx"
        }
    },
    "lmp": {
        "aguilas": {"abbr": "mxc", "o11_name": "aguilas|mexicali", "regional": "", "custom_logo": "/img/lmp_aguilas.png"},
        "naranjeros": {"abbr": "her", "o11_name": "naranjeros|hermosillo", "regional": "", "custom_logo": "/img/lmp_naranjeros.png"},
        "yaquis": {"abbr": "obr", "o11_name": "yaquis|obregon", "regional": "", "custom_logo": "/img/lmp_yaquis.png"},
        "mayos": {"abbr": "nav", "o11_name": "mayos|navojoa", "regional": "", "custom_logo": "/img/lmp_mayos.png"},
        "caneros": {"abbr": "moc", "o11_name": "cañeros|mochis", "regional": "", "custom_logo": "/img/lmp_caneros.png"},
        "algodoneros": {"abbr": "gsv", "o11_name": "algodoneros|guasave", "regional": "", "custom_logo": "/img/lmp_algodoneros.png"},
        "tomateros": {"abbr": "cul", "o11_name": "tomateros|culiacan", "regional": "", "custom_logo": "/img/lmp_tomateros.png"},
        "venados": {"abbr": "maz", "o11_name": "venados|mazatlan", "regional": "", "custom_logo": "/img/lmp_venados.png"},
        "charros": {"abbr": "jal", "o11_name": "charros|jalisco", "regional": "", "custom_logo": "/img/lmp_charros.png"},
        "jaguares": {"abbr": "nay", "o11_name": "jaguares|nayarit|tepic", "regional": "", "custom_logo": "/img/lmp_jaguares.png"}
    },
    "triller": {} # Base de datos dinámica para TrillerTV
}

ESPN_API_URLS = {
    "nfl": "https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard",
    "mlb": "https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/scoreboard",
    "nba": "https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard",
    "nhl": "https://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard",
    "mls": "https://site.api.espn.com/apis/site/v2/sports/soccer/usa.1/scoreboard"
}

class OverrideModel(BaseModel):
    league: str
    team: str
    url: str

class NBASourceModel(BaseModel):
    source: str

OFFICIAL_LOGOS = {league: {} for league in LEAGUES}

def get_today_str():
    tz = pytz.timezone("America/Tijuana")
    return datetime.now(tz).strftime("%Y%m%d")

def normalize_text(text):
    if not text:
        return ""
    t = str(text).lower()
    return ''.join(c for c in unicodedata.normalize('NFD', t) if unicodedata.category(c) != 'Mn').replace(" ", "").replace(".", "").replace("-", "")

def get_matched_slug(league, team_obj):
    name = normalize_text(team_obj.get("name", ""))
    short = normalize_text(team_obj.get("shortDisplayName", ""))
    abbr = normalize_text(team_obj.get("abbreviation", ""))
    
    if league == "mls":
        espn_to_db = {
            "rbny": "newyorkredbulls", "nyrb": "newyorkredbulls", "newyorkrb": "newyorkredbulls",
            "mia": "intermiami", "intermiamicf": "intermiami",
            "skc": "sportingkc", "sportingkansascity": "sportingkc",
            "chi": "chicagofire", "chicagofirefc": "chicagofire",
            "atl": "atlantaunited", "atlantaunitedfc": "atlantaunited",
            "hou": "houstondynamo", "houstondynamofc": "houstondynamo",
            "min": "minnesotaunited", "minnesotaunitedfc": "minnesotaunited",
            "mtl": "cfmontreal", "cfmontreal": "cfmontreal", "montreal": "cfmontreal",
            "sea": "seattlesounders", "seattlesoundersfc": "seattlesounders",
            "van": "vancouverwhitecaps", "vancouverwhitecapsfc": "vancouverwhitecaps",
            "dc": "dcunited", "lafc": "lafc", "losangelesfootballclub": "lafc",
            "la": "lagalaxy", "sd": "sandiegofc"
        }
        if abbr in espn_to_db: return espn_to_db[abbr]
        if name in espn_to_db: return espn_to_db[name]
        if short in espn_to_db: return espn_to_db[short]

    if league == "nba":
        espn_to_db_nba = {
            "76ers": "76ers", "phila": "76ers", "phi": "76ers",
            "blazers": "trailblazers", "por": "trailblazers",
            "utah": "jazz", "uta": "jazz"
        }
        if abbr in espn_to_db_nba: return espn_to_db_nba[abbr]
        if name in espn_to_db_nba: return espn_to_db_nba[name]
        if short in espn_to_db_nba: return espn_to_db_nba[short]
    
    for db_slug, db_data in STREAM_DATABASE.get(league, {}).items():
        if db_data.get("is_es", False): 
            continue
            
        if db_data.get("abbr", "").lower() == abbr and abbr != "": 
            return db_slug
        if db_slug == name or db_slug == short: 
            return db_slug
            
        if league == "nba" and db_slug == "nets" and "hornets" in name: 
            continue
            
        if db_slug in name: 
            return db_slug
        
    return name

def sync_official_logos():
    endpoints = {
        "nfl": "https://site.api.espn.com/apis/site/v2/sports/football/nfl/teams",
        "mlb": "https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/teams",
        "nba": "https://site.api.espn.com/apis/site/v2/sports/basketball/nba/teams",
        "mls": "https://site.api.espn.com/apis/site/v2/sports/soccer/usa.1/teams"
    }
    for league, url in endpoints.items():
        try:
            resp = httpx.get(url, timeout=15)
            data = resp.json()
            sports = data.get("sports", [])
            if sports:
                leagues = sports[0].get("leagues", [])
                if leagues:
                    teams_list = leagues[0].get("teams", [])
                    for t_item in teams_list:
                        t_obj = t_item.get("team", {})
                        slug = get_matched_slug(league, t_obj)
                        
                        logos = t_obj.get("logos", [])
                        if logos:
                            logo_url = logos[0].get("href", "").replace("http://", "https://")
                            logo_url = logo_url.split("?")[0] + "?w=500&h=500&transparent=true"
                            OFFICIAL_LOGOS[league][slug] = logo_url
        except Exception:
            pass

def clean_old_viewers():
    now = time.time()
    for league in LEAGUES:
        for team, ips in list(active_viewers[league].items()):
            active_viewers[league][team] = {ip: data for ip, data in ips.items() if now - data["time"] < 500}

def is_national_tv(net_str):
    net_str = net_str.upper()
    if "NBCSN" in net_str:
        return False
    for n in ["ESPN", "ABC", "NBC", "PEACOCK", "TNT", "NBA TV", "NBATV", "PRIME"]:
        if n in net_str:
            return True
    return False

def get_nba_network_display(network_str):
    if not network_str:
        return "NBA LEAGUE PASS"
    net_up = network_str.upper()
    if "NBCSN" in net_up:
        return "NBA LEAGUE PASS"
    for valid in ["ESPN", "ABC", "NBC", "PEACOCK", "PRIME", "TNT", "NBA TV", "NBATV"]:
        if valid in net_up:
            return network_str
    return "NBA LEAGUE PASS"

def get_team_display_name(league, slug, data):
    base = data.get("base_slug", slug).upper()
    if league == "mls" and data.get("is_es"):
        return f"{base} <span class='text-emerald-400 text-[10px] ml-2 border border-emerald-500/30 bg-emerald-900/20 px-1.5 py-0.5 rounded'>ESPAÑOL</span> <img src='https://flagcdn.com/w20/mx.png' class='inline-block w-4 h-3 ml-1 rounded-sm shadow-sm' style='transform: translateY(-2px);'>"
    return base

def sync_schedule():
    global current_game_info
    today_str = get_today_str()
    for league, url in ESPN_API_URLS.items():
        try:
            full_url = f"{url}?dates={today_str}"
            resp = httpx.get(full_url, timeout=10)
            data = resp.json()
            mapping = {}
            for event in data.get("events", []):
                status = event.get("status", {}).get("type", {}).get("state", "pre")
                competition = event["competitions"][0]
                
                broadcast = ""
                if competition.get("broadcasts"):
                    broadcast = " ".join(competition["broadcasts"][0].get("names", [])).upper()
                
                if league == "nba":
                    broadcast = get_nba_network_display(broadcast)
                elif not broadcast:
                    broadcast = "LOCAL"
                
                for competitor in competition.get("competitors", []):
                    team_data = competitor.get("team", {})
                    slug = get_matched_slug(league, team_data)
                    
                    mapping[slug] = {
                        "network": broadcast,
                        "match_name": event.get("shortName", ""),
                        "match_date": event.get("date", ""),
                        "state": status,
                        "logo": team_data.get("logo", "")
                    }
            current_game_info[league] = mapping
        except Exception:
            pass

    try:
        tz = pytz.timezone("America/Tijuana")
        today_lmp = datetime.now(tz).strftime("%Y-%m-%d")
        
        lmp_url = f"https://api.lmp.mx/3.0.0/scoreboard?date={today_lmp}&days_next=2&days_prev=1"
        lmp_headers = {
            "accept": "application/json, text/plain, */*",
            "accept-language": "es-MX,es;q=0.9,en-US;q=0.8",
            "origin": "https://www.lmp.mx",
            "referer": "https://www.lmp.mx/",
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36"
        }
        
        resp = httpx.get(lmp_url, headers=lmp_headers, timeout=10)
        data = resp.json()
        lmp_mapping = current_game_info.get("lmp", {})
        
        all_games = []
        resp_data = data.get("response", {})
        
        if "games" in resp_data and isinstance(resp_data["games"], list):
            all_games.extend(resp_data["games"])
            
        if "prevGames" in resp_data and isinstance(resp_data["prevGames"], list):
            all_games.extend(resp_data["prevGames"])
        if "nextGames" in resp_data and isinstance(resp_data["nextGames"], list):
            all_games.extend(resp_data["nextGames"])
            
        for game in all_games:
            status_code = game.get("gameStatus", 0)
            state = "pre"
            if status_code == 1: state = "in"
            elif status_code == 2: state = "post"
            
            local_date_str = game.get("localFullDate", "")
            
            if today_lmp not in local_date_str and state != "in":
                continue

            game_date_iso = local_date_str
            
            if local_date_str:
                try:
                    dt = datetime.strptime(local_date_str, "%Y-%m-%d %H:%M:%S")
                    dt_aware = tz.localize(dt)
                    game_date_iso = dt_aware.isoformat()
                    now = datetime.now(tz)
                    if state == "pre" and now >= dt_aware:
                        state = "in"
                except:
                    pass
            
            away_name = game.get("awayTeamShort", "Away")
            home_name = game.get("homeTeamShort", "Home")
            match_name = f"{away_name} vs {home_name}"
            
            away_clean = normalize_text(away_name)
            home_clean = normalize_text(home_name)
            
            for slug, db_data in STREAM_DATABASE["lmp"].items():
                o11_names = [normalize_text(n) for n in db_data.get("o11_name", "").split("|")]
                if any(name and (name in away_clean or name in home_clean) for name in o11_names):
                    lmp_mapping[slug] = {
                        "network": "LMP TV",
                        "match_name": match_name,
                        "match_date": game_date_iso,
                        "state": state,
                        "logo": "" 
                    }
                    
        current_game_info["lmp"] = lmp_mapping
    except Exception as e:
        pass

def get_disney_token(is_manual=False):
    token_file = os.path.join(os.path.dirname(__file__), "accessToken.json")
    refresh_file = os.path.join(os.path.dirname(__file__), "refreshToken.txt")
    
    if os.path.exists(token_file):
        try:
            data = json.loads(open(token_file).read())
            if time.time() < data.get("exp_date", 0):
                return data["access_token"], ""
        except: 
            pass
        
    if not os.path.exists(refresh_file): 
        msg = "⚠️ API Disney+: Falta el archivo 'refreshToken.txt' en la carpeta del servidor."
        if is_manual: 
            return None, msg
        return None, ""
        
    refresh_token = open(refresh_file).read().strip()
    url = "https://disney.api.edge.bamgrid.com/graph/v1/device/graphql"
    headers = {
        'Host': 'disney.api.edge.bamgrid.com',
        'authorization': 'Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84',
        'x-bamsdk-client-id': 'disney-svod-3d9324fc',
        'x-bamsdk-version': '35.8',
        'x-bamsdk-platform': 'javascript/windows/chrome',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
        'accept': 'application/json',
        'content-type': 'application/json',
        'Origin': 'https://www.disneyplus.com'
    }
    payload = {
        "query": "mutation refreshToken($input:RefreshTokenInput!){refreshToken(refreshToken:$input){activeSession{sessionId}}}", 
        "variables": {"input": {"refreshToken": refresh_token}}, 
        "operationName": "refreshToken"
    }
    try:
        r = httpx.post(url, json=payload, headers=headers, timeout=10.0)
        data = r.json()
        if 'errors' in data: 
            if is_manual:
                return None, f"❌ API Disney+: El token expiró. Error: {data['errors'][0].get('message')}"
            return None, ""
            
        new_access = data['extensions']['sdk']['token']['accessToken']
        new_refresh = data['extensions']['sdk']['token']['refreshToken']
        with open(refresh_file, 'w') as f: 
            f.write(new_refresh)
        with open(token_file, 'w') as f: 
            f.write(json.dumps({"access_token": new_access, "exp_date": time.time() + 14000}))
        return new_access, "✅ API Disney+: Token renovado con éxito."
    except Exception as e: 
        if is_manual:
            return None, f"❌ API Disney+: Error HTTP ({str(e)})"
        return None, ""

def sync_disney_schedule(is_manual=False):
    token, msj_token = get_disney_token(is_manual)
    if not token: 
        if is_manual: 
            return False, msj_token
        return False, ""
    
    urls = [
        "https://disney.api.edge.bamgrid.com/explore/v1.18/page/page-26e7614f-0e33-431d-ada3-d5db4e4e1da4?limit=15",
        "https://disney.api.edge.bamgrid.com/explore/v1.18/set/0dc02d86-bc33-450c-8240-93f3a8dda292?limit=60"
    ]
    headers = {
        'authorization': f'Bearer {token}', 
        'x-bamsdk-client-id': 'disney-svod-3d9324fc', 
        'x-bamsdk-version': '35.8', 
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 
        'accept': 'application/json'
    }
    mapping = current_game_info.get("ppv", {})
    logos_encontrados = 0
    titulos_vistos = []
    
    for url in urls:
        try:
            r = httpx.get(url, headers=headers, timeout=15.0)
            if r.status_code == 200:
                data_json = r.json()
                items = data_json.get('data', {}).get('set', {}).get('items', [])
                if not items:
                    for c in data_json.get('data', {}).get('page', {}).get('containers', []): 
                        items.extend(c.get('items', []) or c.get('set', {}).get('items', []))
                for item in items:
                    visuals = item.get('visuals', {})
                    title = visuals.get('title', 'Unknown')
                    if title not in titulos_vistos and title != 'Unknown': 
                        titulos_vistos.append(title)
                    
                    logo_url = ""
                    for pid in [visuals.get('artwork', {}).get('matchup', {}).get('tile', {}).get('1.78', {}).get('imageId'), visuals.get('artwork', {}).get('standard', {}).get('tile', {}).get('1.78', {}).get('imageId')]:
                        if pid:
                            logo_url = f"https://disney.images.edge.bamgrid.com/ripcut-delivery/v2/variant/disney/{pid}/compose?format=webp&width=800"
                            break
                    
                    title_low = title.lower()
                    slug = ""
                    if "granja" in title_low or "acceso" in title_low or "cámara" in title_low or "camara" in title_low:
                        if "1" in title_low or "principal" in title_low or "duelo" in title_low: 
                            slug = "lagranja1"
                        elif "2" in title_low or "secundaria" in title_low or "asamblea" in title_low: 
                            slug = "lagranja2"
                        else: 
                            slug = "lagranja1"
                    
                    if slug:
                        logos_encontrados += 1
                        mapping[slug] = {
                            "network": "DISNEY+", 
                            "match_name": title.strip(), 
                            "match_date": "🔴 24/7 EN VIVO", 
                            "state": "in", 
                            "logo": logo_url
                        }
        except Exception: 
            pass
            
    if mapping and logos_encontrados > 0:
        current_game_info["ppv"] = mapping
        if is_manual: 
            return True, f"✅ API Disney+: Se conectó con éxito y extrajo {logos_encontrados} portadas HD."
        return True, ""
    else:
        if is_manual: 
            return False, f"⚠️ API Disney+: Conexión exitosa, pero no detectó los eventos."
        return False, ""

def sync_triller_schedule(is_manual=False):
    """
    Motor de sincronización de TrillerTV.
    Lee los IDs de o11 y construye las portadas y horarios usando
    la caché centralizada de Triller.py
    """
    host = O11_PANELS["panel_triller"]["host"]
    auth = O11_PANELS["panel_triller"]["auth"]
    
    url_action = f"{host}/api/action"
    headers_o11 = {
        "Accept": "*/*",
        "Authorization": auth,
        "Content-Type": "application/json",
        "Origin": host,
        "Referer": f"{host}/config/trillertv?type=live",
        "User-Agent": "Mozilla/5.0"
    }
    
    try:
        httpx.post(url_action, headers=headers_o11, json={"Action": "channelsupdaterequest", "ProviderId": "trillertv"}, timeout=15.0)
        time.sleep(5) 
        resp_apply = httpx.post(url_action, headers=headers_o11, json={"Action": "channelsupdateapply", "ProviderId": "trillertv"}, timeout=15.0)
        
        if resp_apply.status_code == 200:
            m3u_url = f"{host}/playlist.m3u"
            resp_m3u = httpx.get(m3u_url, headers={"Authorization": auth}, timeout=15.0)
            
            if resp_m3u.status_code == 200:
                lines = resp_m3u.text.strip().split('\n')
                mapping = {}
                db_triller = {}
                
                # Intentamos descargar la caché remota que generó o11
                cache_url = f"{host}/scripts/triller_cache.json" # <--- ASUMIENDO QUE LOS SCRIPTS ESTÁN ACCESIBLES VIA WEB
                remote_cache = {}
                try:
                    r_cache = httpx.get(cache_url, timeout=5.0)
                    if r_cache.status_code == 200:
                        remote_cache = r_cache.json()
                except Exception:
                    pass

                current_extinf = ""
                for line in lines:
                    line = line.strip()
                    if line.startswith("#EXTINF"):
                        current_extinf = line
                    elif line.startswith("http") and current_extinf:
                        id_match = re.search(r'\[(\d+)\]', current_extinf)
                        
                        if id_match:
                            raw_slug = id_match.group(1)
                            safe_slug = f"triller{raw_slug}"
                            
                            raw_title = current_extinf.split(',')[-1].strip()
                            clean_title = re.sub(r'^.*?\[\d+\]\s*', '', raw_title)
                            
                            logo_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Triller_Logo.png/500px-Triller_Logo.png"
                            event_date_iso = "🔜 SCHEDULED"
                            event_state = "pre"
                            
                            if raw_slug == "4534230":
                                event_state = "in"
                                event_date_iso = "🔴 24/7 EN VIVO"
                                clean_title = "TrillerTV 24/7 Live"
                                logo_url = "https://thumbs.fite.tv/api_11/headers/triile_tv_247_logo_EMfZaHQ.png"
                            else:
                                # Usamos la caché si existe
                                if raw_slug in remote_cache:
                                    c_data = remote_cache[raw_slug]
                                    logo_url = c_data.get("logo", logo_url)
                                    start_time_raw = c_data.get("start_time", "")
                                    
                                    if start_time_raw:
                                        try:
                                            dt_raw = start_time_raw.replace("+00:00", "Z").split(".")[0]
                                            dt = datetime.strptime(start_time_raw, "%Y-%m-%dT%H:%M:%S+00:00")
                                            tz_utc = pytz.utc
                                            tz_tj = pytz.timezone("America/Tijuana")
                                            dt_utc = tz_utc.localize(dt)
                                            dt_tj = dt_utc.astimezone(tz_tj)
                                            
                                            now_tj = datetime.now(tz_tj)
                                            event_date_iso = dt_tj.isoformat()
                                            
                                            if now_tj >= dt_tj:
                                                event_state = "in"
                                        except Exception: pass

                            db_triller[safe_slug] = {
                                "abbr": safe_slug,
                                "o11_name": clean_title,
                                "regional": line, 
                                "custom_logo": logo_url
                            }
                            
                            mapping[safe_slug] = {
                                "network": "TRILLER TV",
                                "match_name": clean_title,
                                "match_date": event_date_iso,
                                "state": event_state,
                                "logo": logo_url
                            }
                        current_extinf = ""
                
                if mapping:
                    current_game_info["triller"] = mapping
                    STREAM_DATABASE["triller"] = db_triller
                    if is_manual: return True, f"✅ API TrillerTV: Sincronización exitosa. {len(mapping)} eventos mapeados."
                    return True, ""
                    
        if is_manual: return False, "❌ API TrillerTV: El panel o11 no respondió."
        return False, ""
    except Exception as e:
        if is_manual: return False, f"❌ API TrillerTV: Error de conexión HTTP ({str(e)})"
        return False, ""

def parse_o11_m3u(panel_key="panel_1"):
    host = O11_PANELS.get(panel_key, {}).get("host")
    auth = O11_PANELS.get(panel_key, {}).get("auth")
    if not host or not auth: 
        return False
    
    m3u_url = f"{host}/playlist.m3u"
    headers = {
        "Accept": "*/*", 
        "Accept-Language": "es-MX", 
        "Authorization": auth, 
        "User-Agent": "Mozilla/5.0"
    }
    
    try:
        resp = httpx.get(m3u_url, headers=headers, timeout=20.0)
        if resp.status_code == 200:
            lines = resp.text.strip().split('\n')
            if len(lines) < 2: 
                return False 
            
            for league in ["nba", "mls", "ppv", "lmp"]:
                for slug, data in STREAM_DATABASE[league].items():
                    reg_url = data.get("regional", "")
                    if reg_url and host in reg_url:
                        STREAM_DATABASE[league][slug]["regional"] = ""
            
            current_extinf = ""
            for line in lines:
                line = line.strip()
                if line.startswith("#EXTINF"):
                    current_extinf = line
                elif line.startswith("http") and current_extinf:
                    event_name_full = current_extinf.split(',')[-1].strip()
                    event_name_clean = normalize_text(event_name_full)
                    
                    logo_match = re.search(r'tvg-logo="([^"]+)"', current_extinf)
                    extracted_logo = logo_match.group(1) if logo_match else ""
                    
                    if panel_key == "panel_1":
                        if 'group-title="MLS"' in current_extinf or '[MLS]' in current_extinf or 'MLS' in current_extinf.upper():
                            is_spanish_stream = "mls(es)" in event_name_clean or "mlses" in event_name_clean
                            
                            for slug, data in STREAM_DATABASE["mls"].items():
                                is_es_slug = data.get("is_es", False)
                                
                                if is_spanish_stream != is_es_slug:
                                    continue
                                    
                                o11_name = normalize_text(data.get("o11_name", ""))
                                if o11_name and o11_name in event_name_clean: 
                                    STREAM_DATABASE["mls"][slug]["regional"] = line
                                    
                        for slug in ["lagranja1", "lagranja2"]:
                            o11_name = normalize_text(STREAM_DATABASE["ppv"][slug].get("o11_name", ""))
                            if o11_name and o11_name in event_name_clean:
                                STREAM_DATABASE["ppv"][slug]["regional"] = line
                                if extracted_logo: 
                                    STREAM_DATABASE["ppv"][slug]["dynamic_logo"] = extracted_logo
                                if slug not in current_game_info.get("ppv", {}): 
                                    current_game_info.setdefault("ppv", {})[slug] = {}
                                if not current_game_info["ppv"][slug].get("match_name"):
                                    current_game_info["ppv"][slug].update({
                                        "match_name": event_name_full, 
                                        "match_date": "🔴 24/7 EN VIVO", 
                                        "network": "DISNEY+", 
                                        "state": "in"
                                    })
                        
                        if 'LMP:' in event_name_full or 'LMP :' in event_name_full.upper():
                            for slug, data in STREAM_DATABASE["lmp"].items():
                                o11_names = [normalize_text(n) for n in data.get("o11_name", "").split("|")]
                                if any(name and name in event_name_clean for name in o11_names):
                                    STREAM_DATABASE["lmp"][slug]["regional"] = line

                    elif panel_key == "panel_prime":
                        if any(k in event_name_clean for k in ["danawhite", "contender", "dwcs"]):
                            STREAM_DATABASE["ppv"]["danawhite"]["regional"] = line
                            if extracted_logo: 
                                STREAM_DATABASE["ppv"]["danawhite"]["dynamic_logo"] = extracted_logo
                            
                            match_time = re.search(r'\((?:Live at|Live)\s*(\d{1,2}):(\d{2})\s*(AM|PM)\s*([A-Z]{3,4})\)', event_name_full, re.IGNORECASE)
                            event_state = "pre"
                            event_date_iso = ""

                            if match_time:
                                hour = int(match_time.group(1))
                                minute = int(match_time.group(2))
                                ampm = match_time.group(3).upper()
                                if ampm == "PM" and hour < 12: hour += 12
                                elif ampm == "AM" and hour == 12: hour = 0
                                
                                tz = pytz.timezone("America/Tijuana")
                                now = datetime.now(tz)
                                target_dt = tz.localize(datetime(now.year, now.month, now.day, hour, minute, 0))
                                
                                if "tomorrow" in event_name_clean or "manana" in event_name_clean:
                                    from datetime import timedelta
                                    target_dt = target_dt + timedelta(days=1)
                                    
                                event_date_iso = target_dt.isoformat()
                                if now >= target_dt: 
                                    event_state = "in"
                            else:
                                event_state = "pre"
                                event_date_iso = ""

                            current_game_info.setdefault("ppv", {}).setdefault("danawhite", {}).update({
                                "match_name": event_name_full, 
                                "match_date": event_date_iso, 
                                "network": "PRIME VIDEO", 
                                "state": event_state
                            })
                            
                        is_other_sport = any(word in event_name_clean for word in [
                            "seahawks", "broncos", "patriots", "cowboys", "steelers", "packers", 
                            "chiefs", "eagles", "49ers", "bengals", "buccaneers", "ravens",
                            "blackhawks", "donetsk", "redbulls", "vikings", "canadiens", 
                            "mapleleafs", "nhl", "ucl", "uefa", "champions"
                        ])
                        
                        if not is_other_sport:
                            if "/stream/primemx/" not in line and "/stream/nba" not in line:
                                continue
                                
                            for slug in STREAM_DATABASE["nba"].keys():
                                if slug == "nets" and "hornets" in event_name_clean and "brooklyn" not in event_name_clean: 
                                    continue
                                    
                                if re.search(rf'\b{slug}\b', event_name_full, re.IGNORECASE):
                                    if "tomorrow" in event_name_clean or "manana" in event_name_clean:
                                        continue
                                        
                                    STREAM_DATABASE["nba"][slug]["regional"] = line
                                    
                                    match_time = re.search(r'\((?:Live at|Live)\s*(\d{1,2}):(\d{2})\s*(AM|PM)\s*([A-Z]{3,4})\)', event_name_full, re.IGNORECASE)
                                    event_state = "pre"
                                    
                                    if match_time:
                                        hour = int(match_time.group(1))
                                        minute = int(match_time.group(2))
                                        ampm = match_time.group(3).upper()
                                        if ampm == "PM" and hour < 12: hour += 12
                                        elif ampm == "AM" and hour == 12: hour = 0
                                        
                                        tz = pytz.timezone("America/Tijuana")
                                        now = datetime.now(tz)
                                        target_dt = tz.localize(datetime(now.year, now.month, now.day, hour, minute, 0))
                                        
                                        if now >= target_dt:
                                            event_state = "in"
                                            
                                    current_game_info.setdefault("nba", {}).setdefault(slug, {})
                                    
                                    if not current_game_info["nba"][slug].get("match_name"):
                                        current_game_info["nba"][slug]["match_name"] = event_name_full
                                        current_game_info["nba"][slug]["match_date"] = "🔴 NBA LEAGUE PASS"
                                        
                                    actual_net = current_game_info["nba"][slug].get("network", "")
                                    if not is_national_tv(actual_net):
                                        current_game_info["nba"][slug]["network"] = "NBA LEAGUE PASS"
                                    
                                    if current_game_info["nba"][slug].get("state") != "in":
                                        current_game_info["nba"][slug]["state"] = event_state

                    current_extinf = ""
            return True
        return False
    except Exception as e: 
        return False

def sync_o11_provider(panel_key: str, provider_id: str, title: str, is_manual=False):
    host = O11_PANELS.get(panel_key, {}).get("host")
    auth = O11_PANELS.get(panel_key, {}).get("auth")
    if not host or not auth: 
        return
    
    referer = f"{host}/events/{provider_id}?server=local" if panel_key == "panel_prime" else f"{host}/config/{provider_id}?type=live"
    headers = {
        "Accept": "*/*", 
        "Accept-Language": "es-MX", 
        "Authorization": auth, 
        "Content-Type": "application/json", 
        "Origin": host, 
        "Referer": referer, 
        "User-Agent": "Mozilla/5.0"
    }
    error_encontrado = False
    mensaje_telegram = f"🔄 <b>REPORTE DE DIAGNÓSTICO ({title})</b>\n\n"

    try:
        if panel_key == "panel_prime":
            url = f"{host}/api/provider/get"
            resp_apply = httpx.post(url, headers=headers, json={}, timeout=15.0)
        else:
            url = f"{host}/api/action"
            httpx.post(url, headers=headers, json={"Action": "channelsupdaterequest", "ProviderId": provider_id}, timeout=15.0)
            time.sleep(5)
            resp_apply = httpx.post(url, headers=headers, json={"Action": "channelsupdateapply", "ProviderId": provider_id}, timeout=15.0)
        
        if resp_apply.status_code != 200:
            error_encontrado = True
            mensaje_telegram += f"❌ <b>PANEL o11</b>: Fallo (HTTP {resp_apply.status_code}).\n"
        elif is_manual: 
            mensaje_telegram += f"✅ <b>PANEL o11</b>: Actualización exitosa.\n"
    except Exception:
        error_encontrado = True
        mensaje_telegram += f"❌ <b>PANEL o11</b>: Error de red al contactar el servidor.\n"
    
    time.sleep(5)
    
    if provider_id == "trillertv":
        exito_parser = sync_triller_schedule(is_manual)
    else:
        exito_parser = parse_o11_m3u(panel_key)
        
    if not exito_parser:
        error_encontrado = True
        mensaje_telegram += "❌ <b>MAPEO M3U</b>: Error al descargar lista del panel.\n"
    elif is_manual: 
        mensaje_telegram += "✅ <b>MAPEO M3U</b>: Links asignados a las tarjetas.\n"
        
    if provider_id == "espnmx":
        ok_disney, msg_disney = sync_disney_schedule(is_manual=True)
        mensaje_telegram += f"\n{msg_disney}\n"
        
    if error_encontrado or is_manual: 
        enviar_alerta_telegram(mensaje_telegram)

def auditar_juegos_prime():
    equipos_afectados = []
    for slug, info in current_game_info["nfl"].items():
        network = info.get("network", "")
        if ("PRIME" in network or "AMAZON" in network) and not manual_overrides["nfl"].get(slug): 
            equipos_afectados.append(slug.upper())
            
    if equipos_afectados:
        mensaje = (
            "⚠️ <b>RECORDATORIO DE JUEGOS PRIME (NFL)</b> ⚠️\n\n"
            "El sistema detectó que hoy se transmitirá un juego exclusivo por Amazon Prime.\n\n"
            "Los siguientes equipos <b>NO</b> tienen un Link Maestro asignado aún:\n"
            f"👉 <b>{', '.join(equipos_afectados)}</b>\n\n"
            "Entra al panel del Router y coloca tu enlace en el campo 'Forzar Link Manual' para evitar caídas."
        )
        enviar_alerta_telegram(mensaje)

scheduler = BackgroundScheduler(timezone="America/Tijuana")
scheduler.add_job(sync_schedule, 'interval', minutes=2)
scheduler.add_job(sync_disney_schedule, 'interval', minutes=10)
scheduler.add_job(sync_triller_schedule, 'interval', minutes=15) # Cronjob Exclusivo para Cartelera Triller
scheduler.add_job(clean_old_viewers, 'interval', seconds=30)
scheduler.add_job(auditar_juegos_prime, 'cron', day_of_week='thu', hour=10, minute=0)

scheduler.add_job(lambda: sync_o11_provider("panel_1", "mls", "MLS Apple TV", False), 'interval', minutes=15)
scheduler.add_job(lambda: sync_o11_provider("panel_1", "espnmx", "PPV / ESPN MX (La Granja)", False), 'interval', minutes=30)
scheduler.add_job(lambda: sync_o11_provider("panel_prime", "primemx", "PPV / Prime Video", False), 'interval', minutes=25)
scheduler.add_job(lambda: sync_o11_provider("panel_1", "lmp", "LMP", False), 'interval', minutes=15)

scheduler.start()

@app.on_event("startup")
def startup_event():
    sync_official_logos()
    sync_schedule()
    sync_disney_schedule(is_manual=False)
    sync_triller_schedule(is_manual=False)
    import threading
    threading.Thread(target=parse_o11_m3u, args=("panel_1",)).start()
    threading.Thread(target=parse_o11_m3u, args=("panel_prime",)).start()

def obtener_url_asignada(league, team_slug, network_override=None):
    if manual_overrides[league].get(team_slug): 
        return manual_overrides[league][team_slug]
    
    info = current_game_info.get(league, {}).get(team_slug, {})
    network = network_override if network_override is not None else info.get("network", "")
    network = network.upper()
    data = STREAM_DATABASE[league].get(team_slug, {})
    
    if league == "nfl":
        if "NBCSN" not in network and "NBC" in network or "PEACOCK" in network: return NBC_STREAM
        if "ESPN" in network or "ABC" in network: return ESPN_STREAM
        if "NFL" in network: return NFL_NETWORK_STREAM
        if "CBS" in network: return data.get("CBS")
        if "FOX" in network: return data.get("FOX")
        return data.get("CBS")
        
    elif league == "nba":
        prime_url = data.get("regional", "")
        sling_url = data.get("sling", "")
        
        if "NBCSN" not in network and "NBC" in network: return NBC_STREAM
        if "ABC" in network: return ABC_STREAM
        if "ESPN" in network: return ESPN_STREAM
        if "NBA TV" in network or "NBATV" in network: return NBA_TV_STREAM
        if "TNT" in network: return TNT_STREAM
        
        if "PEACOCK" in network or "PRIME" in network:
            return prime_url if prime_url else sling_url
        
        if nba_settings.get("source") == "sling":
            return sling_url if sling_url else prime_url
        else:
            return prime_url if prime_url else sling_url
            
    elif league == "mlb":
        if "FS1" in network: return FS1_STREAM
        if "TBS" in network: return TBS_STREAM
        if "FOX" in network and "SPORTS" not in network: return FOX_MLB_STREAM
        if "ESPN" in network or "ABC" in network: return ESPN_STREAM
        return data.get("regional", "")
    else:
        if "ESPN" in network or "FOX" in network or "TBS" in network: 
            return data.get("nacional", "")
        return data.get("regional", "")

@app.get("/")
def show_panel(username: str = Depends(verificar_credenciales)):
    html_path = os.path.join(os.path.dirname(__file__), "panel.html")
    try:
        with open(html_path, "r", encoding="utf-8") as f: 
            return HTMLResponse(content=f.read())
    except: 
        return HTMLResponse(content="<h1>Falta panel.html</h1>")

@app.get("/api/system")
def get_system_stats(username: str = Depends(verificar_credenciales)):
    global last_net_io, last_net_time
    current_net_io = psutil.net_io_counters()
    current_time = time.time()
    
    time_diff = current_time - last_net_time
    if time_diff > 0:
        bytes_sent = current_net_io.bytes_sent - last_net_io.bytes_sent
        bytes_recv = current_net_io.bytes_recv - last_net_io.bytes_recv
        tx_mbps = (bytes_sent * 8) / (1024 * 1024 * time_diff)
        rx_mbps = (bytes_recv * 8) / (1024 * 1024 * time_diff)
    else:
        tx_mbps = 0.0
        rx_mbps = 0.0
        
    last_net_io = current_net_io
    last_net_time = current_time
    cpu = psutil.cpu_percent(interval=None)
    ram = psutil.virtual_memory()
    
    return {
        "cpu_percent": cpu, 
        "ram_percent": ram.percent, 
        "ram_used_gb": round(ram.used / (1024**3), 2), 
        "ram_total_gb": round(ram.total / (1024**3), 2), 
        "tx_mbps": round(tx_mbps, 2), 
        "rx_mbps": round(rx_mbps, 2)
    }

@app.get("/api/status")
def get_status(league: str = "nfl", date: str = None, username: str = Depends(verificar_credenciales)):
    if league not in LEAGUES: 
        return {"equipos": []}
        
    target_date = date if date else get_today_str()
    info_temporal = current_game_info.get(league, {})
    
    if date or (league in ESPN_API_URLS and not info_temporal) or (league == "lmp" and not info_temporal):
        temp_mapping = {}
        if league in ESPN_API_URLS:
            try:
                resp = httpx.get(f"{ESPN_API_URLS[league]}?dates={target_date}", timeout=10)
                data = resp.json()
                for event in data.get("events", []):
                    status = event.get("status", {}).get("type", {}).get("state", "pre")
                    competition = event["competitions"][0]
                    broadcast = ""
                    if competition.get("broadcasts"): 
                        broadcast = " ".join(competition["broadcasts"][0].get("names", [])).upper()
                    
                    if league == "nba":
                        broadcast = get_nba_network_display(broadcast)
                    elif not broadcast:
                        broadcast = "LOCAL"
                    
                    for competitor in competition.get("competitors", []):
                        team_data = competitor.get("team", {})
                        slug = get_matched_slug(league, team_data)
                        temp_mapping[slug] = {
                            "network": broadcast, 
                            "match_name": event.get("shortName", ""), 
                            "match_date": event.get("date", ""), 
                            "state": status, 
                            "logo": team_data.get("logo", "")
                        }
                info_temporal = temp_mapping
            except: 
                pass
        elif league == "lmp":
            try:
                lmp_date = f"{target_date[:4]}-{target_date[4:6]}-{target_date[6:]}"
                
                lmp_url = f"https://api.lmp.mx/3.0.0/scoreboard?date={lmp_date}&days_next=2&days_prev=1"
                lmp_headers = {
                    "accept": "application/json, text/plain, */*",
                    "accept-language": "es-MX,es;q=0.9,en-US;q=0.8",
                    "origin": "https://www.lmp.mx",
                    "referer": "https://www.lmp.mx/",
                    "user-agent": "Mozilla/5.0"
                }
                resp = httpx.get(lmp_url, headers=lmp_headers, timeout=10)
                data = resp.json()
                
                all_games = []
                resp_data = data.get("response", {})
                
                if "games" in resp_data and isinstance(resp_data["games"], list): 
                    all_games.extend(resp_data["games"])
                    
                if "prevGames" in resp_data and isinstance(resp_data["prevGames"], list): 
                    all_games.extend(resp_data["prevGames"])
                if "nextGames" in resp_data and isinstance(resp_data["nextGames"], list): 
                    all_games.extend(resp_data["nextGames"])
                
                tz = pytz.timezone("America/Tijuana")
                
                for game in all_games:
                    status_code = game.get("gameStatus", 0)
                    state = "pre"
                    if status_code == 1: state = "in"
                    elif status_code == 2: state = "post"
                    
                    local_date_str = game.get("localFullDate", "")
                    
                    if lmp_date not in local_date_str and state != "in":
                        continue

                    game_date_iso = local_date_str
                    
                    if local_date_str:
                        try:
                            dt = datetime.strptime(local_date_str, "%Y-%m-%d %H:%M:%S")
                            dt_aware = tz.localize(dt)
                            game_date_iso = dt_aware.isoformat()
                            now = datetime.now(tz)
                            if state == "pre" and now >= dt_aware: state = "in"
                        except: pass
                    
                    away_name = game.get("awayTeamShort", "Away")
                    home_name = game.get("homeTeamShort", "Home")
                    match_name = f"{away_name} vs {home_name}"
                    
                    away_clean = normalize_text(away_name)
                    home_clean = normalize_text(home_name)
                    
                    for slug, db_data in STREAM_DATABASE["lmp"].items():
                        o11_names = [normalize_text(n) for n in db_data.get("o11_name", "").split("|")]
                        if any(name and (name in away_clean or name in home_clean) for name in o11_names):
                            temp_mapping[slug] = {
                                "network": "LMP TV",
                                "match_name": match_name,
                                "match_date": game_date_iso,
                                "state": state,
                                "logo": "" 
                            }
                info_temporal = temp_mapping
            except:
                pass
        
    equipos_data = []
    for slug, data in STREAM_DATABASE[league].items():
        base_slug = data.get("base_slug", slug)
        info = info_temporal.get(base_slug, {})
        network_val = info.get("network", "")
        
        if league == "nba":
            network_val = get_nba_network_display(network_val)
        elif not network_val and info.get("match_date"):
            network_val = "LOCAL"
            
        logo_api = info.get("logo", "")
        logo_cache = OFFICIAL_LOGOS.get(league, {}).get(base_slug, "")
        logo_dynamic = data.get("dynamic_logo", "")
        logo_fallback = data.get('custom_logo') or f"https://a.espncdn.com/i/teamlogos/{league}/500/{data.get('abbr', slug)}.png"
        
        if base_slug in ["mls360", "mlsmlswrapup", "mlsmlscountdown", "mlsmlslaprevia"]: 
            logo_final = "https://a.espncdn.com/i/teamlogos/leagues/500/mls.png"
        elif league in ["ppv", "lmp", "triller"]: 
            logo_final = logo_api or logo_dynamic or logo_cache or logo_fallback
        else: 
            logo_final = logo_api or logo_cache or logo_fallback
        
        equipos_data.append({
            "slug": slug, 
            "name": get_team_display_name(league, slug, data),
            "logo": logo_final, 
            "assigned_url": obtener_url_asignada(league, slug, network_val), 
            "manual_url": manual_overrides[league].get(slug, ""), 
            "viewers": len(active_viewers[league].get(slug, {})),
            "network": network_val, 
            "match_name": info.get("match_name", ""), 
            "match_date": info.get("match_date", ""), 
            "state": info.get("state", "pre")
        })
    return {"equipos": equipos_data}

@app.get("/api/nba_source")
def get_nba_source(username: str = Depends(verificar_credenciales)):
    return nba_settings

@app.post("/api/nba_source")
def set_nba_source(data: NBASourceModel, username: str = Depends(verificar_credenciales)):
    nba_settings["source"] = data.source
    return {"status": "ok"}

@app.get("/api/monitor")
def get_monitor(username: str = Depends(verificar_credenciales)):
    flat_viewers = {}
    for league, teams in active_viewers.items():
        for team, ips in teams.items(): 
            flat_viewers[f"{team.upper()} ({league.upper()})"] = ips
    return flat_viewers

@app.get("/api/security")
def get_sec(username: str = Depends(verificar_credenciales)): 
    return security_config

@app.post("/api/security")
def update_sec(data: SecurityModel, username: str = Depends(verificar_credenciales)):
    global security_config
    security_config["enabled"] = data.enabled
    security_config["ips"] = [ip.strip() for ip in data.ips if ip.strip()]
    guardar_seguridad(security_config)
    return {"status": "ok"}

@app.post("/api/override")
def set_override(data: OverrideModel, username: str = Depends(verificar_credenciales)):
    league = data.league if data.league in LEAGUES else "nfl"
    if data.url.strip(): 
        manual_overrides[league][data.team] = data.url.strip()
    else: 
        manual_overrides[league].pop(data.team, None)
    return {"status": "ok"}

@app.get("/api/check")
async def check_stream(url: str, username: str = Depends(verificar_credenciales)):
    try:
        async with httpx.AsyncClient(verify=False) as client:
            resp = await client.get(url, timeout=7.0)
            if resp.status_code == 200: 
                return {"status": "online"}
            return {"status": "offline"}
    except: 
        return {"status": "offline"}

@app.get("/api/force_sync_o11/{provider_id}")
def force_sync(provider_id: str, username: str = Depends(verificar_credenciales)):
    import threading
    if provider_id == "mls":
        threading.Thread(target=sync_o11_provider, args=("panel_1", "mls", "MLS Apple TV", True)).start()
        return {"status": "Iniciando MLS..."}
    elif provider_id == "espnmx":
        threading.Thread(target=sync_o11_provider, args=("panel_1", "espnmx", "PPV / Disney+", True)).start()
        return {"status": "Iniciando Disney+..."}
    elif provider_id == "primemx":
        threading.Thread(target=sync_o11_provider, args=("panel_prime", "primemx", "PPV / Prime Video", True)).start()
        return {"status": "Iniciando Prime Video..."}
    elif provider_id == "lmp":
        threading.Thread(target=sync_o11_provider, args=("panel_1", "lmp", "LMP", True)).start()
        return {"status": "Iniciando LMP..."}
    elif provider_id == "trillertv":
        threading.Thread(target=sync_o11_provider, args=("panel_triller", "trillertv", "PPV / Triller TV", True)).start()
        return {"status": "Iniciando Triller TV..."}
    return {"status": "Panel desconocido."}

@app.get("/download/{league}_playlist.m3u")
def download_playlist(league: str, request: Request, username: str = Depends(verificar_credenciales)):
    if league not in LEAGUES and league != "all": 
        raise HTTPException(status_code=404)
    host = request.headers.get("host", "51.81.243.2:8005")
    lines = ["#EXTM3U"]
    target_leagues = LEAGUES if league == "all" else [league]
    
    for l in target_leagues:
        for slug, data in STREAM_DATABASE[l].items():
            if l in ["ppv", "lmp", "triller"]: 
                logo_url = data.get('dynamic_logo') or data.get('custom_logo', "https://a.espncdn.com/i/teamlogos/leagues/500/espn.png")
            else: 
                logo_url = f"https://a.espncdn.com/i/teamlogos/{l}/500/{data.get('abbr', slug)}.png"
            
            display_name = get_team_display_name(l, slug, data)
            lines.append(f'#EXTINF:-1 tvg-id="{slug}" tvg-logo="{logo_url}" group-title="{l.upper()} VIP",{display_name}')
            lines.append(f"http://{host}/live/{l}/{slug}.m3u8")
            
    filename = "Master_Hermes_Nexus.m3u" if league == "all" else f"{league}_playlist.m3u"
    return PlainTextResponse("\n".join(lines), headers={"Content-Disposition": f"attachment; filename={filename}"})

@app.get("/live/{league}/{team}.m3u8")
@app.get("/live/{league}/{team}.ts")
async def resolve_team_stream(league: str, team: str, request: Request):
    client_ip = request.client.host
    if security_config.get("enabled") and client_ip not in security_config.get("ips", []): 
        raise HTTPException(status_code=403, detail="Acceso Denegado: IP no autorizada en Whitelist")
    if league not in LEAGUES: 
        raise HTTPException(status_code=404, detail="Liga no soportada")
    
    team_slug = team.lower().replace("-", "").strip()
    if team_slug not in STREAM_DATABASE[league]: 
        raise HTTPException(status_code=404, detail="Equipo no encontrado")
    
    user_agent = request.headers.get("user-agent", "Desconocido")
    if team_slug not in active_viewers[league]: 
        active_viewers[league][team_slug] = {}
    active_viewers[league][team_slug][client_ip] = {"time": time.time(), "user_agent": user_agent}
    
    target = obtener_url_asignada(league, team_slug)
    if not target: 
        target = STREAM_DATABASE[league][team_slug].get("regional", "")
    return RedirectResponse(url=target, status_code=302)

@app.get("/live/{team}.m3u8")
@app.get("/live/{team}.ts")
async def resolve_nfl_legacy_stream(team: str, request: Request): 
    return await resolve_team_stream("nfl", team, request)

if __name__ == "__main__": 
    uvicorn.run(app, host="0.0.0.0", port=8005)