#!/usr/bin/env python3
"""Browser control utility via Playwright CDP sessions."""

from __future__ import annotations

import argparse
import asyncio
import json
import os
import re
import signal
import subprocess
import time
from collections.abc import Callable
from pathlib import Path

SESSION_FILE = "/tmp/playwright-session.json"
SESSION_DIR = "/tmp/pw-browser-session"
DEFAULT_OUT = str(Path(os.environ.get("WORKSPACE_ROOT", str(Path(__file__).resolve().parent.parent))) / "dashboard/screenshot-live.png")
CHROMIUM = os.path.expanduser("~/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome")
CDP_PORT = 9222

# ⚠️ 보안 경고: 안티봇 우회 기법은 합법적 용도에만 사용하세요. 사이트 ToS를 확인하세요.
STEALTH_ARGS: tuple[str, ...] = (
    "--test-type",
    "--lang=en-US",
    "--mute-audio",
    "--disable-sync",
    "--hide-scrollbars",
    "--disable-logging",
    "--start-maximized",
    "--enable-async-dns",
    "--accept-lang=en-US",
    "--use-mock-keychain",
    "--disable-translate",
    "--disable-voice-input",
    "--window-position=0,0",
    "--disable-wake-on-wifi",
    "--ignore-gpu-blocklist",
    "--enable-tcp-fast-open",
    "--enable-web-bluetooth",
    "--disable-cloud-import",
    "--disable-print-preview",
    "--disable-dev-shm-usage",
    "--metrics-recording-only",
    "--disable-crash-reporter",
    "--disable-partial-raster",
    "--disable-gesture-typing",
    "--disable-checker-imaging",
    "--disable-prompt-on-repost",
    "--force-color-profile=srgb",
    "--font-render-hinting=none",
    "--aggressive-cache-discard",
    "--disable-cookie-encryption",
    "--disable-domain-reliability",
    "--disable-threaded-animation",
    "--disable-threaded-scrolling",
    "--enable-simple-cache-backend",
    "--disable-background-networking",
    "--enable-surface-synchronization",
    "--disable-image-animation-resync",
    "--disable-renderer-backgrounding",
    "--disable-ipc-flooding-protection",
    "--prerender-from-omnibox=disabled",
    "--safebrowsing-disable-auto-update",
    "--disable-offer-upload-credit-cards",
    "--disable-background-timer-throttling",
    "--disable-new-content-rendering-timeout",
    "--run-all-compositor-stages-before-draw",
    "--disable-client-side-phishing-detection",
    "--disable-backgrounding-occluded-windows",
    "--disable-layer-tree-host-memory-pressure",
    "--autoplay-policy=user-gesture-required",
    "--disable-offer-store-unmasked-wallet-cards",
    "--disable-blink-features=AutomationControlled",
    "--disable-component-extensions-with-background-pages",
    "--enable-features=NetworkService,NetworkServiceInProcess,TrustTokens,TrustTokensAlwaysAllowIssuance",
    "--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4",
    "--disable-features=AudioServiceOutOfProcess,TranslateUI,BlinkGenPropertyTrees",
)

HARMFUL_ARGS: tuple[str, ...] = (
    "--enable-automation",
    "--disable-popup-blocking",
    "--disable-component-update",
    "--disable-default-apps",
    "--disable-extensions",
)

BLOCKED_RESOURCE_TYPES: set[str] = {"image", "font", "media", "stylesheet", "other"}


def generate_stealth_headers(browser_mode: bool = False) -> dict[str, str]:
    """browserforge로 실제 Chrome 헤더 생성."""
    try:
        import platform

        from browserforge.headers import HeaderGenerator

        os_name = platform.system().lower()
        if os_name == "darwin":
            os_name = "macos"
        elif os_name not in ("linux", "windows", "macos"):
            os_name = "linux"  # fallback

        if browser_mode:
            gen = HeaderGenerator(browser="chrome", os=os_name, device="desktop")
        else:
            gen = HeaderGenerator(browser=("chrome", "firefox", "edge"), os=os_name, device="desktop")

        headers = gen.generate()
        return dict(headers)
    except ImportError:
        # browserforge not installed, return minimal headers
        return {
            "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
        }


def get_google_referer() -> str:
    """Google 검색 referer URL 반환."""
    return "https://www.google.com/"


def create_resource_blocker(block_types: set[str] | None = None) -> Callable:
    """리소스 차단 async route handler 반환."""
    types_to_block = block_types if block_types is not None else BLOCKED_RESOURCE_TYPES

    async def handler(route):
        if route.request.resource_type in types_to_block:
            await route.abort()
        else:
            await route.continue_()

    return handler


