"""
DAZN Live Scanner
Cattura frame dal flusso DAZN EPG e usa OCR per estrarre
gli eventi in onda e i canali associati ("Events X").
Si aggiorna ogni minuto.
"""

import sys
import io
import subprocess
import json
import re
import time
import logging
import schedule
from datetime import datetime
from pathlib import Path

# Fix encoding per Windows
if hasattr(sys.stdout, 'reconfigure'):
    sys.stdout.reconfigure(encoding='utf-8')
if hasattr(sys.stderr, 'reconfigure'):
    sys.stderr.reconfigure(encoding='utf-8')

# --- Configurazione ---
STREAM_URL = (
    "https://sell2k25flux.flux-atv2k22.xyz:443/JellyfinLINE/d9DZQGmdwC/26433"
)
OUTPUT_DIR = Path(__file__).parent / "epg_data"
FRAMES_DIR = Path(__file__).parent / "frames"
LOG_FILE = Path(__file__).parent / "dazn_scanner.log"

# Area sidebar "IN ONDA ORA" nel frame 1920x1080
# La sidebar con la lista eventi e' nella parte destra (presa fino in fondo)
SIDEBAR_CROP = (1095, 55, 1920, 1080)  # (left, top, right, bottom)
# Dentro la sidebar, il testo degli eventi parte da x ~ 370 (relativo al crop)
SIDEBAR_TEXT_X_MIN = 350

# --- Logging ---
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(LOG_FILE, encoding="utf-8"),
        logging.StreamHandler(sys.stderr),
    ],
)
logger = logging.getLogger(__name__)

# OCR reader (lazy init)
_ocr_reader = None


def get_ocr_reader():
    """Inizializza il reader OCR (lazy, una sola volta)."""
    global _ocr_reader
    if _ocr_reader is None:
        import easyocr
        logger.info("[OCR] Caricamento modello EasyOCR (prima volta, potrebbe richiedere tempo)...")
        _ocr_reader = easyocr.Reader(["it", "en"], gpu=False)
        logger.info("[OCR] Modello caricato.")
    return _ocr_reader


def capture_frames(output_dir: Path, prefix: str, duration: int = 15, fps: str = "1/3") -> list[Path]:
    """Cattura multipli frame dal flusso per coprire la rotazione della sidebar."""
    try:
        cmd = [
            "ffmpeg", "-y",
            "-t", str(duration),
            "-i", STREAM_URL,
            "-vf", f"fps={fps}",
            "-q:v", "2",
            str(output_dir / f"{prefix}_%03d.jpg"),
        ]
        subprocess.run(cmd, capture_output=True, text=True, timeout=duration + 30)
        
        frames = sorted(output_dir.glob(f"{prefix}_*.jpg"))
        valid_frames = [f for f in frames if f.stat().st_size > 0]
        if valid_frames:
            logger.info(f"[FRAME] Catturati {len(valid_frames)} frame utili per lo scrolling.")
        else:
            logger.error(f"[FRAME] Nessun file frame creato")
        return valid_frames
    except subprocess.TimeoutExpired:
        logger.error("[FRAME] Timeout cattura frame multipli")
        return []
    except Exception as e:
        logger.error(f"[FRAME] Errore: {e}")
        return []


def extract_sidebar(frame_path: Path, sidebar_path: Path) -> bool:
    """Ritaglia la sidebar 'IN ONDA ORA' dal frame."""
    try:
        from PIL import Image
        img = Image.open(frame_path)
        if img.size != (1920, 1080):
            logger.warning(f"[CROP] Risoluzione inattesa: {img.size}, adattamento...")
            # Scala le coordinate proporzionalmente
            sx = img.size[0] / 1920
            sy = img.size[1] / 1080
            crop = (
                int(SIDEBAR_CROP[0] * sx),
                int(SIDEBAR_CROP[1] * sy),
                int(SIDEBAR_CROP[2] * sx),
                int(SIDEBAR_CROP[3] * sy),
            )
        else:
            crop = SIDEBAR_CROP

        sidebar = img.crop(crop)
        sidebar.save(sidebar_path)
        return True
    except Exception as e:
        logger.error(f"[CROP] Errore: {e}")
        return False


