"""
Extrae URLs de stream de Vidalytics navegando por ClickFunnels.
La URL stream.m3u8 se captura automaticamente cuando cada leccion carga.
No es necesario llamar a play() - el player Bitmovin/Vidalytics precarga el m3u8.
"""
import asyncio
import json
import re
import subprocess
import sys
import unicodedata
from pathlib import Path

from playwright.async_api import async_playwright

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 ts_url_to_m3u8(ts_url: str) -> str:
    """Convierte .ts segment URL → stream.m3u8 (reemplaza desde /ts/ en adelante)."""
    m = re.match(
        r'(https://fast[^/]*\.vidalytics\.com/video/[^/]+/[^/]+/\d+/\d+__FFMPEG)/',
        ts_url
    )
    if m:
        return m.group(1) + "/stream.m3u8"
    idx = ts_url.find("/ts/")
    if idx > 0:
        return ts_url[:idx] + "/stream.m3u8"
    return ts_url


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")
    return await do_login(ctx)


def extract_vidalytics_id(html: str) -> str | None:
    patterns = [
        r'vidalytics_embed_([A-Za-z0-9_-]{10,})',
        r'Vidalytics\.embed\(["\']([A-Za-z0-9_-]+)["\']',
        r'vid(?:id|Id|eo_id)\s*[:=]\s*["\']([A-Za-z0-9_-]{10,})["\']',
    ]
    for p in patterns:
        m = re.search(p, html)
        if m:
            return m.group(1)
    return None


async def click_next_lesson(page) -> bool:
    selectors = [
        "a[href='#next-lesson']",
        "#next-lesson",
        "a:has-text('Siguiente')",
        "a:has-text('Next')",
        "button:has-text('Siguiente')",
    ]
    for sel in selectors:
        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


def make_capture_handler(capture: dict):
    """
    Captura la URL stream.m3u8 de Vidalytics automaticamente.
    Se activa tan pronto como el player hace el request al master playlist.
    Priority: stream.m3u8 > variant.m3u8 > .ts reconstruction
    """
    async def on_response(response):
        if capture.get("locked"):
            return
        url = response.url
        if not ("fast.vidalytics.com/video/" in url or "fast-dev.vidalytics.com/video/" in url):
            return
        ctype = response.headers.get("content-type", "")
        # Mejor: master playlist stream.m3u8
        if "stream.m3u8" in url:
            capture["url"] = url
            capture["locked"] = True
            return
        # Buena: variant playlist
        if "_variant.m3u8" in url and not capture.get("url"):
            capture["url"] = url
            return
        # Fallback: cualquier m3u8
        if ".m3u8" in url and not capture.get("url"):
            capture["url"] = url
            return
        # Ultimo recurso: reconstruir desde .ts
        if (".ts" in url or "mp2t" in ctype) and not capture.get("url"):
            capture["url"] = ts_url_to_m3u8(url)
    return on_response


async def extract_streams_from_clickfunnels(lessons: list) -> list:
    id_to_lesson = {l["vidalytics_id"]: l for l in lessons if l.get("vidalytics_id")}
    pending_ids  = {l["vidalytics_id"] for l in lessons
                    if l.get("vidalytics_id") and not l.get("stream_url")}

    log(f"Lecciones pendientes: {len(pending_ids)}", "HEAD")
    if not pending_ids:
        log("Nada pendiente.", "OK")
        return lessons

    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, continuando...", "ERROR")

        # capture["url"] se setea automaticamente cuando el player carga el m3u8
        capture = {"url": None, "locked": False}
        handler = make_capture_handler(capture)

        page = await ctx.new_page()
        page.on("response", handler)

        log("Navegando al curso...")
        await page.goto(MEMBERS_URL, wait_until="domcontentloaded", timeout=30000)
        # Esperar que el player cargue y fetchee el stream.m3u8
        await page.wait_for_timeout(6000)

        lesson_num = 0
        found_count = 0
        max_lessons = 95

        while lesson_num < max_lessons:
            lesson_num += 1
            html = await page.content()
            vid_id = extract_vidalytics_id(html)

            if not vid_id:
                log(f"[{lesson_num:2d}] Sin Vidalytics ID", "SKIP")
            elif vid_id not in pending_ids:
                log(f"[{lesson_num:2d}] {vid_id[:16]} (no es objetivo / ya tiene stream)", "SKIP")
            else:
                lesson = id_to_lesson.get(vid_id, {})
                title = lesson.get("title", f"Leccion {lesson_num}")[:45]

                # Si el handler ya capturo la URL (durante carga de pagina)
                if not capture.get("url"):
                    # Esperar un poco mas por si aun esta cargando
                    await page.wait_for_timeout(3000)

                if capture.get("url"):
                    id_to_lesson[vid_id]["stream_url"] = capture["url"]
                    log(f"[{lesson_num:2d}] {title}", "OK")
                    log(f"  {capture['url'][:100]}", "OK")
                    found_count += 1
                    pending_ids.discard(vid_id)
                else:
                    log(f"[{lesson_num:2d}] {title} | Sin m3u8 (intentando play...)", "SKIP")
                    # Fallback: trigear play para obtener .ts y reconstruir
                    for frame in page.frames:
                        try:
                            await frame.evaluate("""async () => {
                                const v = document.querySelector('video');
                                if (v) { try { await v.play(); } catch(e) {} }
                            }""")
                        except Exception:
                            pass
                    await page.wait_for_timeout(5000)
                    if capture.get("url"):
                        id_to_lesson[vid_id]["stream_url"] = capture["url"]
                        log(f"  OK via play(): {capture['url'][:100]}", "OK")
                        found_count += 1
                        pending_ids.discard(vid_id)
                    else:
                        log(f"  Sin URL definitivamente", "ERROR")

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

            if not pending_ids:
                log("Todas las streams capturadas.", "OK")
                break

            prev_html = html
            # Reset capture para la siguiente leccion
            capture["url"] = None
            capture["locked"] = False

            clicked = await click_next_lesson(page)
            if not clicked:
                log("Sin boton siguiente. Fin.", "OK")
                break

            # Esperar que cargue el nuevo contenido + stream.m3u8
            await page.wait_for_timeout(5000)

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

        page.remove_listener("response", handler)
        await page.close()
        await browser.close()

    return lessons


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

    # Fallback ffmpeg para HLS
    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


async def main():
    log("=== Vidalytics Stream Extractor + Downloader ===", "HEAD")

    lessons = json.loads(STRUCTURE_FILE.read_text(encoding="utf-8"))
    log(f"Total lecciones: {len(lessons)}")

    has_stream = [l for l in lessons if l.get("stream_url")]
    log(f"Con stream URL: {len(has_stream)}")
    log(f"Sin stream URL: {len(lessons) - len(has_stream)}")

    lessons = await extract_streams_from_clickfunnels(lessons)

    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")]
    log(f"Stream URLs obtenidas: {len(has_stream)}/{len(lessons)}", "OK")

    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())
