#!/usr/bin/env python3
"""
====================================================================
 DAZN - Local HTTPS Proxy (aiohttp)
 Proxy locale ad alte prestazioni:
 - MPD caldo in RAM (~2s) solo sui canali in play
 - Token CDN sticky: stesso edge fino a scadenza/403 (niente swap ogni 200s)
 - Live delay 8s, 404 restano 404 (niente 50202 sul player)
 - Segmenti in streaming + retry su timeout/401
 - Patch ClearKey per VLC, mpv, Kodi, TiviMate
====================================================================
"""
import os
import sys
import json
import time
import base64
import urllib.parse
import socket
import ssl
import subprocess
import asyncio
import re
from datetime import datetime
from typing import Optional, Dict, List, Tuple, Any

# Token Fastly/Edge durano ore; Akamai ~5 min. Il rinnovo fisso a 200s
# cambiava CDN a metà live (path diversi) e il player prendeva 404→502.
MPD_TTL_S = 2.0
LIVE_DELAY = "PT8.0S"
TOKEN_REFRESH_MARGIN_S = 90.0
MAX_REFRESH_AGE_S = 25 * 60
CDN_PREF = [
    "dca-deit-live", "dcb-deit-live", "dca-de-live", "dcb-de-live", "dcg-de-live",
    "deit", "de", "dca-fs-live", "dcb-fs-live", "dcs-fs-live", "dct-fs-live", "fs",
    "dca-ac-live", "dcb-ac-live", "dcs-ac-live", "dct-ac-live", "ac",
    "dcb-ak-live", "dcs-ak-live", "dct-ak-live", "ak",
]

try:
    import aiohttp
    from aiohttp import web
except ImportError:
    print("ERRORE: aiohttp non è installato. Esegui: pip install aiohttp", flush=True)
    sys.exit(1)

# Colori ANSI per log
GREEN   = '\033[32m'
YELLOW  = '\033[33m'
RED     = '\033[31m'
CYAN    = '\033[36m'
MAGENTA = '\033[35m'
RESET   = '\033[0m'

def log(msg: str, color: str = RESET) -> None:
    print(f"{color}[{time.strftime('%H:%M:%S')}] [PROXY] {msg}{RESET}", flush=True)

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
    sys.path.insert(0, SCRIPT_DIR)

CHANNELS_FILE = os.path.join(SCRIPT_DIR, "dazn_proxy_data.json")
CONFIG_FILE   = os.path.join(SCRIPT_DIR, "dazn_config.json")

# Import funzioni chiave da dazn.py per il rinnovo dinamico
try:
    import dazn
except Exception as e:
    dazn = None
    log(f"Attenzione import dazn: {e}", YELLOW)

# Stato globale dei canali e cache in memoria
# channel_id -> dict con stato, token cdn, manifest in cache, lock
CHANNEL_STATES = {}
_channels_mtime = 0

