#!/usr/bin/env python3
"""
====================================================================
 DAZN - Local HTTPS Proxy (aiohttp)
 Proxy locale ad alte prestazioni:
 - Download continuo e polling dell'MPD in background ("sempre attivo")
 - Rinnovo automatico e dinamico del CDN e del token ogni 200 secondi
 - Sincronizzazione Live Edge (ultimi 4 segmenti ~7.6s, nessun ritardo di 1 minuto)
 - Patch automatica ClearKey per player standard (VLC, mpv, Kodi, TiviMate)
 - Auto-retry trasparente in caso di 401/403 dal CDN
====================================================================
"""
import os
import sys
import json
import time
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

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 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", ""),
                                "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,
                                "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["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

# ── Rinnovo Dinamico del CDN e del Token (ogni 200s 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) -> 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")

    # Priorità assoluta a Fastly e 'ac' rispetto ad Akamai
    chosen_detail = None
    for pref in ['dct-fs-live', 'dcs-fs-live', 'dct-ac-live', 'fs', 'ac', 'dct-ak-live', 'dcs-ak-live', 'ak']:
        for d in details_list:
            if pref in d.get('CdnName', '').lower():
                chosen_detail = d
                break
        if chosen_detail:
            break

    if not chosen_detail and details_list:
        chosen_detail = details_list[0]

    raw_manifest_url = chosen_detail.get("ManifestUrl", "")
    cdn_token = chosen_detail.get("CdnToken", {})
    cdn_name = chosen_detail.get("CdnName", "")

    parsed_base = urllib.parse.urlparse(raw_manifest_url)
    base_path = parsed_base.path.rsplit('/', 1)[0]
    base_cdn_url = f"{parsed_base.scheme}://{parsed_base.netloc}{base_path}/"

    # 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
    }

async def refresh_channel_cdn(channel_id: str, force: bool = False) -> bool:
    """Rinnova il CDN token per il canale se sono passati >= 200s o se force=True"""
    state = CHANNEL_STATES.get(channel_id)
    if not state:
        return False

    async with state["lock"]:
        elapsed = time.time() - state["last_cdn_refresh"]
        if not force and elapsed < 200.0:
            return True

        log(f"🔄 [{state['name']}] Rinnovo CDN & Token in corso (trascorsi {int(elapsed)}s)...", CYAN)
        try:
            res = await asyncio.to_thread(_refresh_cdn_sync, channel_id, state["name"], state.get("raw_channel", {}))
            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["last_cdn_refresh"] = time.time()
            acct_str = f" via {res['account_used']}" if res.get("account_used") else ""
            log(f"🔑 [{state['name']}] CDN & Token aggiornati con successo{acct_str}! CDN: {res['cdn_name']} (prossimo cambio tra 200s)", GREEN)
            return True
        except Exception as e:
            log(f"❌ [{state['name']}] Errore durante il rinnovo CDN: {e}", RED)
            return False

