"""
RAIO Course Downloader v5
- Navega todas las lecciones en ClickFunnels
- Captura stream.m3u8 y embed ID de Vidalytics automaticamente
  (el player Bitmovin los fetchea durante la carga de cada seccion)
- Descarga con yt-dlp / ffmpeg
"""
import asyncio
import json
import re
import subprocess
import sys
import unicodedata
from pathlib import Path

from playwright.async_api import async_playwright
from bs4 import BeautifulSoup

sys.stdout.reconfigure(encoding="utf-8")

BASE_URL     = "https://kaleanders.clickfunnels.com"
LOGIN_PAGE   = f"{BASE_URL}/login-raio31910526?page_id=31910527&page_key=kyy8t5uqnf4ym1ng&login_redirect=1"
MEMBERS_URL  = f"{BASE_URL}/members-raio"
EMAIL        = "kokimobil35@gmail.com"
PASSWORD     = "Korea123.@!@#"

SCRIPTS_DIR    = Path(__file__).parent
BASE_DIR       = SCRIPTS_DIR.parent
STRUCTURE_FILE = SCRIPTS_DIR / "course_structure.json"
COOKIES_FILE   = SCRIPTS_DIR / ".course_cookies.json"
VIDEOS_DIR     = BASE_DIR / "storage" / "app" / "course-media" / "curso-de-ingles-raio"

VIDEOS_DIR.mkdir(parents=True, exist_ok=True)

def log(msg, level="INFO"):
    icons = {"INFO": "  ", "OK": "[OK]", "SKIP": "[--]", "ERROR": "[!!]", "HEAD": "\n==="}
    print(f"{icons.get(level,'  ')} {msg}", flush=True)

def slugify(text):
    text = unicodedata.normalize("NFD", text.lower())
    text = "".join(c for c in text if unicodedata.category(c) != "Mn")
    return re.sub(r"[-\s]+", "-", re.sub(r"[^a-z0-9\s-]", "", text)).strip("-")

def stream_token(url: str) -> str | None:
    """Extrae el token unico de video de la URL de stream."""
    m = re.search(r'/video/[^/]+/([^/]+)/', url)
    return m.group(1) if m else None

def embed_id_from_license(url: str) -> str | None:
    """https://fast.vidalytics.com/license?hash={embed_id}%7Cdomain"""
    m = re.search(r'hash=([A-Za-z0-9_-]{10,})(?:%7C|[|])', url)
    return m.group(1) if m else None


# ── Sesion ─────────────────────────────────────────────────────────────────────

async def do_login(ctx) -> bool:
    page = await ctx.new_page()
    await page.goto(LOGIN_PAGE, wait_until="domcontentloaded", timeout=30000)
    await page.wait_for_timeout(2500)
    await page.evaluate(f"""() => {{
        const f = document.querySelector('#login-form');
        if (!f) return;
        const e = f.querySelector('input[name="member[email]"]');
        const p = f.querySelector('input[name="member[password]"]');
        if (e) {{ e.value = "{EMAIL}"; e.dispatchEvent(new Event('input',{{bubbles:true}})); }}
        if (p) {{ p.value = "{PASSWORD}"; p.dispatchEvent(new Event('input',{{bubbles:true}})); }}
        const form = f.querySelector('form');
        if (form) form.submit();
    }}""")
    try:
        await page.wait_for_load_state("domcontentloaded", timeout=15000)
    except Exception:
        pass
    await page.wait_for_timeout(3000)
    ok = "members" in page.url and "login" not in page.url
    log(f"Login {'OK' if ok else 'FALLO'} -> {page.url}", "OK" if ok else "ERROR")
    await page.close()
    return ok


