#!/usr/bin/env python3
"""RC109 real-DOM browser QA helper.

Uses Playwright + system Chromium in this sandbox. Direct file/http navigation may be
blocked, so this script inlines preview.html assets and renders via page.set_content().
"""
from __future__ import annotations

import asyncio
import base64
import json
import mimetypes
import os
import re
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "qa_screenshots"
VIEWPORTS = [
    ("mobile", 390, 844),
    ("ipad", 820, 1180),
    ("desktop", 1440, 1024),
]


def _read_asset(src: str) -> str:
    clean = src.split("?", 1)[0].split("#", 1)[0]
    return (ROOT / clean).read_text(encoding="utf-8")


def build_inlined_preview() -> str:
    html = (ROOT / "preview.html").read_text(encoding="utf-8")
    base = f'<base href="file://{ROOT.as_posix()}/">'
    qa_head = """
<script>
(function(){
  try{ history.replaceState = function(){}; history.pushState = function(){}; }catch(e){}
  const nativeFetch = window.fetch ? window.fetch.bind(window) : null;
  window.fetch = function(url, options){
    const s = String(url || '');
    if(s.includes('ota_youtube_proxy.php')){
      return Promise.resolve({ ok:true, json:function(){ return Promise.resolve({ embedUrl:'https://www.youtube.com/embed/TESTID12345?rel=0&modestbranding=1' }); } });
    }
    if(s.includes('video_backup_api.php')){
      return Promise.resolve({ ok:true, json:function(){ return Promise.resolve({ status:'missing', exists:false }); } });
    }
    return nativeFetch ? nativeFetch(url, options) : Promise.reject(new Error('fetch disabled in RC109 QA'));
  };
})();
</script>
"""
    html = html.replace("<head>", f"<head>{base}\n{qa_head}", 1)

    def repl_link(match: re.Match[str]) -> str:
        attrs = match.group(1)
        href_match = re.search(r'href=["\']([^"\']+)["\']', attrs)
        if not href_match:
            return match.group(0)
        href = href_match.group(1)
        css = _read_asset(href)
        return f"<style data-inline-src=\"{href}\">\n{css}\n</style>"

    html = re.sub(r'<link\s+([^>]*rel=["\']stylesheet["\'][^>]*)>', repl_link, html)

    def repl_script(match: re.Match[str]) -> str:
        attrs = match.group(1)
        src_match = re.search(r'src=["\']([^"\']+)["\']', attrs)
        if not src_match:
            return match.group(0)
        src = src_match.group(1)
        js = _read_asset(src)
        return f"<script data-inline-src=\"{src}\">\n{js}\n</script>"

    html = re.sub(r'<script\s+([^>]*src=["\'][^"\']+["\'][^>]*)>\s*</script>', repl_script, html)
    return html


def as_data_uri(src: str) -> tuple[str, str] | None:
    if not src or src.startswith(("data:", "http://", "https://", "blob:")):
        return None
    clean = src.split("?", 1)[0].split("#", 1)[0]
    if clean.startswith("file://"):
        path = Path(clean[7:])
    else:
        path = ROOT / clean.lstrip("/")
    if not path.exists() or not path.is_file():
        return None
    mime = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
    data = base64.b64encode(path.read_bytes()).decode("ascii")
    return clean, f"data:{mime};base64,{data}"


async def inline_dom_images(page):
    imgs = await page.evaluate("""
      () => Array.from(document.images).map((img, i) => ({ i, src: img.getAttribute('src') || '' }))
    """)
    for img in imgs:
        converted = as_data_uri(img["src"])
        if not converted:
            continue
        original, data_uri = converted
        await page.evaluate("""
          ({ index, original, dataUri }) => {
            const img = document.images[index];
            if(!img) return;
            img.setAttribute('data-qa-original-src', original);
            img.src = dataUri;
          }
        """, {"index": img["i"], "original": original, "dataUri": data_uri})


async def setup_page(browser, html: str, width: int, height: int):
    ctx = await browser.new_context(viewport={"width": width, "height": height}, device_scale_factor=1)

    async def _route(route):
        url = route.request.url
        if url.startswith(("http://", "https://")):
            await route.abort()
        else:
            await route.continue_()

    await ctx.route("**/*", _route)
    page = await ctx.new_page()
    page.set_default_timeout(12000)
    page.set_default_navigation_timeout(12000)
    await page.set_content(html, wait_until="domcontentloaded")
    await page.evaluate("""
      () => {
        if(typeof showQa === 'function') showQa();
        const rows = Array.from(document.querySelectorAll('.next-library-row'));
        const idx = rows.findIndex(row => row.dataset.exerciseKey === 'bear-crawl');
        if(idx >= 0 && typeof showExerciseDetail === 'function') showExerciseDetail(idx);
      }
    """)
    await page.wait_for_timeout(700)
    await inline_dom_images(page)
    await page.wait_for_timeout(250)
    return ctx, page