# ── 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. Buffer e delay ottimali: 6s di delay live per buffer solido senza lag
    if 'suggestedPresentationDelay=' in mpd_text:
        mpd_text = re.sub(r'suggestedPresentationDelay="PT[^"]+"', 'suggestedPresentationDelay="PT6.0S"', mpd_text)
    else:
        mpd_text = re.sub(r'(<MPD\b[^>]*)', r'\1 suggestedPresentationDelay="PT6.0S"', mpd_text, count=1)

    # Manteniamo la SegmentTimeline integra e completa (senza troncare a 4 segmenti!),
    # così il player ha tutti i 60 secondi di buffer a disposizione ed evita lag e blocchi!

    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=100, keepalive_timeout=60, enable_cleanup_closed=True)
        timeout = aiohttp.ClientTimeout(total=15, connect=5)
        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

    now = time.time()
    if (now - state["last_cdn_refresh"]) >= 200.0:
        await refresh_channel_cdn(channel_id)

    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}"

    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():
    """
    Loop continuo di proxying in memoria per i canali in riproduzione:
    - Sincronizza e mantiene caldi in RAM gli MPD solo per i canali richiesti negli ultimi 60s
    - Rigenera automaticamente i token CDN ogni 190 secondi per i flussi attivi
    - Canali inattivi (nessun player connesso): non effettuano chiamate Playback API DAZN
    """
    log("🚀 Avvio Poller di background on-demand (mantiene caldi e aggiornati i canali attivi)...", CYAN)
    await asyncio.sleep(1) # breve attesa per inizializzazione server

    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

            for cid, state in list(CHANNEL_STATES.items()):
                # Mantieni caldi e aggiornati SOLO i canali attivamente in riproduzione negli ultimi 60s
                if (now - state.get("last_requested", 0)) <= 60.0:
                    # 1. Rinnovo automatico e preventivo del CDN/Token prima dei 200s
                    if (now - state.get("last_cdn_refresh", 0)) >= 190.0:
                        await refresh_channel_cdn(cid)

                    # 2. Download e patch in RAM del manifest MPD per live edge
                    await download_and_cache_mpd(cid)
                    await asyncio.sleep(0.05) # micro pausa anti-spike CPU

            await asyncio.sleep(2.5)
        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 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"))
            group = "DAZN Eventi Live" if is_live else "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")

    # 1. GESTIONE MANIFEST MPD (Restituito immediatamente dalla RAM a latenza zero!)
    if is_mpd:
        cached_bytes = state.get("cached_mpd")
        if not cached_bytes or (time.time() - state.get("cached_mpd_time", 0)) > 4.0:
            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")

        resp = web.Response(
            body=cached_bytes,
            status=200,
            content_type='application/dash+xml',
            headers={
                'Access-Control-Allow-Origin': '*',
                'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
                'Pragma': 'no-cache',
                'Expires': '0'
            }
        )
        return resp

    # 2. GESTIONE SEGMENTI (.m4s) - Streaming ad alte prestazioni con aiohttp
    now = time.time()
    if (now - state["last_cdn_refresh"]) >= 200.0:
        await refresh_channel_cdn(channel_id)

    base_cdn_url = state.get("base_cdn_url")
    cdn_token = state.get("cdn_token", {})
    t_name = cdn_token.get("Name", "dazn-token")
    t_val = cdn_token.get("Value", "")

    if not base_cdn_url or not t_val:
        raise web.HTTPInternalServerError(text="Dati CDN del canale non completi")

    clean_filename = filename.split('?')[0]
    base_query = state.get("base_query", "")

    query_parts = []
    if base_query:
        for qp in base_query.split('&'):
            if qp and not qp.startswith(f"{t_name}="):
                query_parts.append(qp)
    query_parts.append(f"{t_name}={t_val}")
    qs = "&".join(query_parts)

    target_url = f"{base_cdn_url}{clean_filename}?{qs}"

    req_headers = load_headers().copy()
    req_headers["Cookie"] = f"{t_name}={t_val}"
    if 'Host' in req_headers:
        del req_headers['Host']
    if 'Range' in request.headers:
        req_headers['Range'] = request.headers['Range']

    session = await get_http_session()
    try:
        async with session.get(target_url, headers=req_headers) as cdn_resp:
            # Se token scaduto su segmento, rinnova e riprova subito
            if cdn_resp.status in (401, 403):
                log(f"⚠️ [{state['name']}] Errore CDN ({cdn_resp.status}) per {clean_filename}. 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", "")
                    query_parts = [qp for qp in query_parts if not qp.startswith(f"{t_name}=")]
                    query_parts.append(f"{t_name}={t_val}")
                    target_url = f"{state['base_cdn_url']}{clean_filename}?{'&'.join(query_parts)}"
                    req_headers["Cookie"] = f"{t_name}={t_val}"
                    async with session.get(target_url, headers=req_headers) as retry_resp:
                        if retry_resp.status in (200, 206):
                            content = await retry_resp.read()
                            out_headers = {'Access-Control-Allow-Origin': '*'}
                            if 'Content-Type' in retry_resp.headers:
                                out_headers['Content-Type'] = retry_resp.headers['Content-Type']
                            if 'Content-Length' in retry_resp.headers:
                                out_headers['Content-Length'] = retry_resp.headers['Content-Length']
                            if 'Content-Range' in retry_resp.headers:
                                out_headers['Content-Range'] = retry_resp.headers['Content-Range']
                            return web.Response(body=content, status=retry_resp.status, headers=out_headers)

            if cdn_resp.status not in (200, 206):
                log(f"Errore CDN ({cdn_resp.status}) per segmento {clean_filename}", RED)
                raise web.HTTPBadGateway(text=f"CDN Error: {cdn_resp.status}")

            content = await cdn_resp.read()
            out_headers = {'Access-Control-Allow-Origin': '*'}
            if 'Content-Type' in cdn_resp.headers:
                out_headers['Content-Type'] = cdn_resp.headers['Content-Type']
            if 'Content-Length' in cdn_resp.headers:
                out_headers['Content-Length'] = cdn_resp.headers['Content-Length']
            if 'Content-Range' in cdn_resp.headers:
                out_headers['Content-Range'] = cdn_resp.headers['Content-Range']

            return web.Response(body=content, status=cdn_resp.status, headers=out_headers)

    except web.HTTPException:
        raise
    except Exception as e:
        log(f"Proxy Error per {clean_filename}: {e}", RED)
        raise web.HTTPInternalServerError(text="Internal Proxy Error")

# ── 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()
    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)