def jout(d):
    print(json.dumps(d, ensure_ascii=False))


def load_session():
    try:
        with open(SESSION_FILE) as f:
            return json.load(f)
    except Exception:
        return None


def save_session(pid, port):
    with open(SESSION_FILE, "w") as f:
        json.dump({"pid": pid, "port": port}, f)


def clear_session():
    try:
        os.remove(SESSION_FILE)
    except FileNotFoundError:
        pass


def alive(pid):
    try:
        os.kill(pid, 0)
        return True
    except Exception:
        return False


def parse_vp(s):
    try:
        w, h = s.lower().split("x")
        return int(w), int(h)
    except Exception:
        return 1280, 720


def launch_chrome(port, stealth=False):
    """Launch headless Chrome with CDP and return PID."""
    os.makedirs(SESSION_DIR, exist_ok=True)
    args = [
        CHROMIUM,
        "--headless",
        "--no-sandbox",
        f"--remote-debugging-port={port}",
        f"--user-data-dir={SESSION_DIR}",
        "about:blank",
    ]
    if stealth:
        args = (
            [CHROMIUM]
            + list(STEALTH_ARGS)
            + [
                "--headless",
                "--no-sandbox",
                f"--remote-debugging-port={port}",
                f"--user-data-dir={SESSION_DIR}",
                "about:blank",
            ]
        )
    proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    time.sleep(1.5)
    if proc.poll() is not None:
        raise RuntimeError("Chrome failed to start")
    return proc.pid


async def get_page(pw, args):
    stealth = getattr(args, "stealth", False)
    block_resources = getattr(args, "block_resources", False)
    remote_cdp = getattr(args, "remote_cdp", None)

    if stealth:
        vp = (1920, 1080)
    else:
        vp = parse_vp(args.viewport)

    # 원격 CDP 연결 (우선 처리)
    if remote_cdp:
        b = await pw.chromium.connect_over_cdp(remote_cdp)
        ctx = b.contexts[0] if b.contexts else await b.new_context()
        pg = ctx.pages[0] if ctx.pages else await ctx.new_page()
        pg.set_default_timeout(args.timeout)
        await pg.set_viewport_size({"width": vp[0], "height": vp[1]})
        if block_resources:
            await pg.route("**/*", create_resource_blocker())
        return b, pg

    sess = load_session()
    if sess and alive(sess["pid"]):
        try:
            b = await pw.chromium.connect_over_cdp(f"http://127.0.0.1:{sess['port']}")
            ctx = b.contexts[0] if b.contexts else await b.new_context()
            pg = ctx.pages[0] if ctx.pages else await ctx.new_page()
            pg.set_default_timeout(args.timeout)
            await pg.set_viewport_size({"width": vp[0], "height": vp[1]})
            if block_resources:
                await pg.route("**/*", create_resource_blocker())
            return b, pg
        except Exception:
            try:
                os.kill(sess["pid"], signal.SIGTERM)
            except Exception:
                pass
            clear_session()

    pid = launch_chrome(CDP_PORT, stealth=stealth)
    save_session(pid, CDP_PORT)
    b = await pw.chromium.connect_over_cdp(f"http://127.0.0.1:{CDP_PORT}")

    ctx_opts: dict = {"viewport": {"width": vp[0], "height": vp[1]}}
    if stealth:
        ctx_opts["device_scale_factor"] = 2
        ctx_opts["screen"] = {"width": 1920, "height": 1080}
        headers = generate_stealth_headers(browser_mode=True)
        headers["Referer"] = get_google_referer()
        ctx_opts["extra_http_headers"] = headers
        ctx_opts["user_agent"] = headers.get("User-Agent", "")

    ctx = b.contexts[0] if b.contexts else await b.new_context(**ctx_opts)
    pg = ctx.pages[0] if ctx.pages else await ctx.new_page()
    pg.set_default_timeout(args.timeout)
    await pg.set_viewport_size({"width": vp[0], "height": vp[1]})

    if block_resources:
        await pg.route("**/*", create_resource_blocker())

    return b, pg


async def shot(pg, path, full=False):
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    await pg.screenshot(path=path, full_page=full)
    return path


async def info(pg):
    return {"url": pg.url, "title": await pg.title()}


# ── commands ────────────────────────────────────────────────────────────────


async def cmd_navigate(a, pw):
    _, pg = await get_page(pw, a)
    await pg.goto(a.url, wait_until="domcontentloaded", timeout=a.timeout)
    await pg.wait_for_timeout(a.wait)
    jout({"status": "ok", "screenshot": await shot(pg, a.output), **await info(pg)})