async def load_session(ctx) -> bool:
    if COOKIES_FILE.exists():
        try:
            cookies = json.loads(COOKIES_FILE.read_text(encoding="utf-8"))
            if cookies:
                await ctx.add_cookies(cookies)
                page = await ctx.new_page()
                await page.goto(MEMBERS_URL, wait_until="domcontentloaded", timeout=25000)
                await page.wait_for_timeout(2000)
                ok = "members" in page.url and "login" not in page.url
                await page.close()
                if ok:
                    log("Sesion restaurada desde cookies.", "OK")
                    return True
                log("Cookies expiradas, re-login...", "INFO")
        except Exception as e:
            log(f"Error cookies: {e}", "ERROR")
    ok = await do_login(ctx)
    if ok:
        cookies = await ctx.cookies()
        COOKIES_FILE.write_text(json.dumps(cookies, indent=2), encoding="utf-8")
    return ok


# ── Extraccion de contenido ────────────────────────────────────────────────────

def extract_lesson_title(soup: BeautifulSoup, num: int) -> str:
    def is_bad(text: str) -> bool:
        bad = {"importante", "tip", "ve el video", "consejo", "nota",
               "hemos actualizado", "actualizado el curso"}
        if any(b in text.lower() for b in bad):
            return True
        # Fechas: "9 mayo 2023", "01/01/2024"
        if re.search(r'\b(20\d\d|enero|febrero|marzo|abril|mayo|junio|julio|'
                     r'agosto|septiembre|octubre|noviembre|diciembre)\b', text.lower()):
            return True
        return False

    for tag in soup.find_all(["h1", "h2", "h3"]):
        text = tag.get_text(strip=True)
        if 4 < len(text) < 150 and not is_bad(text):
            return text
    for p in soup.find_all("p"):
        text = p.get_text(strip=True)
        if 10 < len(text) < 100 and not is_bad(text):
            return text[:80]
    return f"Leccion {num}"


async def click_next_lesson(page) -> bool:
    for sel in ["a[href='#next-lesson']", "#next-lesson",
                "a:has-text('Siguiente')", "a:has-text('Next')"]:
        try:
            btn = await page.query_selector(sel)
            if btn:
                visible = await btn.is_visible()
                if visible:
                    await btn.click()
                    return True
                await page.evaluate(f"document.querySelector('{sel}')?.click()")
                return True
        except Exception:
            continue
    return False


async def scrape_course(ctx) -> list:
    """
    Navega el curso y captura stream URLs para cada leccion.
    Por leccion, recoge URLs de fast.vidalytics.com durante una ventana de 6s
    tras la carga de la seccion.
    """
    page = await ctx.new_page()

    # Acumulador de URLs capturadas por ventana de tiempo
    window_urls: list[str] = []

    async def on_response(response):
        url = response.url
        if "fast.vidalytics.com" in url or "fast-dev.vidalytics.com" in url:
            window_urls.append(url)

    page.on("response", on_response)

    log("Cargando pagina del curso...")
    await page.goto(MEMBERS_URL, wait_until="domcontentloaded", timeout=30000)
    await page.wait_for_timeout(7000)  # ventana inicial para leccion 1

    lessons = []
    seen_tokens = set()
    lesson_num = 0
    max_lessons = 95

    while lesson_num < max_lessons:
        lesson_num += 1
        html = await page.content()
        soup = BeautifulSoup(html, "html.parser")
        title = extract_lesson_title(soup, lesson_num)

        # Extraer stream URL y embed ID de las URLs capturadas en esta ventana
        stream_url = None
        vid_id = None
        for url in window_urls:
            if stream_url is None and "stream.m3u8" in url:
                stream_url = url
            if stream_url is None and "_variant.m3u8" in url:
                stream_url = url  # Fallback al primer variant
            if vid_id is None:
                vid_id = embed_id_from_license(url)

        # Deduplicar por token de stream
        token = stream_token(stream_url) if stream_url else None

        log(f"[{lesson_num:2d}] {title[:50]}")
        if vid_id:
            log(f"  ID: {vid_id}", "OK")
        if stream_url:
            log(f"  Stream: {stream_url[:90]}", "OK")
        else:
            log(f"  Sin stream URL ({len(window_urls)} URLs en ventana)", "SKIP")

        if token and token in seen_tokens:
            log(f"  Duplicado, saltando...", "SKIP")
        else:
            if token:
                seen_tokens.add(token)
            lessons.append({
                "num": lesson_num,
                "title": title,
                "vidalytics_id": vid_id,
                "stream_url": stream_url,
            })

        # Guardar progreso
        if lesson_num % 15 == 0:
            STRUCTURE_FILE.write_text(
                json.dumps(lessons, ensure_ascii=False, indent=2), encoding="utf-8"
            )
            log(f"  Progreso guardado ({lesson_num} lecciones)")

        # Limpiar ventana y navegar
        window_urls.clear()
        prev_html = html
        clicked = await click_next_lesson(page)
        if not clicked:
            log("Sin boton siguiente. Fin del curso.", "OK")
            break

        # Esperar que cargue la siguiente seccion y el player fetchee sus URLs
        await page.wait_for_timeout(6000)

        new_html = await page.content()
        if new_html == prev_html:
            log("Pagina sin cambios. Fin.", "SKIP")
            break

    page.remove_listener("response", on_response)
    await page.close()
    return lessons