def parse_ocr_results(ocr_results: list) -> tuple[list[dict], int]:
    """
    Analizza i risultati OCR e raggruppa per evento.
    Restituisce (lista_eventi, numero_eventi_attesi)
    """
    expected_live_count = 0
    
    sidebar_texts = []
    for bbox, text, conf in ocr_results:
        x_center = (bbox[0][0] + bbox[2][0]) / 2
        y_center = (bbox[0][1] + bbox[2][1]) / 2

        # Cerca numero di eventi live nell'header (es. "15 LIVE" o "15 IN ONDA ORA")
        m = re.search(r'(\d+)\s*(?:live|in onda)', text, re.IGNORECASE)
        if m:
            val = int(m.group(1))
            if val > expected_live_count:
                expected_live_count = val
        else:
            # Fallback: numero da solo nella parte alta (y < 100)
            if y_center < 100 and text.strip().isdigit():
                val = int(text.strip())
                if val > expected_live_count and val < 50: # max ragionevole
                    expected_live_count = val

        # Ignora testi con confidenza troppo bassa
        if conf < 0.2:
            continue

        # Filtra: solo zona testo della sidebar (a destra delle thumbnail)
        if x_center > SIDEBAR_TEXT_X_MIN:
            sidebar_texts.append({
                "text": text.strip(),
                "x": x_center,
                "y": y_center,
                "conf": conf,
            })

    # Ordina per posizione verticale
    sidebar_texts.sort(key=lambda t: t["y"])

    # Ignora l'header "IN ONDA ORA" e "LIVE" (primi 2 testi, y < 60)
    # E ignora testi troppo in basso che potrebbero essere tagliati dallo scrolling
    crop_height = SIDEBAR_CROP[3] - SIDEBAR_CROP[1]
    sidebar_texts = [t for t in sidebar_texts if 60 < t["y"] < (crop_height - 20)]

    # Raggruppa per righe (testi con y simile, entro 15px)
    rows = []
    current_row = []
    last_y = -100

    for t in sidebar_texts:
        if abs(t["y"] - last_y) > 15:
            if current_row:
                rows.append(current_row)
            current_row = [t]
            last_y = t["y"]
        else:
            current_row.append(t)
            # Aggiorna y come media
            last_y = sum(item["y"] for item in current_row) / len(current_row)

    if current_row:
        rows.append(current_row)

    # Unisci i testi di ogni riga
    merged_rows = []
    for row in rows:
        row.sort(key=lambda t: t["x"])  # ordina da sinistra a destra
        full_text = " ".join(t["text"] for t in row)
        avg_y = sum(t["y"] for t in row) / len(row)
        merged_rows.append({"text": full_text, "y": avg_y})

    # Ora raggruppa a coppie: titolo + info
    # Il titolo non contiene "Events", la riga info si'
    events = []
    i = 0
    while i < len(merged_rows):
        row = merged_rows[i]
        text = row["text"]

        # Cerca "Events" nel testo
        events_match = re.search(r"Events\s*(\d+)", text, re.IGNORECASE)

        if events_match:
            # Questa riga contiene "Events X" - e' una riga info
            channel = int(events_match.group(1))

            # Estrai l'orario (formato HH:MM)
            time_match = re.search(r"(\d{1,2}[:\.,]\d{2})", text)
            event_time = time_match.group(1).replace(",", ":").replace(".", ":") if time_match else ""

            # Il titolo e' nella riga precedente (se esiste e non e' gia' stato usato)
            if events and events[-1].get("_needs_info"):
                # Completa l'evento precedente
                events[-1]["time"] = event_time
                events[-1]["channel"] = f"Events {channel}"
                events[-1]["channel_number"] = channel
                del events[-1]["_needs_info"]
            else:
                # Potrebbe essere tutto su una riga o titolo mancante
                # Estrai il titolo rimuovendo orario e Events X
                title = text
                title = re.sub(r"\d{1,2}[:\.,]\d{2}", "", title)
                title = re.sub(r"Events\s*\d+", "", title, flags=re.IGNORECASE)
                title = title.strip(" |,-")

                events.append({
                    "title": title if title else "?",
                    "time": event_time,
                    "channel": f"Events {channel}",
                    "channel_number": channel,
                })
        else:
            # Controlla se la riga inizia con un orario (HH:MM) — è una riga info, non un titolo
            # Es: "20:45 Zona Serie A 1" è l'orario + canale dell'evento precedente
            time_start = re.match(r"^(\d{1,2}[:\.,]\d{2})\s*(.*)", text)
            if time_start:
                event_time = time_start.group(1).replace(",", ":").replace(".", ":")
                remaining = time_start.group(2).strip()

                # Determina il canale dal testo rimanente
                channel_name = ""
                channel_num = 0
                if re.search(r"Zona\s+Serie\s+A", remaining, re.IGNORECASE):
                    channel_name = "ZonaSerieA"
                elif remaining:
                    # Rimuovi eventuali numeri finali spuri (es. "1" alla fine)
                    channel_name = re.sub(r"\s+\d+$", "", remaining).strip()
                    if not channel_name:
                        channel_name = remaining

                if events and events[-1].get("_needs_info"):
                    # Completa l'evento precedente con orario e canale
                    events[-1]["time"] = event_time
                    events[-1]["channel"] = channel_name
                    events[-1]["channel_number"] = channel_num
                    del events[-1]["_needs_info"]
                else:
                    # Riga info senza evento precedente — skip
                    logger.debug(f"[PARSE] Riga info orfana ignorata: {text}")

                i += 1
                continue

            # Riga senza "Events" e senza orario - probabilmente un titolo
            # Salta righe che sono chiaramente non titoli
            if len(text) < 3 or text.upper() in ["IT", "LIVE", "NH"]:
                i += 1
                continue

            events.append({
                "title": text.strip(),
                "_needs_info": True,  # aspetta la riga successiva
            })

        i += 1

    # Pulisci eventi incompleti
    final_events = []
    for ev in events:
        if "_needs_info" in ev:
            # Evento senza riga info - aggiungi comunque senza canale
            del ev["_needs_info"]
            ev.setdefault("time", "")
            ev.setdefault("channel", "")
            ev.setdefault("channel_number", 0)
        final_events.append(ev)

    return final_events, expected_live_count


