#!/usr/bin/env python3
"""
Browser-based video downloader for KVS and other protected sites.
Uses Playwright to intercept video streams within the browser session.

This bypasses session-bound URL restrictions by downloading within
the same browser context that loaded the page.

Usage:
  python browser_download.py <url> <output_dir> [--timeout 120]
"""

import sys
import os
import json
import re
import asyncio
import argparse
import subprocess
import tempfile
import urllib.request
from pathlib import Path
from urllib.parse import urlparse, unquote

BROWSER_STATE_FILE = Path('/home/rampage/Vertex/projects/grabbit/data/browser_state.json')
DEFAULT_WINDOWS_PROFILE_DIR = 'C:\\Users\\Rampage\\AppData\\Local\\GrabbitChromeProfile'

# Check for playwright
try:
    from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeout
except ImportError:
    print(json.dumps({
        'success': False,
        'error': 'Playwright not installed. Run: pip install playwright && playwright install chromium',
        'error_type': 'missing_dependency'
    }))
    sys.exit(1)


def output_json(data: dict):
    """Print JSON to stdout"""
    print(json.dumps(data))
    sys.exit(0 if data.get('success') else 1)


def sanitize_filename(name: str, max_length: int = 100) -> str:
    """Sanitize filename for filesystem"""
    # Remove/replace invalid chars
    name = re.sub(r'[<>:"/\\|?*]', '_', name)
    name = re.sub(r'\s+', ' ', name).strip()
    # Truncate
    if len(name) > max_length:
        name = name[:max_length]
    return name or 'video'