# ── Descarga ───────────────────────────────────────────────────────────────────

def download_stream(url: str, output_path: Path) -> bool:
    if output_path.exists() and output_path.stat().st_size > 1_000_000:
        log(f"Ya existe ({output_path.stat().st_size // 1_000_000}MB): {output_path.name}", "SKIP")
        return True

    log(f"Descargando -> {output_path.name}")

    cmd = [
        "yt-dlp",
        "--format", "bestvideo+bestaudio/best",
        "--merge-output-format", "mp4",
        "--output", str(output_path),
        "--no-playlist",
        "--progress",
        url,
    ]
    result = subprocess.run(cmd)
    if result.returncode == 0 and output_path.exists() and output_path.stat().st_size > 100_000:
        log(f"OK ({output_path.stat().st_size // 1_000_000}MB)", "OK")
        return True

    if ".m3u8" in url:
        cmd_ff = [
            "ffmpeg", "-y", "-i", url,
            "-c", "copy", "-bsf:a", "aac_adtstoasc",
            str(output_path),
        ]
        result2 = subprocess.run(cmd_ff, capture_output=True)
        if result2.returncode == 0 and output_path.exists() and output_path.stat().st_size > 100_000:
            log(f"OK via ffmpeg ({output_path.stat().st_size // 1_000_000}MB)", "OK")
            return True

    log(f"Fallo: {output_path.name}", "ERROR")
    return False


# ── Main ───────────────────────────────────────────────────────────────────────

async def main():
    log("RAIO Course Downloader v5", "HEAD")

    async with async_playwright() as pw:
        browser = await pw.chromium.launch(headless=True)
        ctx = await browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124 Safari/537.36",
            viewport={"width": 1280, "height": 800},
        )
        ok = await load_session(ctx)
        if not ok:
            log("Sin sesion activa.", "ERROR")
            await browser.close()
            return

        log("Extrayendo curso...", "HEAD")
        lessons = await scrape_course(ctx)
        await browser.close()

    STRUCTURE_FILE.write_text(json.dumps(lessons, ensure_ascii=False, indent=2), encoding="utf-8")
    has_stream = [l for l in lessons if l.get("stream_url")]
    no_stream  = [l for l in lessons if not l.get("stream_url")]
    log(f"Lecciones: {len(lessons)} | Con stream: {len(has_stream)} | Sin stream: {len(no_stream)}", "OK")

    if no_stream:
        log("Sin stream URL:")
        for l in no_stream:
            log(f"  [{l['num']:2d}] {l['title'][:55]}")

    log("Descargando videos...", "HEAD")
    dl = sk = fl = 0
    for lesson in lessons:
        url = lesson.get("stream_url")
        if not url:
            log(f"Sin URL: {lesson['title'][:40]}", "SKIP"); sk += 1; continue
        slug = slugify(lesson["title"])
        out = VIDEOS_DIR / f"{slug}.mp4"
        if download_stream(url, out):
            dl += 1
        else:
            fl += 1

    log(f"Total: {len(lessons)} | Descargados: {dl} | Sin URL: {sk} | Fallidos: {fl}", "OK")


if __name__ == "__main__":
    asyncio.run(main())