async def cmd_click(a, pw):
    _, pg = await get_page(pw, a)
    loc = pg.get_by_text(a.text, exact=False) if a.text else pg.locator(a.selector)
    el = loc.nth(a.nth) if a.nth is not None else loc.first
    tag, vis = "unknown", ""
    try:
        await el.evaluate("e => e.style.outline='3px solid red'")
        await shot(pg, a.output)
        tag = await el.evaluate("e => e.tagName.toLowerCase()")
        vis = (await el.inner_text())[:80]
    except Exception:
        pass
    await el.click()
    await pg.wait_for_timeout(a.wait)
    jout(
        {
            "status": "ok",
            "screenshot": await shot(pg, a.output),
            "element": tag,
            "text": vis,
            **await info(pg),
        }
    )


async def cmd_type(a, pw):
    _, pg = await get_page(pw, a)
    loc = pg.locator(a.selector).first
    await loc.fill(a.text)
    await pg.wait_for_timeout(a.wait)
    try:
        tag = await loc.evaluate("e => e.tagName.toLowerCase()")
    except Exception:
        tag = "input"
    jout(
        {
            "status": "ok",
            "screenshot": await shot(pg, a.output),
            "element": tag,
            "text": a.text,
            **await info(pg),
        }
    )


async def cmd_scroll(a, pw):
    _, pg = await get_page(pw, a)
    js = {
        "down": "window.scrollBy(0,window.innerHeight*.8)",
        "up": "window.scrollBy(0,-window.innerHeight*.8)",
        "top": "window.scrollTo(0,0)",
        "bottom": "window.scrollTo(0,document.body.scrollHeight)",
    }[a.direction]
    await pg.evaluate(js)
    await pg.wait_for_timeout(a.wait)
    jout({"status": "ok", "screenshot": await shot(pg, a.output), **await info(pg)})


async def cmd_screenshot(a, pw):
    _, pg = await get_page(pw, a)
    jout({"status": "ok", "screenshot": await shot(pg, a.output, a.full), **await info(pg)})


async def cmd_eval(a, pw):
    _, pg = await get_page(pw, a)
    result = await pg.evaluate(a.js)
    jout({"status": "ok", "screenshot": await shot(pg, a.output), "result": result, **await info(pg)})


async def cmd_close(a, pw):
    sess = load_session()
    if sess and alive(sess["pid"]):
        try:
            os.kill(sess["pid"], signal.SIGTERM)
        except Exception:
            pass
    clear_session()
    jout({"status": "ok", "message": "session closed"})


# ── argparse ────────────────────────────────────────────────────────────────


def build_parser():
    common = argparse.ArgumentParser(add_help=False)
    common.add_argument("--output", default=DEFAULT_OUT)
    common.add_argument("--viewport", default="1280x720")
    common.add_argument("--wait", type=int, default=1000)
    common.add_argument("--timeout", type=int, default=10000)
    common.add_argument("--stealth", action="store_true", help="Enable stealth mode (anti-bot detection)")
    common.add_argument("--block-resources", action="store_true", help="Block images/fonts/media/CSS loading")
    p = argparse.ArgumentParser(description="Browser control via Playwright", parents=[common])
    p.add_argument(
        "--remote-cdp", type=str, default=None, help="Remote CDP endpoint URL (e.g., http://100.116.204.95:9222)"
    )
    s = p.add_subparsers(dest="command", required=True)
    s.add_parser("navigate", parents=[common]).add_argument("url")
    clk = s.add_parser("click", parents=[common])
    clk.add_argument("selector", nargs="?", default="body")
    clk.add_argument("--text")
    clk.add_argument("--nth", type=int, default=None)
    typ = s.add_parser("type", parents=[common])
    typ.add_argument("selector")
    typ.add_argument("text")
    s.add_parser("scroll", parents=[common]).add_argument("direction", choices=["up", "down", "top", "bottom"])
    s.add_parser("screenshot", parents=[common]).add_argument("--full", action="store_true")
    s.add_parser("eval", parents=[common]).add_argument("js")
    s.add_parser("close", parents=[common])
    return p


HANDLERS = {
    "navigate": cmd_navigate,
    "click": cmd_click,
    "type": cmd_type,
    "scroll": cmd_scroll,
    "screenshot": cmd_screenshot,
    "eval": cmd_eval,
    "close": cmd_close,
}


async def main():
    from playwright.async_api import async_playwright

    args = build_parser().parse_args()
    try:
        async with async_playwright() as pw:
            await HANDLERS[args.command](args, pw)
    except Exception as e:
        jout({"status": "error", "message": str(e)})


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