class VideoInterceptor:
    """Intercepts and downloads video streams from browser"""

    def __init__(
        self,
        output_dir: str,
        timeout: int = 120,
        headed: bool = False,
        cdp_endpoint: str | None = None,
        bootstrap_windows_chrome: bool = False,
        windows_profile_dir: str | None = None,
        cdp_port: int = 9223
    ):
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.timeout = timeout
        self.headed = headed
        self.cdp_endpoint = cdp_endpoint
        self.bootstrap_windows_chrome = bootstrap_windows_chrome
        self.windows_profile_dir = self._sanitize_windows_profile_dir(windows_profile_dir)
        self.cdp_port = cdp_port
        self.video_urls = []
        self.downloaded_file = None
        self.title = 'video'
        self.thumbnail = ''
        self.last_stage = 'init'

    def _extract_video_id(self, source_url: str) -> str:
        if not source_url:
            return ''
        patterns = [
            r'/video/(\d+)/',
            r'/(\d+)_\d+p\.mp4',
            r'/(\d+)\.mp4',
        ]
        for pattern in patterns:
            match = re.search(pattern, source_url, re.I)
            if match:
                return match.group(1)
        return ''

    def _build_output_path(self, source_url: str, ext: str = '.mp4') -> Path:
        safe_title = sanitize_filename(self.title or 'video')
        video_id = self._extract_video_id(source_url)
        stem = f'{safe_title}_{video_id}' if video_id else safe_title
        if not ext:
            return self.output_dir / stem
        return self.output_dir / f'{stem}{ext}'

    def _sanitize_windows_profile_dir(self, value: str | None) -> str:
        if not value:
            return DEFAULT_WINDOWS_PROFILE_DIR
        raw = value.strip().strip('"')
        if not raw:
            return DEFAULT_WINDOWS_PROFILE_DIR
        if re.fullmatch(r'/+', raw):
            return DEFAULT_WINDOWS_PROFILE_DIR
        if '://' in raw:
            return DEFAULT_WINDOWS_PROFILE_DIR
        if not re.match(r'^[A-Za-z]:[\\/]', raw):
            return DEFAULT_WINDOWS_PROFILE_DIR
        return raw.replace('/', '\\')

    def _is_cdp_ready(self) -> bool:
        endpoint = self.cdp_endpoint or f'http://127.0.0.1:{self.cdp_port}'
        try:
            with urllib.request.urlopen(f'{endpoint}/json/version', timeout=2) as r:
                return r.status == 200
        except Exception:
            return False

    def _start_windows_chrome(self):
        # Start a dedicated Chrome instance with remote debugging in a separate profile.
        profile_arg = self.windows_profile_dir.replace("'", "''")
        cmd = (
            "Start-Process -FilePath 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' "
            f"-ArgumentList '--remote-debugging-port={self.cdp_port} "
            f"--user-data-dir=\"{profile_arg}\" "
            "--profile-directory=Default --no-first-run --no-default-browser-check "
            "--window-position=-32000,-32000 --window-size=1200,800'"
        )
        subprocess.run(['powershell.exe', '-NoProfile', '-Command', cmd], check=False)

        for _ in range(20):
            if self._is_cdp_ready():
                return True
            try:
                import time
                time.sleep(0.5)
            except Exception:
                pass
        return False

    def _ranked_videos(self) -> list[dict]:
        """Return candidate videos sorted from most to least likely valid."""
        if not self.video_urls:
            return []

        def quality_from_url(url: str) -> int:
            match = re.search(r'_(\d{3,4})p\.mp4', url, re.I)
            if match:
                return int(match.group(1))
            match = re.search(r'(\d{3,4})p', url, re.I)
            if match:
                return int(match.group(1))
            return 0

        def score(video: dict) -> int:
            url = (video.get('url') or '').lower()
            typ = (video.get('type') or 'unknown').lower()
            s = 0
            q = quality_from_url(url)

            # Prefer explicit quality streams
            if '_1080p.mp4' in url or '1080p' in url:
                s += 400
            if '_720p.mp4' in url or '720p' in url:
                s += 300
            if '_480p.mp4' in url or '480p' in url:
                s += 200
            s += q

            # KVS-specific hints
            if typ.startswith('player'):
                s += 350
            if '/get_file/9/' in url:
                s += 250
            if '/get_file/' in url:
                s += 150
            if '/get_file/1/' in url and '_720p' not in url and '_1080p' not in url:
                s -= 300

            # Prefer mp4 over manifests when available
            if '.mp4' in url:
                s += 120
            if '.m3u8' in url:
                s += 40

            # Fallback based on detected type
            type_order = {'1080p': 80, '720p': 60, '480p': 40, 'unknown': 20, 'hls': 10}
            s += type_order.get(typ, 0)

            return s

        return sorted(self.video_urls, key=score, reverse=True)

    async def collect_player_urls(self, page):
        """Collect live KVS URLs from in-page player objects."""
        try:
            found = await page.evaluate("""() => {
                const out = [];
                const add = (u, t) => {
                    if (!u || typeof u !== 'string') return;
                    let v = u.trim();
                    if (!v) return;
                    const m = v.match(/^function\\/\\d+\\/(.+)$/);
                    if (m && m[1]) v = m[1];
                    if (!/^https?:\\/\\//i.test(v)) return;
                    out.push({ url: v, type: t || 'player' });
                };

                const confs = [];
                try {
                    if (window.player_obj && window.player_obj.conf) confs.push(window.player_obj.conf);
                } catch (e) {}
                try {
                    if (window.kvsplayer && window.kvsplayer.kt_player && window.kvsplayer.kt_player.conf) confs.push(window.kvsplayer.kt_player.conf);
                } catch (e) {}
                try {
                    if (window.kt_player && window.kt_player.conf) confs.push(window.kt_player.conf);
                } catch (e) {}

                for (const conf of confs) {
                    add(conf.video_url, 'player');
                    add(conf.video_alt_url, 'player_alt');
                    for (const [k, val] of Object.entries(conf)) {
                        // Capture all explicit quality variants and any direct get_file links.
                        if (k.startsWith('video_url_')) add(val, k);
                        if (typeof val === 'string' && /get_file\/\d+\/.+\.mp4/i.test(val)) add(val, k);
                    }
                }
                return out;
            }""")

            if isinstance(found, list):
                for item in found:
                    if not isinstance(item, dict):
                        continue
                    u = item.get('url')
                    t = item.get('type', 'player')
                    if isinstance(u, str) and u:
                        self.video_urls.append({
                            'url': u,
                            'headers': {'Referer': page.url},
                            'type': str(t)
                        })
        except Exception:
            pass

        # Fallback: parse rendered HTML for inline KVS config URLs
        try:
            html = await page.content()
            patterns = [
                r"video_url\s*:\s*['\"]function/\d+/(https?://[^'\"\s]+)['\"]",
                r"video_url_\d+p\s*:\s*['\"]function/\d+/(https?://[^'\"\s]+)['\"]",
                r"(https?://[^'\"\s]+/get_file/9/[^'\"\s]+\.mp4/?)",
            ]
            for pattern in patterns:
                for m in re.finditer(pattern, html, re.I):
                    u = m.group(1).strip()
                    if u:
                        self.video_urls.append({
                            'url': u,
                            'headers': {'Referer': page.url},
                            'type': 'player_html'
                        })
        except Exception:
            pass

    def _is_valid_download(self, file_path: str, min_size: int = 256 * 1024) -> bool:
        try:
            p = Path(file_path)
            return p.exists() and p.stat().st_size >= min_size
        except Exception:
            return False

    def _has_high_confidence_candidate(self) -> bool:
        for item in self.video_urls:
            url = (item.get('url') or '').lower()
            typ = (item.get('type') or '').lower()
            if '/get_file/9/' in url:
                return True
            if typ.startswith('player') and '.mp4' in url:
                return True
        return False

    async def download_video(self, page, video_info: dict) -> bool:
        """Download video using browser's fetch with proper session"""
        url = video_info['url']

        # Determine filename
        parsed = urlparse(url)
        path_parts = parsed.path.split('/')
        filename = path_parts[-1] if path_parts else 'video.mp4'
        filename = unquote(filename)

        # Use title if available
        if self.title and self.title != 'video':
            ext = Path(filename).suffix or '.mp4'
            filename = f"{sanitize_filename(self.title)}{ext}"

        source_url = video_info.get('url') or page.url
        ext = Path(filename).suffix or '.mp4'
        output_path = self._build_output_path(source_url, ext)

        # Use browser's JavaScript to fetch and download
        # This maintains the session cookies and headers
        try:
            # For direct video files, use page.request which maintains session
            response = await page.request.get(url, headers={
                'Referer': page.url,
                'Accept': '*/*',
            })

            if response.ok:
                body = await response.body()
                with open(output_path, 'wb') as f:
                    f.write(body)
                self.downloaded_file = str(output_path)
                return True
            else:
                return False
        except Exception:
            return False

    async def download_with_stream(self, page, video_info: dict) -> bool:
        """Download video using streaming approach for large files"""
        url = video_info['url']
        source_url = video_info.get('url') or page.url
        output_path = self._build_output_path(source_url, '.mp4')

        try:
            # Use Playwright's request context which shares browser session
            api_context = page.context.request

            response = await api_context.get(url, headers={
                'Referer': page.url,
            })

            if response.ok:
                body = await response.body()
                with open(output_path, 'wb') as f:
                    f.write(body)
                self.downloaded_file = str(output_path)
                return True

            return False

        except Exception as e:
            return False

    async def download_with_ytdlp(self, page, video_info: dict) -> bool:
        """
        Download using yt-dlp with cookies exported from the live browser context.
        This is more reliable for KVS session-bound URLs than raw fetch.
        """
        url = video_info['url']
        ext = '.mp4'
        if '.m3u8' in url.lower():
            ext = '.mp4'
        elif '.webm' in url.lower():
            ext = '.webm'
        elif '.mp4' in url.lower():
            ext = '.mp4'

        source_url = video_info.get('url') or page.url
        base_path = self._build_output_path(source_url, '')
        output_template = str(base_path.with_name(base_path.name + '.%(ext)s'))
        fallback_output = str(base_path.with_suffix(ext))

        # Export cookies from browser context to Netscape cookie file
        tmp_cookie_file = None
        try:
            cookies = await page.context.cookies()
            tmp_fd, tmp_cookie_file = tempfile.mkstemp(prefix='grabbit-kvs-', suffix='.txt')
            os.close(tmp_fd)

            with open(tmp_cookie_file, 'w', encoding='utf-8') as f:
                f.write('# Netscape HTTP Cookie File\n\n')
                for c in cookies:
                    domain = c.get('domain', '')
                    if not domain:
                        continue
                    include_subdomains = 'TRUE' if domain.startswith('.') else 'FALSE'
                    path = c.get('path') or '/'
                    secure = 'TRUE' if c.get('secure') else 'FALSE'
                    expires = str(int(c.get('expires', 0))) if c.get('expires') else '0'
                    name = c.get('name', '')
                    value = c.get('value', '')
                    if name:
                        f.write(f'{domain}\t{include_subdomains}\t{path}\t{secure}\t{expires}\t{name}\t{value}\n')

            parsed = urlparse(page.url)
            origin = f'{parsed.scheme}://{parsed.netloc}'

            cmd = [
                '/home/rampage/.local/bin/yt-dlp',
                '--no-check-certificates',
                '--impersonate', 'chrome-131',
                '--cookies', tmp_cookie_file,
                '--add-header', f'Referer:{page.url}',
                '--add-header', f'Origin:{origin}',
                '-o', output_template,
                '--merge-output-format', 'mp4',
                url
            ]

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            _stdout, stderr = await proc.communicate()

            if proc.returncode != 0:
                return False

            # Pick freshest downloaded file for this title
            candidates = list(self.output_dir.glob(f'{base_path.name}*'))
            if candidates:
                candidates.sort(key=lambda p: p.stat().st_mtime, reverse=True)
                self.downloaded_file = str(candidates[0])
                return True

            if Path(fallback_output).exists():
                self.downloaded_file = fallback_output
                return True

            return False
        except Exception:
            return False
        finally:
            if tmp_cookie_file and os.path.exists(tmp_cookie_file):
                try:
                    os.unlink(tmp_cookie_file)
                except Exception:
                    pass

    async def download_with_curl(self, page, video_info: dict) -> bool:
        """Download with curl following redirects; works well for KVS get_file links."""
        url = video_info['url']
        source_url = video_info.get('url') or page.url
        output_path = self._build_output_path(source_url, '.mp4')

        try:
            cmd = [
                'curl', '-L', '--fail', '--silent', '--show-error',
                '-A', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
                '-H', f'Referer: {page.url}',
                '-o', str(output_path),
                url
            ]
            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            _stdout, _stderr = await proc.communicate()
            if proc.returncode != 0:
                return False
            self.downloaded_file = str(output_path)
            return True
        except Exception:
            return False

    async def download_via_browser_download(self, page, video_info: dict) -> bool:
        """
        Trigger the browser's own download pipeline for the captured media URL.
        This keeps all session/auth behavior inside Chromium.
        """
        url = video_info['url']
        source_url = video_info.get('url') or page.url
        out_path = self._build_output_path(source_url, '.mp4')

        try:
            async with page.expect_download(timeout=8000) as dl_info:
                await page.evaluate(
                    """(u) => {
                        const a = document.createElement('a');
                        a.href = u;
                        a.download = 'video.mp4';
                        a.rel = 'noopener';
                        document.body.appendChild(a);
                        a.click();
                        a.remove();
                    }""",
                    url
                )

            download = await dl_info.value
            await download.save_as(str(out_path))
            self.downloaded_file = str(out_path)
            return True
        except Exception:
            return False

    async def run(self, url: str) -> dict:
        """Main entry point - navigate and download video"""
        async with async_playwright() as p:
            self.last_stage = 'playwright_started'
            using_cdp = self.cdp_endpoint is not None

            if self.bootstrap_windows_chrome and not self._is_cdp_ready():
                self.last_stage = 'bootstrap_windows_chrome'
                self._start_windows_chrome()

            if using_cdp:
                self.last_stage = 'connect_over_cdp'
                browser = await p.chromium.connect_over_cdp(self.cdp_endpoint)
                self.last_stage = 'cdp_connected'
                context = browser.contexts[0] if browser.contexts else await browser.new_context()
            else:
                # Launch browser with stealth settings
                self.last_stage = 'chromium_launch'
                browser = await p.chromium.launch(
                    headless=not self.headed,
                    args=[
                        '--no-sandbox',
                        '--disable-setuid-sandbox',
                        '--disable-blink-features=AutomationControlled',
                    ]
                )

                self.last_stage = 'new_context'
                context = await browser.new_context(
                    viewport={'width': 1920, 'height': 1080},
                    user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
                    java_script_enabled=True,
                )

            # Restore previous session cookies to improve continuity across runs
            try:
                if BROWSER_STATE_FILE.exists():
                    state = json.loads(BROWSER_STATE_FILE.read_text(encoding='utf-8'))
                    cookies = state.get('cookies', [])
                    if cookies:
                        await context.add_cookies(cookies)
            except Exception:
                pass

            self.last_stage = 'new_page'
            page = await context.new_page()

            self.last_stage = 'page_ready'

            async def close_and_persist():
                try:
                    cookies = await context.cookies()
                    BROWSER_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
                    BROWSER_STATE_FILE.write_text(json.dumps({'cookies': cookies}), encoding='utf-8')
                except Exception:
                    pass
                if not using_cdp:
                    await browser.close()
                else:
                    try:
                        await page.close()
                    except Exception:
                        pass

            try:
                # Navigate to page
                self.last_stage = 'goto'
                await page.goto(url, wait_until='domcontentloaded', timeout=60000)

                # Wait for page to fully load and video player to initialize
                self.last_stage = 'post_goto_wait'
                await page.wait_for_timeout(1200)
                await self.collect_player_urls(page)

                # Try to extract title
                try:
                    title_elem = await page.query_selector('title')
                    if title_elem:
                        self.title = await title_elem.inner_text()
                        self.title = re.sub(r'\s*[-|].*$', '', self.title).strip()
                except:
                    pass

                if not self._has_high_confidence_candidate():
                    # Try to find and click play button to trigger video load
                    play_selectors = [
                        '.play-button', '.vjs-play-control', '.player-play',
                        '[aria-label="Play"]', '.fp-play', '#play-button',
                        'video', '.video-js'
                    ]

                    for selector in play_selectors:
                        try:
                            self.last_stage = f'click_play:{selector}'
                            elem = await page.query_selector(selector)
                            if elem:
                                await elem.click()
                                await page.wait_for_timeout(700)
                                break
                        except:
                            continue

                    # Wait briefly for player state updates and refresh collected URLs
                    self.last_stage = 'intercept_wait'
                    await page.wait_for_timeout(1500)
                    await self.collect_player_urls(page)
                    if len(self.video_urls) <= 1:
                        await page.wait_for_timeout(1200)
                        await self.collect_player_urls(page)

                # Rank candidate videos
                ranked_videos = self._ranked_videos()
                best_video = ranked_videos[0] if ranked_videos else None

                if not best_video:
                    # Try scrolling to trigger lazy-loaded player
                    await page.evaluate('window.scrollTo(0, document.body.scrollHeight / 2)')
                    await page.wait_for_timeout(1000)
                    ranked_videos = self._ranked_videos()
                    best_video = ranked_videos[0] if ranked_videos else None

                if not best_video:
                    await close_and_persist()
                    return {
                        'success': False,
                        'error': 'No video stream found on page',
                        'error_type': 'no_video',
                        'intercepted_urls': len(self.video_urls)
                    }

                # Try top candidates until we get a valid non-placeholder file
                success = False
                selected_video = None
                for candidate in ranked_videos[:6]:
                    best_video = candidate
                    selected_video = candidate

                    try:
                        self.last_stage = 'download_with_curl'
                        success = await asyncio.wait_for(self.download_with_curl(page, best_video), timeout=15)
                    except Exception:
                        success = False
                    if not success:
                        try:
                            self.last_stage = 'download_via_browser_download'
                            success = await asyncio.wait_for(self.download_via_browser_download(page, best_video), timeout=6)
                        except Exception:
                            success = False
                    if not success:
                        try:
                            self.last_stage = 'download_with_ytdlp'
                            success = await asyncio.wait_for(self.download_with_ytdlp(page, best_video), timeout=15)
                        except Exception:
                            success = False
                    if not success:
                        try:
                            self.last_stage = 'download_with_stream'
                            success = await asyncio.wait_for(self.download_with_stream(page, best_video), timeout=10)
                        except Exception:
                            success = False
                    if not success:
                        try:
                            self.last_stage = 'download_video'
                            success = await asyncio.wait_for(self.download_video(page, best_video), timeout=10)
                        except Exception:
                            success = False

                    if success and self.downloaded_file and self._is_valid_download(self.downloaded_file):
                        break

                    # Reject tiny placeholder output and try next candidate
                    if self.downloaded_file:
                        try:
                            Path(self.downloaded_file).unlink(missing_ok=True)
                        except Exception:
                            pass
                        self.downloaded_file = None
                    success = False

                await close_and_persist()

                if success:
                    return {
                        'success': True,
                        'file_path': self.downloaded_file,
                        'title': self.title,
                        'video_url': selected_video['url'] if selected_video else best_video['url'],
                        'quality': selected_video['type'] if selected_video else best_video['type']
                    }
                else:
                    return {
                        'success': False,
                        'error': 'Failed to download a valid video stream',
                        'error_type': 'download_failed',
                        'video_url': best_video['url'] if best_video else '',
                        'intercepted_urls': len(self.video_urls),
                        'candidates': [v.get('url', '') for v in ranked_videos[:10]]
                    }

            except PlaywrightTimeout:
                await close_and_persist()
                return {
                    'success': False,
                    'error': f'Page load timed out after {self.timeout}s',
                    'error_type': 'timeout'
                }
            except Exception as e:
                await close_and_persist()
                return {
                    'success': False,
                    'error': str(e),
                    'error_type': 'browser_error'
                }