def scan_live_events() -> list[dict] | None:
    """
    Pipeline completa: cattura frames multipli -> OCR -> parsing eventi uniti.
    Restituisce la lista di eventi o None in caso di errore.
    """
    FRAMES_DIR.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

    # 1. Cattura frame multipli per gestire lo scrolling
    # Usiamo 20 secondi per avere più margine
    logger.info("[SCAN] Avvio scansione eventi live multipli (copertura scrolling)...")
    frames = capture_frames(FRAMES_DIR, f"frame_{timestamp}", duration=20, fps="1/3")
    
    if not frames:
        return None

    reader = get_ocr_reader()
    
    expected_count = 0
    events_by_channel = {}
    events_no_channel = {}

    import difflib

    for i, frame_path in enumerate(frames):
        sidebar_path = FRAMES_DIR / f"sidebar_{timestamp}_{i}.jpg"
        
        # 2. Ritaglia sidebar
        if not extract_sidebar(frame_path, sidebar_path):
            continue

        # 3. OCR
        logger.info(f"[OCR] Analisi frame {i+1}/{len(frames)} in corso...")
        ocr_results = reader.readtext(str(sidebar_path))
        
        # 4. Parsing
        events, frame_expected = parse_ocr_results(ocr_results)
        
        if frame_expected > expected_count:
            expected_count = frame_expected
            logger.info(f"[PARSE] Rilevato contatore eventi in onda: {expected_count}")
        
        for ev in events:
            chan = ev.get('channel_number', 0)
            if chan > 0:
                if chan not in events_by_channel:
                    events_by_channel[chan] = ev
                else:
                    existing = events_by_channel[chan]
                    # Aggiorna orario se mancante
                    if not existing.get('time') and ev.get('time'):
                        existing['time'] = ev.get('time')
                    # Aggiorna titolo se questo è più lungo (meno troncato)
                    if len(ev['title']) > len(existing['title']):
                        existing['title'] = ev['title']
            else:
                title_key = re.sub(r'[^a-z0-9]', '', ev['title'].lower())
                if len(title_key) < 4:
                    continue
                if title_key not in events_no_channel:
                    events_no_channel[title_key] = ev
                else:
                    if len(ev['title']) > len(events_no_channel[title_key]['title']):
                        events_no_channel[title_key]['title'] = ev['title']

        # Early exit: se abbiamo trovato tutti gli eventi attesi con canale
        if expected_count > 0 and len(events_by_channel) >= expected_count:
            logger.info(f"[SCAN] Trovati tutti i {expected_count} eventi previsti! Interrompo l'analisi dei frame successivi.")
            break

    final_events = list(events_by_channel.values())
    
    # Aggiungi eventi senza canale se non abbiamo raggiunto il target
    if expected_count == 0 or len(final_events) < expected_count:
        for title_key, ev in events_no_channel.items():
            # Controllo fuzzy per evitare duplicati di eventi già in final_events
            is_dup = False
            for chan_ev in final_events:
                chan_title_key = re.sub(r'[^a-z0-9]', '', chan_ev['title'].lower())
                if difflib.SequenceMatcher(None, title_key, chan_title_key).ratio() > 0.65:
                    is_dup = True
                    break
            if not is_dup:
                final_events.append(ev)
                if expected_count > 0 and len(final_events) >= expected_count:
                    break

    # Ordina per numero canale (quelli senza canale andranno a 0)
    final_events.sort(key=lambda x: x.get('channel_number', 0))

    logger.info(f"[PARSE] Estratti {len(final_events)} eventi univoci totali (Target: {expected_count})")

    # 5. Salva risultato
    result = {
        "timestamp": datetime.now().isoformat(),
        "source": "dazn_live_stream_ocr",
        "total_events": len(final_events),
        "events": final_events,
    }

    # Salva l'ultimo scan (sovrascrive)
    latest_path = OUTPUT_DIR / "dazn_live_events.json"
    with open(latest_path, "w", encoding="utf-8") as f:
        json.dump(result, f, ensure_ascii=False, indent=2)

    # Salva anche uno storico con timestamp
    history_path = OUTPUT_DIR / f"dazn_live_{timestamp}.json"
    with open(history_path, "w", encoding="utf-8") as f:
        json.dump(result, f, ensure_ascii=False, indent=2)

    # Stampa riepilogo
    print(f"\n{'='*60}")
    print(f"  DAZN LIVE - {datetime.now().strftime('%H:%M:%S')}")
    print(f"  {len(final_events)} eventi in onda (uniti da rotazione)")
    print(f"{'='*60}")
    for ev in final_events:
        ch = ev.get("channel", "")
        t = ev.get("time", "")
        print(f"  {t:>5}  {ch:<12}  {ev['title']}")
    print(f"{'='*60}\n")

    # Pulizia aggressiva: mantieni solo gli ultimi 21 frame/sidebar e storico EPG
    cleanup_frames(keep=21)

    return final_events


