#!/usr/bin/env python3
"""
Instagram browser-session scraper.
Collects media URLs from live network/DOM while scrolling profile pages.

Usage:
  python instagram_scrape.py <profile_url> <output_dir> [--start-index 0] [--max-items 100]
"""

import argparse
import asyncio
import json
import os
import random
import re
import subprocess
import time
import urllib.parse
import urllib.request
from pathlib import Path

try:
    from playwright.async_api import async_playwright
except Exception:
    print(json.dumps({
        'success': False,
        'error': 'Playwright not installed. Run: pip install playwright && playwright install chromium',
        'error_type': 'missing_dependency'
    }))
    raise SystemExit(1)

DEFAULT_WINDOWS_PROFILE_DIR = 'C:\\Users\\Rampage\\AppData\\Local\\GrabbitChromeProfile'


def _sanitize_windows_profile_dir(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(endpoint: str) -> bool:
    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(cdp_port: int, windows_profile_dir: str) -> bool:
    profile_arg = windows_profile_dir.replace("'", "''")
    cmd = (
        "Start-Process -FilePath 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' "
        f"-ArgumentList '--remote-debugging-port={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)

    endpoint = f'http://127.0.0.1:{cdp_port}'
    for _ in range(20):
        if _is_cdp_ready(endpoint):
            return True
        try:
            import time
            time.sleep(0.5)
        except Exception:
            pass
    return False


def _safe_ext_from_url(url: str) -> str:
    parsed = urllib.parse.urlparse(url)
    base = os.path.basename(parsed.path)
    if '.' in base:
        ext = base.rsplit('.', 1)[-1].lower()
        if ext in ('jpg', 'jpeg', 'png', 'gif', 'webp', 'mp4', 'webm'):
            return f'.{ext}'
    return '.jpg'


def _normalize_media_key(url: str) -> str:
    try:
        parsed = urllib.parse.urlparse(url)
        return f'{parsed.scheme}://{parsed.netloc}{parsed.path}'
    except Exception:
        return url


async def scrape_profile(
    profile_url: str,
    output_dir: str,
    start_index: int,
    max_items: int,
    cdp_endpoint: str | None,
    bootstrap_windows_chrome: bool,
    windows_profile_dir: str,
    cdp_port: int,
    scroll_rounds: int,
    total_timeout: int,
    request_retries: int,
    request_backoff: float,
) -> dict:
    out_dir = Path(output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    collected: list[str] = []
    seen = set()

    def add_url(u: str):
        if not u or not isinstance(u, str):
            return
        url = u.strip()
        if not url:
            return
        if 'cdninstagram' not in url and 'fbcdn.net' not in url:
            return
        if '/vp/' not in url and '/v/' not in url and '/t51.2885-15/' not in url:
            return
        media_key = _normalize_media_key(url)
        if media_key in seen:
            return
        seen.add(media_key)
        collected.append(url)

    async with async_playwright() as p:
        using_cdp = cdp_endpoint is not None

        if bootstrap_windows_chrome and cdp_endpoint is None:
            endpoint = f'http://127.0.0.1:{cdp_port}'
            if not _is_cdp_ready(endpoint):
                _start_windows_chrome(cdp_port, _sanitize_windows_profile_dir(windows_profile_dir))
            cdp_endpoint = endpoint
            using_cdp = True

        browser = None
        context = None
        if using_cdp:
            try:
                browser = await asyncio.wait_for(
                    p.chromium.connect_over_cdp(cdp_endpoint),
                    timeout=8
                )
                context = browser.contexts[0] if browser.contexts else await browser.new_context()
            except Exception:
                using_cdp = False

        if not using_cdp:
            browser = await p.chromium.launch(headless=True, args=['--no-sandbox'])
            context = await browser.new_context(
                viewport={'width': 1366, 'height': 2000},
                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',
            )

        page = await context.new_page()

        async def handle_response(response):
            try:
                url = response.url
                if not url:
                    return
                headers = response.headers
                ctype = (headers.get('content-type') or '').lower()
                if not (ctype.startswith('image/') or ctype.startswith('video/')):
                    return
                if response.status != 200:
                    return
                add_url(url)
            except Exception:
                pass

        def on_response(response):
            task = asyncio.create_task(handle_response(response))
            task.add_done_callback(lambda t: t.exception())

        page.on('response', on_response)

        started = time.monotonic()
        await page.goto(profile_url, wait_until='domcontentloaded', timeout=45000)
        await page.wait_for_timeout(1500)

        async def collect_from_dom():
            dom_urls = await page.evaluate("""() => {
                const urls = [];
                const imgs = Array.from(document.querySelectorAll('img'));
                for (const img of imgs) {
                    if (img.currentSrc) urls.push(img.currentSrc);
                    if (img.src) urls.push(img.src);
                    if (img.srcset) {
                        for (const part of img.srcset.split(',')) {
                            const u = part.trim().split(' ')[0];
                            if (u) urls.push(u);
                        }
                    }
                }
                return urls;
            }""")
            for u in dom_urls:
                add_url(u)

        await collect_from_dom()

        stable_rounds = 0
        last_count = len(collected)
        target_needed = start_index + (max_items if max_items > 0 else 200)
        target_with_buffer = target_needed + 20

        for _ in range(scroll_rounds):
            if time.monotonic() - started > total_timeout:
                break
            await page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
            await page.wait_for_timeout(900 + int(random.random() * 400))
            await collect_from_dom()

            current_count = len(collected)
            if current_count == last_count:
                stable_rounds += 1
            else:
                stable_rounds = 0
            last_count = current_count

            enough_collected = len(collected) >= target_with_buffer
            if enough_collected and stable_rounds >= 2:
                break

            if stable_rounds >= 6:
                break

        sliced = collected[start_index:]
        if max_items > 0:
            sliced = sliced[:max_items]

        downloaded = 0
        for idx, media_url in enumerate(sliced, start=1):
            if time.monotonic() - started > total_timeout:
                break
            ext = _safe_ext_from_url(media_url)
            filename = f'instagram-{start_index + idx:05d}{ext}'
            out_path = out_dir / filename
            if out_path.exists() and out_path.stat().st_size > 0:
                downloaded += 1
                continue

            for attempt in range(max(1, request_retries)):
                try:
                    response = await asyncio.wait_for(
                        context.request.get(media_url, headers={'Referer': profile_url}),
                        timeout=12
                    )
                    if not response.ok:
                        status = response.status
                        if status in (429, 500, 502, 503, 504) and attempt + 1 < request_retries:
                            await asyncio.sleep(request_backoff * (2 ** attempt))
                            continue
                        break

                    body = await asyncio.wait_for(response.body(), timeout=12)
                    if not body:
                        break

                    out_path.write_bytes(body)
                    downloaded += 1
                    print(f'[IG-SCRAPE] downloaded {downloaded}/{len(sliced)} {filename}')
                    break
                except Exception:
                    if attempt + 1 < request_retries:
                        await asyncio.sleep(request_backoff * (2 ** attempt))
                    else:
                        break

        if not using_cdp:
            await browser.close()

        success = downloaded > 0 or len(sliced) == 0
        result = {
            'success': success,
            'collected': len(collected),
            'selected': len(sliced),
            'downloaded': downloaded
        }
        if not success:
            result['error'] = 'No media downloaded from selected range'
        return result


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('profile_url')
    parser.add_argument('output_dir')
    parser.add_argument('--start-index', type=int, default=0)
    parser.add_argument('--max-items', type=int, default=0)
    parser.add_argument('--cdp-endpoint', type=str, default=None)
    parser.add_argument('--bootstrap-windows-chrome', action='store_true')
    parser.add_argument('--windows-profile-dir', type=str, default=DEFAULT_WINDOWS_PROFILE_DIR)
    parser.add_argument('--cdp-port', type=int, default=9223)
    parser.add_argument('--scroll-rounds', type=int, default=45)
    parser.add_argument('--total-timeout', type=int, default=120)
    parser.add_argument('--request-retries', type=int, default=3)
    parser.add_argument('--request-backoff', type=float, default=0.8)
    parser.add_argument('--check', action='store_true')
    args = parser.parse_args()

    if args.check:
        print(json.dumps({'success': True, 'engine': 'playwright-inspector'}))
        return

    if args.start_index < 0:
        print(json.dumps({'success': False, 'error': 'start-index must be >= 0'}))
        raise SystemExit(2)

    if args.max_items < 0:
        print(json.dumps({'success': False, 'error': 'max-items must be >= 0'}))
        raise SystemExit(2)

    result = asyncio.run(scrape_profile(
        args.profile_url,
        args.output_dir,
        args.start_index,
        args.max_items,
        args.cdp_endpoint,
        args.bootstrap_windows_chrome,
        args.windows_profile_dir,
        args.cdp_port,
        args.scroll_rounds,
        args.total_timeout,
        args.request_retries,
        args.request_backoff,
    ))
    print(json.dumps(result))
    raise SystemExit(0 if result.get('success') else 1)


if __name__ == '__main__':
    main()