async def screenshot_metrics(page, name: str, width: int, height: int):
    out = OUT / f"rc109_library_expanded_{name}_{width}x{height}.png"
    await page.screenshot(path=str(out), full_page=False)
    metrics = await page.evaluate("""
      () => {
        const detail = document.querySelector('.next-library-detail-inline');
        const video = detail?.querySelector('.next-library-video');
        const copy = detail?.querySelector('.next-library-copy-panel');
        const img = detail?.querySelector('.next-exercise-visual-figure');
        const ill = detail?.querySelector('.next-exercise-illustration');
        const backup = detail?.querySelector('.backup-upload-status');
        const more = detail?.querySelector('.next-library-pass-uses small');
        const passUses = Array.from(detail?.querySelectorAll('.next-library-pass-uses span') || []);
        const rect = el => el ? JSON.parse(JSON.stringify(el.getBoundingClientRect())) : null;
        return {
          screenshot: `qa_screenshots/${out.name}`,
          viewport: [window.innerWidth, window.innerHeight],
          key: detail?.dataset.exerciseKey || '',
          title: detail?.querySelector('h2')?.textContent?.trim() || '',
          detailRect: rect(detail),
          grid: detail ? getComputedStyle(detail).gridTemplateColumns : '',
          detailBg: detail ? getComputedStyle(detail).backgroundImage : '',
          videoRect: rect(video),
          copyRect: rect(copy),
          imageRendered: img ? [img.naturalWidth, img.naturalHeight] : null,
          imageFit: img ? getComputedStyle(img).objectFit : '',
          imageOriginalSrc: img?.getAttribute('data-qa-original-src') || img?.getAttribute('src') || '',
          iframeSrc: detail?.querySelector('iframe')?.getAttribute('src') || '',
          illustration: ill ? {
            key: ill.dataset.exerciseKey || '',
            quality: ill.dataset.quality || '',
            bgMode: ill.dataset.backgroundMode || '',
            theme: ill.dataset.theme || ''
          } : null,
          backupIdle: backup ? getComputedStyle(backup).display : null,
          passUseCount: passUses.length,
          moreText: more?.textContent?.trim() || '',
          brokenImages: Array.from(document.images).filter(img => img.naturalWidth === 0).map(img => img.getAttribute('data-qa-original-src') || img.getAttribute('src'))
        };
      }
    """.replace("${out.name}", out.name))
    # page.evaluate string replacement isn't reliable inside template literals above; set directly.
    metrics["screenshot"] = f"qa_screenshots/{out.name}"
    return metrics


async def direct_youtube_check(page):
    await page.evaluate("""
      () => {
        const rows = Array.from(document.querySelectorAll('.next-library-row'));
        const idx = rows.findIndex(row => row.dataset.exerciseKey === 'dynamic-warm-up');
        if(idx >= 0 && typeof showExerciseDetail === 'function') showExerciseDetail(idx);
      }
    """)
    await page.wait_for_timeout(400)
    return await page.evaluate("""
      () => {
        const detail = document.querySelector('.next-library-detail-inline[data-exercise-key="dynamic-warm-up"]');
        const video = detail?.querySelector('.next-library-video');
        const iframe = detail?.querySelector('iframe');
        return {
          exerciseKey: detail?.dataset.exerciseKey || '',
          title: detail?.querySelector('h2')?.textContent?.trim() || '',
          videoShellClass: video?.className || '',
          iframeSrc: iframe?.getAttribute('src') || ''
        };
      }
    """)


async def main():
    from playwright.async_api import async_playwright
    OUT.mkdir(exist_ok=True)
    html = build_inlined_preview()
    # Runtime HTML intentionally kept in memory only; do not ship stale inlined QA copies.
    results = []
    youtube_check = None
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            executable_path="/usr/bin/chromium",
            headless=True,
            args=["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--disable-gpu-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
        )
        for name, w, h in VIEWPORTS:
            ctx, page = await setup_page(browser, html, w, h)
            results.append(await screenshot_metrics(page, name, w, h))
            if name == "desktop":
                youtube_check = await direct_youtube_check(page)
            await ctx.close()
        await browser.close()
    payload = {
        "method": "Chromium Playwright page.set_content with inlined preview.html because direct file/http navigation was blocked in sandbox.",
        "screenshots": results,
        "directYoutubeCheck": youtube_check,
    }
    (OUT / "rc109_library_expanded_dom_metrics.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps(payload, ensure_ascii=False, indent=2), flush=True)


if __name__ == "__main__":
    asyncio.run(main())
    # Chromium/Playwright can leave non-daemon helpers alive in the sandbox after a successful run.
    # Force a clean CLI exit so automated QA does not hang after screenshots/metrics are written.
    os._exit(0)