async def main():
    parser = argparse.ArgumentParser(description='Browser-based video downloader')
    parser.add_argument('url', help='Video page URL')
    parser.add_argument('output_dir', help='Output directory')
    parser.add_argument('--timeout', type=int, default=120, help='Timeout in seconds')
    parser.add_argument('--headed', action='store_true', help='Run browser in headed mode (use with xvfb-run for silent operation)')
    parser.add_argument('--cdp-endpoint', type=str, default=None, help='Connect to an existing Chrome via CDP (e.g. http://127.0.0.1:9223)')
    parser.add_argument('--bootstrap-windows-chrome', action='store_true', help='Start dedicated Windows Chrome with remote debugging if CDP is unavailable')
    parser.add_argument('--windows-profile-dir', type=str, default=None, help='Windows Chrome user-data-dir for dedicated automation instance')
    parser.add_argument('--cdp-port', type=int, default=9223, help='CDP port used when bootstrapping Windows Chrome')

    args = parser.parse_args()

    interceptor = VideoInterceptor(
        args.output_dir,
        args.timeout,
        headed=args.headed,
        cdp_endpoint=args.cdp_endpoint,
        bootstrap_windows_chrome=args.bootstrap_windows_chrome,
        windows_profile_dir=args.windows_profile_dir,
        cdp_port=args.cdp_port
    )
    hard_timeout_seconds = max(args.timeout + 15, 30)
    try:
        result = await asyncio.wait_for(interceptor.run(args.url), timeout=hard_timeout_seconds)
    except asyncio.TimeoutError:
        result = {
            'success': False,
            'error': f'Browser pipeline hard timeout after {hard_timeout_seconds}s',
            'error_type': 'timeout',
            'stage': interceptor.last_stage
        }
    output_json(result)


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