def load_channels_from_disk():
    global _channels_mtime
    if os.path.exists(CHANNELS_FILE):
        try:
            mtime = os.path.getmtime(CHANNELS_FILE)
            if mtime > _channels_mtime:
                with open(CHANNELS_FILE, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    channels = data.get("channels", [])
                    for ch in channels:
                        cid = ch.get("id")
                        if not cid:
                            continue
                        if ch.get("source") == "seriea_radio":
                            continue
                        if cid not in CHANNEL_STATES:
                            # Calcola età iniziale dall'ora di estrazione
                            ext_at = ch.get("extracted_at")
                            initial_age = 999.0
                            if ext_at:
                                try:
                                    dt = datetime.fromisoformat(ext_at)
                                    initial_age = (datetime.now() - dt).total_seconds()
                                except Exception:
                                    pass

                            parsed_base = urllib.parse.urlparse(ch.get("base_manifest_url", ""))
                            base_path = parsed_base.path.rsplit('/', 1)[0] if parsed_base.path else ""
                            base_cdn = f"{parsed_base.scheme}://{parsed_base.netloc}{base_path}/" if parsed_base.netloc else ""
                            
                            CHANNEL_STATES[cid] = {
                                "id": cid,
                                "name": ch.get("name", "DAZN Channel"),
                                "raw_channel": ch,
                                "base_manifest_url": ch.get("base_manifest_url", ""),
                                "base_cdn_url": base_cdn,
                                "base_query": parsed_base.query,
                                "cdn_token": ch.get("cdn_token", {}),
                                "cdn_name": ch.get("cdn_name", ""),
                                "source": ch.get("source", ""),
                                "key_string": ch.get("key_string", ""),
                                "last_cdn_refresh": time.time() - max(0.0, initial_age),
                                "cached_mpd": None,
                                "cached_mpd_time": 0.0,
                                "last_sync_log": 0.0,
                                "last_requested": 0.0,
                                "cdn_alternates": [],
                                "_refresh_task": None,
                                "lock": asyncio.Lock()
                            }
                        else:
                            # Aggiorna dati se cambiati
                            state = CHANNEL_STATES[cid]
                            state["name"] = ch.get("name", state["name"])
                            state["key_string"] = ch.get("key_string", state["key_string"])
                            state["source"] = ch.get("source", state.get("source", ""))
                            state["raw_channel"] = ch
                _channels_mtime = mtime
        except Exception as e:
            log(f"Errore lettura {CHANNELS_FILE}: {e}", RED)
    return CHANNEL_STATES

_headers_cache = {}
_headers_mtime = 0

def load_headers():
    global _headers_cache, _headers_mtime
    default_headers = {
        "Origin": "https://www.dazn.com",
        "Referer": "https://www.dazn.com/",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0",
        "sec-ch-ua": '"Chromium";v="154", "Microsoft Edge";v="154", "Not A(Brand";v="99"',
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": '"Windows"'
    }
    if os.path.exists(CONFIG_FILE):
        try:
            mtime = os.path.getmtime(CONFIG_FILE)
            if mtime > _headers_mtime:
                with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                    cfg = json.load(f)
                    h = cfg.get("dazn", {}).get("headers", {})
                    for k, v in h.items():
                        default_headers[k] = v
                _headers_cache = default_headers
                _headers_mtime = mtime
        except Exception:
            pass
    return _headers_cache if _headers_cache else default_headers


def _token_ttl_seconds(cdn_token: Any) -> float:
    """Secondi rimanenti del token CDN. Akamai exp=unix, JWT exp, tend:unix."""
    if not isinstance(cdn_token, dict):
        return 120.0
    val = str(cdn_token.get("Value") or "")
    now = time.time()
    m = re.search(r'(?:^|[~&])exp=(\d+)', val)
    if m and not val.startswith("eyJ"):
        return max(15.0, int(m.group(1)) - now)
    if val.startswith("eyJ"):
        try:
            pad = val.split(".")[1] + "=" * ((4 - len(val.split(".")[1]) % 4) % 4)
            payload = json.loads(base64.urlsafe_b64decode(pad))
            exp = float(payload.get("exp") or 0)
            if exp:
                return max(30.0, exp - now)
        except Exception:
            pass
    m = re.search(r'tend:(\d+)', val)
    if m:
        return max(30.0, int(m.group(1)) - now)
    exp_s = cdn_token.get("ExpirySeconds")
    if isinstance(exp_s, (int, float)) and exp_s > 0:
        return float(min(exp_s, MAX_REFRESH_AGE_S))
    return 300.0


def _needs_cdn_refresh(state: dict, force: bool = False) -> bool:
    if force:
        return True
    if not (state.get("cdn_token") or {}).get("Value"):
        return True
    if not state.get("base_cdn_url"):
        return True
    ttl = _token_ttl_seconds(state.get("cdn_token") or {})
    age = time.time() - float(state.get("last_cdn_refresh") or 0)
    if (state.get("source") or (state.get("raw_channel") or {}).get("source")) == "seriea_radio":
        return ttl < 15.0
    return ttl < TOKEN_REFRESH_MARGIN_S or age >= MAX_REFRESH_AGE_S


def _detail_to_pack(detail: dict) -> dict:
    raw_manifest_url = detail.get("ManifestUrl", "")
    cdn_token = detail.get("CdnToken", {}) or {}
    parsed_base = urllib.parse.urlparse(raw_manifest_url)
    base_path = parsed_base.path.rsplit("/", 1)[0] if parsed_base.path else ""
    return {
        "base_manifest_url": raw_manifest_url,
        "base_cdn_url": f"{parsed_base.scheme}://{parsed_base.netloc}{base_path}/" if parsed_base.netloc else "",
        "base_query": parsed_base.query,
        "cdn_token": cdn_token,
        "cdn_name": detail.get("CdnName", ""),
    }


def _pick_detail(details: List[dict], preferred: str = "") -> dict:
    if preferred:
        pref = preferred.lower()
        for d in details:
            if d.get("CdnName", "").lower() == pref:
                return d
    for name in CDN_PREF:
        for d in details:
            if name in d.get("CdnName", "").lower():
                return d
    return details[0]


# ── Rinnovo Dinamico del CDN e del Token (solo a scadenza o su 401) ─
_proxy_alt_tokens: Dict[str, Tuple[str, Dict[str, Any]]] = {}

def _load_accounts() -> List[str]:
    accounts_file = os.path.join(SCRIPT_DIR, 'accounts.txt')
    if os.path.exists(accounts_file):
        try:
            with open(accounts_file, 'r', encoding='utf-8') as f:
                return [l.strip() for l in f if ':' in l and not l.startswith('#')]
        except Exception:
            pass
    return []

def _get_account_token(email: str, password: str, proxy_dict: Optional[Dict[str, str]] = None) -> Tuple[Optional[str], Dict[str, Any]]:
    """Restituisce il token per l'account specificato, riutilizzando la cache in memoria se valida"""
    global _proxy_alt_tokens
    if email in _proxy_alt_tokens:
        tok, payload = _proxy_alt_tokens[email]
        if tok:
            val, rem, _ = dazn.check_token_status(tok)
            if val:
                return tok, payload

    status, new_token = dazn.dazn_login(email, password, proxy_dict)
    if status == "SUCCESS" and new_token:
        _, _, payload = dazn.check_token_status(new_token)
        _proxy_alt_tokens[email] = (new_token, payload)
        return new_token, payload
    return None, {}

def _refresh_cdn_sync(channel_id: str, channel_name: str, raw_channel: dict, preferred_cdn: str = "") -> dict:
    """Esegue la chiamata Playback alle API DAZN per ottenere il nuovo CDN e token con fallback multi-account"""
    if not dazn:
        raise RuntimeError("Modulo dazn non disponibile per il rinnovo")

    config = dazn.load_config()
    proxy_dict = dazn.proxy_manager.get_dict()

    # Prepara candidati: prima token principale, poi account alternativi da accounts.txt
    accounts = _load_accounts()
    candidates = []
    main_token = dazn.get_token(config)
    if main_token:
        m_payload = dazn.decode_jwt_payload(main_token)
        candidates.append(("Account Principale", main_token, m_payload))

    for acct_line in accounts:
        em, pw = acct_line.split(':', 1)
        candidates.append((em, pw, None))

    playback_data = None
    last_err = "Nessun account disponibile"
    account_used = "N/A"

    for label, token_or_pw, payload_cand in candidates:
        if payload_cand is not None:
            token = token_or_pw
            tokens_payload = payload_cand
        else:
            token, tokens_payload = _get_account_token(label, token_or_pw, proxy_dict)
            if not token:
                continue

        viewer_id = tokens_payload.get('viewerId', '') if tokens_payload else ''
        playback_url = dazn.build_playback_url(raw_channel, config, viewer_id)
        p_data, err = dazn.request_playback_info(playback_url, token, config, proxy_dict)

        if err:
            last_err = err
            if "Concurrency" in str(err):
                log(f"⏳ [{channel_name}] Concurrency su {label}, provo account successivo...", YELLOW)
                continue
            else:
                log(f"⚠️ [{channel_name}] Errore Playback su {label}: {err}", YELLOW)
                continue

        if p_data and p_data.get("PlaybackDetails"):
            playback_data = p_data
            account_used = label
            break

    if not playback_data:
        raise RuntimeError(f"Errore Playback DAZN: {last_err}")

    details_list = playback_data.get("PlaybackDetails", [])
    if not details_list:
        raise RuntimeError("Nessun PlaybackDetails disponibile nella risposta DAZN")

    chosen_detail = _pick_detail(details_list, preferred_cdn)
    pack = _detail_to_pack(chosen_detail)
    raw_manifest_url = pack["base_manifest_url"]
    cdn_token = pack["cdn_token"]
    cdn_name = pack["cdn_name"]
    base_cdn_url = pack["base_cdn_url"]
    parsed_base = urllib.parse.urlparse(raw_manifest_url)
    alternates = [_detail_to_pack(d) for d in details_list if d is not chosen_detail]

    # Aggiorna anche dazn_channels.json / dazn_proxy_data.json su disco se esiste
    try:
        if os.path.exists(CHANNELS_FILE):
            with open(CHANNELS_FILE, 'r', encoding='utf-8') as f:
                data = json.load(f)
            updated = False
            for ch in data.get("channels", []):
                if ch.get("id") == channel_id:
                    ch["base_manifest_url"] = raw_manifest_url
                    ch["cdn_token"] = cdn_token
                    ch["cdn_name"] = cdn_name
                    sep = '&' if '?' in raw_manifest_url else '?'
                    t_name = cdn_token.get("Name", "dazn-token")
                    t_val = cdn_token.get("Value", "")
                    ch["manifest_url"] = f"{raw_manifest_url}{sep}{t_name}={t_val}"
                    ch["extracted_at"] = datetime.now().isoformat()
                    updated = True
                    break
            if updated:
                with open(CHANNELS_FILE, 'w', encoding='utf-8') as f:
                    json.dump(data, f, indent=2, ensure_ascii=False)
    except Exception as e:
        log(f"Avviso salvataggio {CHANNELS_FILE}: {e}", YELLOW)

    return {
        "base_manifest_url": raw_manifest_url,
        "base_cdn_url": base_cdn_url,
        "base_query": parsed_base.query,
        "cdn_token": cdn_token,
        "cdn_name": cdn_name,
        "account_used": account_used,
        "alternates": alternates,
        "cdn_changed": bool(preferred_cdn) and preferred_cdn.lower() != (cdn_name or "").lower(),
    }

async def refresh_channel_cdn(channel_id: str, force: bool = False, wait: bool = False) -> bool:
    """Rinnova il token restando sullo stesso CDN. Non blocca i segmenti se wait=False."""
    state = CHANNEL_STATES.get(channel_id)
    if not state:
        return False
    if not _needs_cdn_refresh(state, force=force):
        return True

    task = state.get("_refresh_task")
    if task and not task.done():
        if wait or force:
            try:
                return bool(await task)
            except Exception:
                return False
        return True

    async def _run() -> bool:
        elapsed = time.time() - float(state.get("last_cdn_refresh") or 0)
        src = state.get("source") or (state.get("raw_channel") or {}).get("source") or ""
        if src == "seriea_radio" and dazn:
            log(f"🔄 [{state['name']}] Rinnovo hdnts Lega (età {int(elapsed)}s)...", CYAN)
            try:
                _url, pack, err = await asyncio.to_thread(dazn.request_seriea_entitlement)
                if err or not pack:
                    log(f"❌ [{state['name']}] Entitlement Lega: {err}", RED)
                    return False
                parsed = urllib.parse.urlparse(pack["base_manifest_url"])
                base_path = parsed.path.rsplit("/", 1)[0] if parsed.path else ""
                state["base_manifest_url"] = pack["base_manifest_url"]
                state["base_cdn_url"] = f"{parsed.scheme}://{parsed.netloc}{base_path}/" if parsed.netloc else ""
                state["base_query"] = pack.get("base_query") or parsed.query
                state["cdn_token"] = pack["cdn_token"]
                state["cdn_name"] = "legaseriea-netrw"
                state["source"] = "seriea_radio"
                state["last_cdn_refresh"] = time.time()
                log(f"🔑 [{state['name']}] hdnts Lega rinnovato (~40s)", GREEN)
                return True
            except Exception as e:
                log(f"❌ [{state['name']}] Entitlement Lega: {e}", RED)
                return False
            finally:
                state["_refresh_task"] = None

        preferred = state.get("cdn_name") or ""
        log(f"🔄 [{state['name']}] Rinnovo token (CDN sticky={preferred or 'auto'}, età {int(elapsed)}s)...", CYAN)
        try:
            res = await asyncio.to_thread(
                _refresh_cdn_sync, channel_id, state["name"], state.get("raw_channel", {}), preferred
            )
            old_cdn = state.get("base_cdn_url")
            state["base_manifest_url"] = res["base_manifest_url"]
            state["base_cdn_url"] = res["base_cdn_url"]
            state["base_query"] = res["base_query"]
            state["cdn_token"] = res["cdn_token"]
            state["cdn_name"] = res["cdn_name"]
            state["cdn_alternates"] = res.get("alternates") or []
            state["last_cdn_refresh"] = time.time()
            if old_cdn and old_cdn != res["base_cdn_url"]:
                state["cached_mpd"] = None
                state["cached_mpd_time"] = 0.0
                log(f"⚠️ [{state['name']}] CDN cambiato {old_cdn} → {res['base_cdn_url']}", YELLOW)
            ttl = int(_token_ttl_seconds(res["cdn_token"]))
            acct_str = f" via {res['account_used']}" if res.get("account_used") else ""
            log(f"🔑 [{state['name']}] Token OK{acct_str} · {res['cdn_name']} · valido ~{ttl}s", GREEN)
            return True
        except Exception as e:
            log(f"❌ [{state['name']}] Errore rinnovo CDN: {e}", RED)
            return False
        finally:
            state["_refresh_task"] = None

    state["_refresh_task"] = asyncio.create_task(_run())
    if wait or force:
        return bool(await state["_refresh_task"])
    return True

# ── Patch del Manifest MPD per Live Edge & ClearKey ─────────────────
def patch_mpd_content(mpd_text: str) -> bytes:
    """
    Rimuove Widevine/PlayReady, inietta ClearKey per player standard (VLC, mpv, Kodi, TiviMate),
    rimuove BaseURL per instradare i segmenti sul proxy locale e ottimizza i parametri di buffer
    per garantire fluidità assoluta senza lag né buffering.
    """
    # 1. Rimuovi protezioni proprietarie e inserisci ClearKey
    mpd_text = re.sub(r'<ContentProtection[^>]*schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"[^>]*>.*?</ContentProtection>', '', mpd_text, flags=re.DOTALL)
    mpd_text = re.sub(r'<ContentProtection[^>]*schemeIdUri="urn:microsoft:playready"[^>]*>.*?</ContentProtection>', '', mpd_text, flags=re.DOTALL)
    mpd_text = re.sub(r'<ContentProtection[^>]*schemeIdUri="urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95"[^>]*>.*?</ContentProtection>', '', mpd_text, flags=re.DOTALL)

    clearkey_cp = '<ContentProtection schemeIdUri="urn:uuid:e2719d58-a985-b3c9-781a-214f90116bb0" />'
    mpd_text = re.sub(r'(<ContentProtection[^>]*value="cenc"[^>]*/>)', r'\1' + clearkey_cp, mpd_text)

    # 2. Rimuovi qualsiasi BaseURL per far passare i segmenti attraverso il proxy
    mpd_text = re.sub(r'<BaseURL>.*?</BaseURL>', '', mpd_text, flags=re.DOTALL)

    # 3. 8s di delay: 6s chiedeva segmenti non ancora sul CDN → 404 → 50202 sul player
    if 'suggestedPresentationDelay=' in mpd_text:
        mpd_text = re.sub(r'suggestedPresentationDelay="PT[^"]+"', f'suggestedPresentationDelay="{LIVE_DELAY}"', mpd_text)
    else:
        mpd_text = re.sub(r'(<MPD\b[^>]*)', rf'\1 suggestedPresentationDelay="{LIVE_DELAY}"', mpd_text, count=1)
    if 'minimumUpdatePeriod=' in mpd_text:
        mpd_text = re.sub(r'minimumUpdatePeriod="PT[^"]+"', 'minimumUpdatePeriod="PT2.0S"', mpd_text)
    else:
        mpd_text = re.sub(r'(<MPD\b[^>]*)', r'\1 minimumUpdatePeriod="PT2.0S"', mpd_text, count=1)

    return mpd_text.encode('utf-8')

# ── Client HTTP Asincrono Pooling ──────────────────────────────────
HTTP_SESSION: Optional[aiohttp.ClientSession] = None

async def get_http_session() -> aiohttp.ClientSession:
    global HTTP_SESSION
    if HTTP_SESSION is None or HTTP_SESSION.closed:
        connector = aiohttp.TCPConnector(
            limit=200,
            limit_per_host=32,
            keepalive_timeout=120,
            ttl_dns_cache=300,
            enable_cleanup_closed=True,
        )
        timeout = aiohttp.ClientTimeout(total=None, connect=4, sock_connect=4, sock_read=12)
        HTTP_SESSION = aiohttp.ClientSession(connector=connector, timeout=timeout)
    return HTTP_SESSION

async def download_and_cache_mpd(channel_id: str) -> bool:
    """Scarica il manifesto MPD on-demand dal CDN DAZN e lo salva in cache locale"""
    state = CHANNEL_STATES.get(channel_id)
    if not state:
        return False

    if _needs_cdn_refresh(state):
        await refresh_channel_cdn(channel_id, wait=True)

    base_manifest_url = state.get("base_manifest_url")
    cdn_token = state.get("cdn_token", {})
    t_name = cdn_token.get("Name", "dazn-token")
    t_val = cdn_token.get("Value", "")

    if not base_manifest_url or not t_val:
        return False

    sep = '&' if '?' in base_manifest_url else '?'
    target_url = f"{base_manifest_url}{sep}{t_name}={t_val}"

    if (state.get("source") or "") == "seriea_radio":
        headers = {
            "Origin": "https://www.legaseriea.it",
            "Referer": "https://www.legaseriea.it/",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
        }
    else:
        headers = load_headers().copy()
    headers["Cookie"] = f"{t_name}={t_val}"
    if 'Host' in headers:
        del headers['Host']

    session = await get_http_session()
    try:
        async with session.get(target_url, headers=headers) as resp:
            if resp.status in (401, 403):
                log(f"⚠️ [{state['name']}] CDN ha restituito {resp.status} su MPD. Rinnovo token...", YELLOW)
                ok = await refresh_channel_cdn(channel_id, force=True)
                if ok:
                    cdn_token = state.get("cdn_token", {})
                    t_name = cdn_token.get("Name", "dazn-token")
                    t_val = cdn_token.get("Value", "")
                    sep = '&' if '?' in state["base_manifest_url"] else '?'
                    target_url = f"{state['base_manifest_url']}{sep}{t_name}={t_val}"
                    headers["Cookie"] = f"{t_name}={t_val}"
                    async with session.get(target_url, headers=headers) as retry_resp:
                        if retry_resp.status == 200:
                            raw_text = await retry_resp.text(errors='ignore')
                            state["cached_mpd"] = patch_mpd_content(raw_text)
                            state["cached_mpd_time"] = time.time()
                            return True
                return False

            if resp.status == 200:
                raw_text = await resp.text(errors='ignore')
                state["cached_mpd"] = patch_mpd_content(raw_text)
                state["cached_mpd_time"] = time.time()
                # Log di conferma sincronizzazione ogni 60 secondi
                if (time.time() - state.get("last_sync_log", 0)) > 60.0:
                    log(f"⚡ [{state['name']}] Flusso proxato e sincronizzato in RAM (Live Edge OK)", GREEN)
                    state["last_sync_log"] = time.time()
                return True
            else:
                last_err_time = state.get("last_err_log", 0)
                if (time.time() - last_err_time) > 60.0:
                    log(f"Avviso CDN ({resp.status}) scaricando MPD per {state['name']}", YELLOW)
                    state["last_err_log"] = time.time()
                return False
    except Exception as e:
        last_err_time = state.get("last_err_log", 0)
        if (time.time() - last_err_time) > 60.0:
            log(f"Eccezione scaricando MPD per {state['name']}: {e}", RED)
            state["last_err_log"] = time.time()
        return False

# ── Poller di Background Continuo (On-Demand per Canali Attivi) ──────
async def background_poller():
    """MPD caldo ogni ~2s sui canali attivi. Token solo a scadenza, stesso CDN."""
    log("🚀 Poller on-demand: MPD caldo, token sticky (niente swap CDN ogni 200s)", CYAN)
    await asyncio.sleep(1)
    last_disk_check = 0.0
    while True:
        try:
            now = time.time()
            if (now - last_disk_check) > 10.0:
                load_channels_from_disk()
                last_disk_check = now

            active = [
                cid for cid, state in list(CHANNEL_STATES.items())
                if (now - state.get("last_requested", 0)) <= 60.0
            ]
            if active:
                sem = asyncio.Semaphore(4)

                async def _warm(cid: str):
                    async with sem:
                        st = CHANNEL_STATES.get(cid)
                        if st and _needs_cdn_refresh(st):
                            await refresh_channel_cdn(cid, wait=False)
                        await download_and_cache_mpd(cid)

                await asyncio.gather(*[_warm(cid) for cid in active], return_exceptions=True)

            await asyncio.sleep(2.0)
        except asyncio.CancelledError:
            log("🛑 Poller di background arrestato.", YELLOW)
            break
        except Exception as e:
            log(f"Errore poller background: {e}", RED)
            await asyncio.sleep(3)

# ── Endpoint Playlist & Streaming ───────────────────────────────────
async def handle_playlist(request):
    load_channels_from_disk()
    lines = ["#EXTM3U x-tvg-url=\"\""]

    host = request.host
    if not host:
        host = "addictedbytheproject.nl:8443"

    for cid, state in CHANNEL_STATES.items():
        ch = state.get("raw_channel", {})
        if (state.get("source") or ch.get("source")) == "seriea_radio":
            continue
        if ch.get("status") == "OK" or state.get("key_string"):
            logo = ch.get("logo", "")
            logo_attr = f' tvg-logo="{logo}"' if logo else ""
            is_live = bool(ch.get("epg"))
            if (state.get("source") or ch.get("source")) == "seriea_radio":
                group = "Radio TV Serie A"
            elif is_live:
                group = "DAZN Eventi Live"
            else:
                group = "DAZN Canali"
            lines.append(f'#EXTINF:-1 tvg-id="{cid}" tvg-name="{state["name"]}"{logo_attr} group-title="{group}",{state["name"]}')
            lines.append('#KODIPROP:inputstream=inputstream.adaptive')
            lines.append('#KODIPROP:inputstream.adaptive.manifest_type=mpd')
            lines.append('#KODIPROP:inputstream.adaptive.license_type=clearkey')
            lines.append(f'#KODIPROP:inputstream.adaptive.license_key={state.get("key_string")}')
            lines.append(f'https://{host}/play/{cid}/stream.mpd')

    body = ("\n".join(lines) + "\n")
    return web.Response(text=body, content_type='audio/mpegurl', headers={'Access-Control-Allow-Origin': '*'})

async def handle_play(request):
    channel_id = request.match_info.get('id')
    filename = request.match_info.get('filename')

    if not channel_id or not filename:
        raise web.HTTPBadRequest(text="Bad Request")

    state = CHANNEL_STATES.get(channel_id)
    if not state:
        load_channels_from_disk()
        state = CHANNEL_STATES.get(channel_id)

    if not state:
        raise web.HTTPNotFound(text="Channel Not Found")

    state["last_requested"] = time.time()
    is_mpd = filename.startswith("stream.mpd")
    cors = {'Access-Control-Allow-Origin': '*'}

    if is_mpd:
        cached_bytes = state.get("cached_mpd")
        age = time.time() - float(state.get("cached_mpd_time") or 0)
        if not cached_bytes or age > MPD_TTL_S:
            await download_and_cache_mpd(channel_id)
            cached_bytes = state.get("cached_mpd")
        if not cached_bytes:
            raise web.HTTPBadGateway(text="Impossibile ottenere MPD dal CDN")
        return web.Response(
            body=cached_bytes,
            status=200,
            content_type='application/dash+xml',
            headers={
                **cors,
                'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
                'Pragma': 'no-cache',
                'Expires': '0',
            },
        )

    # Segmenti: mai 502 su 404 (ExoPlayer muore con 50202). Stream, non buffer.
    if _needs_cdn_refresh(state):
        asyncio.create_task(refresh_channel_cdn(channel_id, wait=False))

    clean_filename = filename.split('?')[0]
    session = await get_http_session()

    def _build_target() -> Tuple[str, Dict[str, str], str, str]:
        cdn_token = state.get("cdn_token") or {}
        t_name = cdn_token.get("Name", "dazn-token")
        t_val = cdn_token.get("Value", "")
        base_cdn_url = state.get("base_cdn_url") or ""
        parts = []
        base_query = state.get("base_query") or ""
        if base_query:
            for qp in base_query.split("&"):
                if qp and not qp.startswith(f"{t_name}="):
                    parts.append(qp)
        if t_val:
            parts.append(f"{t_name}={t_val}")
        url = f"{base_cdn_url}{clean_filename}"
        if parts:
            url += "?" + "&".join(parts)
        if (state.get("source") or "") == "seriea_radio":
            hdrs = {
                "Origin": "https://www.legaseriea.it",
                "Referer": "https://www.legaseriea.it/",
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
            }
        else:
            hdrs = load_headers().copy()
        if t_val:
            hdrs["Cookie"] = f"{t_name}={t_val}"
        hdrs.pop("Host", None)
        if "Range" in request.headers:
            hdrs["Range"] = request.headers["Range"]
        return url, hdrs, t_name, t_val

    async def _stream_cdn(cdn_resp) -> web.StreamResponse:
        out = dict(cors)
        if "Content-Type" in cdn_resp.headers:
            out["Content-Type"] = cdn_resp.headers["Content-Type"]
        if "Content-Length" in cdn_resp.headers:
            out["Content-Length"] = cdn_resp.headers["Content-Length"]
        if "Content-Range" in cdn_resp.headers:
            out["Content-Range"] = cdn_resp.headers["Content-Range"]
        if "Accept-Ranges" in cdn_resp.headers:
            out["Accept-Ranges"] = cdn_resp.headers["Accept-Ranges"]
        resp = web.StreamResponse(status=cdn_resp.status, headers=out)
        await resp.prepare(request)
        async for chunk in cdn_resp.content.iter_chunked(64 * 1024):
            await resp.write(chunk)
        await resp.write_eof()
        return resp

    target_url, req_headers, _, t_val = _build_target()
    if not state.get("base_cdn_url") or not t_val:
        raise web.HTTPServiceUnavailable(text="Dati CDN del canale non completi")

    try:
        for attempt in range(2):
            target_url, req_headers, _, _ = _build_target()
            try:
                async with session.get(target_url, headers=req_headers) as cdn_resp:
                    if cdn_resp.status in (401, 403) and attempt == 0:
                        log(f"⚠️ [{state['name']}] CDN {cdn_resp.status} su {clean_filename}, rinnovo sticky...", YELLOW)
                        await refresh_channel_cdn(channel_id, force=True, wait=True)
                        continue
                    if cdn_resp.status == 404:
                        if attempt == 0:
                            await asyncio.sleep(0.2)
                            continue
                        return web.Response(status=404, text="Not Found", headers=cors)
                    if cdn_resp.status not in (200, 206):
                        log(f"CDN {cdn_resp.status} segmento {clean_filename}", YELLOW)
                        return web.Response(status=cdn_resp.status, text=f"CDN {cdn_resp.status}", headers=cors)
                    return await _stream_cdn(cdn_resp)
            except (asyncio.TimeoutError, aiohttp.ClientError) as e:
                if attempt == 0:
                    log(f"Retry segmento {clean_filename}: {e}", YELLOW)
                    await asyncio.sleep(0.15)
                    continue
                log(f"Timeout/CDN down {clean_filename}: {e}", RED)
                return web.Response(status=504, text="CDN timeout", headers=cors)
        return web.Response(status=404, text="Not Found", headers=cors)
    except Exception as e:
        log(f"Proxy Error per {clean_filename}: {e}", RED)
        return web.Response(status=504, text="Proxy error", headers=cors)

# ── Certificati SSL & Setup ─────────────────────────────────────────
def generate_cert():
    cert_path = os.path.join(SCRIPT_DIR, "cert.pem")
    key_path = os.path.join(SCRIPT_DIR, "key.pem")
    if not os.path.exists(cert_path) or not os.path.exists(key_path):
        log("Generazione di un nuovo certificato SSL self-signed via OpenSSL...", YELLOW)
        try:
            subprocess.run(["openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes",
                    "-keyout", key_path, "-out", cert_path, "-days", "365",
                    "-subj", "/CN=localhost",
                    "-addext", "subjectAltName=DNS:addictedbytheproject.nl,DNS:localhost"],
                    check=True)
            log("Certificato SSL generato con successo.", GREEN)
        except Exception as e:
            log(f"Errore durante la generazione del certificato SSL: {e}", RED)
    return cert_path, key_path

async def cleanup_app(app):
    global HTTP_SESSION
    if HTTP_SESSION and not HTTP_SESSION.closed:
        await HTTP_SESSION.close()

async def start_background_tasks(app):
    app['poller'] = asyncio.create_task(background_poller())
    yield
    app['poller'].cancel()
    try:
        await app['poller']
    except asyncio.CancelledError:
        pass

def init_app():
    app = web.Application(client_max_size=16 * 1024 * 1024)
    app.cleanup_ctx.append(start_background_tasks)
    app.router.add_get('/', handle_playlist)
    app.router.add_get('/playlist.m3u', handle_playlist)
    app.router.add_get('/play/{id}/{filename:.*}', handle_play)
    app.on_cleanup.append(cleanup_app)
    return app

if __name__ == '__main__':
    port = 8443
    log(f"Avvio DAZN HTTPS Proxy Server sulla porta {port}...", CYAN)

    # Inizializza i canali in memoria
    load_channels_from_disk()

    cert_path, key_path = generate_cert()
    ssl_context = None
    if os.path.exists(cert_path) and os.path.exists(key_path):
        try:
            ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
            ssl_context.load_cert_chain(cert_path, key_path)
            log("Contesto SSL inizializzato correttamente.", GREEN)
        except Exception as e:
            log(f"Errore inizializzazione SSL: {e}", RED)
            ssl_context = None

    try:
        web.run_app(init_app(), host='0.0.0.0', port=port, ssl_context=ssl_context, print=None)
    except KeyboardInterrupt:
        log("\nArresto Proxy Server...", YELLOW)
        sys.exit(0)