def cleanup_frames(keep: int = 21):
    """Rimuove i frame, sidebar e storico EPG piu' vecchi, mantenendo solo gli ultimi 'keep'."""
    if not FRAMES_DIR.exists():
        return

    removed = 0
    for pattern in ["frame_*.jpg", "sidebar_*.jpg"]:
        files = sorted(FRAMES_DIR.glob(pattern))
        if len(files) > keep:
            for f in files[:-keep]:
                f.unlink(missing_ok=True)
                removed += 1

    # Pulizia storico JSON (stessa soglia)
    json_files = sorted(OUTPUT_DIR.glob("dazn_live_*.json"))
    if len(json_files) > keep:
        for f in json_files[:-keep]:
            f.unlink(missing_ok=True)
            removed += 1

    if removed > 0:
        logger.info(f"[CLEANUP] Rimossi {removed} file vecchi (soglia: {keep})")


def scheduled_scan():
    """Job schedulato: scansione eventi."""
    try:
        scan_live_events()
    except Exception as e:
        logger.error(f"[ERRORE] Scansione fallita: {e}", exc_info=True)


def main():
    print("")
    print("  ====================================")
    print("    DAZN Live Event Scanner")
    print("    Estrae eventi e canali dallo")
    print("    stream EPG ogni minuto")
    print("  ====================================")
    print("")

    # Prima scansione immediata
    logger.info("[AVVIO] Prima scansione...")
    scan_live_events()

    # Schedula ogni minuto
    schedule.every(1).minutes.do(scheduled_scan)
    logger.info("[SCHEDULE] Scansione ogni 1 minuto")
    logger.info("[INFO] Premi Ctrl+C per uscire.\n")

    try:
        while True:
            schedule.run_pending()
            time.sleep(10)
    except KeyboardInterrupt:
        logger.info("\n[STOP] Arresto richiesto. Ciao!")


if __name__ == "__main__":
    main()
