#!/usr/bin/env python3
"""
clark-browser (stealth Chromium) launcher for manual browsing.

Chromium engine counterpart to launch_camoufox.py. Used for profiles whose
browser_engine is "chromium" — sites like Wise/PayPal that crash the Firefox
engine load correctly here, and each profile gets a unique, deterministic
C++-level fingerprint derived from its stored fingerprint_seed.

Launches native Chromium via subprocess (not Playwright) so cookies, history,
passwords, and tab restore behave like normal Chrome. Playwright is only used
for a one-shot cookie import on brand-new empty profiles.

Reads a LaunchConfig JSON (same shape as the Camoufox launcher) from argv[1].
"""

import hashlib
import json
import os
import plistlib
import shutil
import signal
import socket
import subprocess
import sys
import threading
import time
from typing import Dict, List, Optional, Any
from urllib.request import urlopen
from urllib.error import URLError


def _hide_from_dock_macos() -> None:
    """Prevent this launcher process from showing its own Dock icon on macOS."""
    if sys.platform != "darwin":
        return
    try:
        from ctypes import Structure, byref, c_int, c_uint32, cdll

        class ProcessSerialNumber(Structure):
            _fields_ = [
                ("highLongOfPSN", c_uint32),
                ("lowLongOfPSN", c_uint32),
            ]

        carbon = cdll.LoadLibrary(
            "/System/Library/Frameworks/Carbon.framework/Carbon"
        )
        psn = ProcessSerialNumber()
        carbon.GetCurrentProcess(byref(psn))
        # kProcessTransformToUIElementApplication — no Dock tile.
        carbon.TransformProcessType(byref(psn), c_int(4))
    except Exception:
        pass


# Must run before Playwright connects to the window server.
_hide_from_dock_macos()

RUNNING_MARKER = ".cpm-running"


# ---------------------------------------------------------------------------
# Cookie parsing (Netscape cookies.txt / Playwright / GenLogin / Cookie-Editor)
# Kept in sync with launch_camoufox.py so imported cookies behave identically
# on both engines.
# ---------------------------------------------------------------------------
def _normalize_samesite(raw, secure: bool) -> Optional[str]:
    if raw is None or str(raw).lower() in ("unspecified", "null", ""):
        return "None" if secure else "Lax"
    s = str(raw).lower()
    if s in ("no_restriction", "none"):
        return "None"
    if s == "strict":
        return "Strict"
    if s == "lax":
        return "Lax"
    if raw in ("Strict", "Lax", "None"):
        return raw
    return "None" if secure else "Lax"


def _normalize_expires(c: dict) -> Optional[int]:
    raw = c.get("expires", c.get("expirationDate"))
    if not isinstance(raw, (int, float)) or raw <= 0:
        return None
    return int(raw)


def _parse_json_cookie(c: dict) -> Optional[dict]:
    if "name" not in c or "value" not in c:
        return None
    domain = str(c.get("domain", "")).strip()
    if not domain:
        return None
    secure = bool(c.get("secure"))
    cookie = {
        "name": str(c["name"]),
        "value": str(c["value"]),
        "domain": domain,
        "path": str(c.get("path", "/")),
    }
    expires = _normalize_expires(c)
    if expires is not None:
        cookie["expires"] = expires
    if "httpOnly" in c:
        cookie["httpOnly"] = bool(c["httpOnly"])
    cookie["secure"] = secure
    same_site = _normalize_samesite(c.get("sameSite"), secure)
    if same_site:
        cookie["sameSite"] = same_site
    return cookie


def _parse_cookies(text: str) -> list:
    """Parse cookies from Netscape cookies.txt or a JSON array."""
    text = text.strip()
    if not text:
        return []

    if text.startswith("["):
        try:
            data = json.loads(text)
            if not isinstance(data, list):
                return []
            out = []
            for c in data:
                if isinstance(c, dict):
                    parsed = _parse_json_cookie(c)
                    if parsed:
                        out.append(parsed)
            return out
        except json.JSONDecodeError:
            return []

    out = []
    for line in text.splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        parts = line.split("\t")
        if len(parts) < 7:
            continue
        domain, _flag, path, secure_str, expires_str, name, value = (
            parts[0], parts[1], parts[2], parts[3], parts[4], parts[5],
            "\t".join(parts[6:]),
        )
        try:
            expires_num = int(expires_str)
        except ValueError:
            expires_num = 0
        cookie = {
            "name": name,
            "value": value,
            "domain": domain or "",
            "path": path or "/",
        }
        if expires_num > 0:
            cookie["expires"] = expires_num
        cookie["secure"] = secure_str == "TRUE"
        cookie["sameSite"] = "None" if cookie["secure"] else "Lax"
        if cookie.get("domain"):
            out.append(cookie)
    return out


# ---------------------------------------------------------------------------
# Session lifecycle helpers (Electron watches the .cpm-running marker)
# ---------------------------------------------------------------------------
def _marker_path(profile_dir: str) -> str:
    return os.path.join(profile_dir, RUNNING_MARKER)


def _touch_marker(profile_dir: str) -> None:
    try:
        path = _marker_path(profile_dir)
        with open(path, "w") as f:
            f.write(str(os.getpid()))
        os.utime(path, None)
    except Exception:
        pass


def _remove_marker(profile_dir: str) -> None:
    try:
        os.remove(_marker_path(profile_dir))
    except Exception:
        pass


def _force_exit_after(seconds: float, code: int = 0) -> None:
    """Guarantee the launcher dies after the browser closes (teardown can hang)."""

    def _kill() -> None:
        time.sleep(seconds)
        os._exit(code)

    threading.Thread(target=_kill, daemon=True).start()


def _profile_cookie_count(profile_data_dir: str) -> int:
    """Return how many cookies are already stored in the Chromium profile."""
    cookies_db = os.path.join(profile_data_dir, "Default", "Cookies")
    if not os.path.isfile(cookies_db):
        return 0
    try:
        import sqlite3

        conn = sqlite3.connect(f"file:{cookies_db}?mode=ro", uri=True, timeout=1.0)
        try:
            return int(conn.execute("select count(*) from cookies").fetchone()[0])
        finally:
            conn.close()
    except Exception:
        return 0


def _profile_is_initialized(profile_data_dir: str) -> bool:
    """True once this profile dir has been used for a normal browsing session."""
    if os.path.isfile(os.path.join(profile_data_dir, "Local State")):
        return True
    if _has_saved_session(profile_data_dir):
        return True
    history_db = os.path.join(profile_data_dir, "Default", "History")
    if os.path.isfile(history_db) and os.path.getsize(history_db) > 32768:
        return True
    cookies_db = os.path.join(profile_data_dir, "Default", "Cookies")
    if os.path.isfile(cookies_db) and os.path.getsize(cookies_db) > 32768:
        return True
    return False


def _should_import_config_cookies(profile_data_dir: str, cookies_raw: str) -> bool:
    """
    Import cookies from the app profile editor only on the first launch.

    Re-importing on every launch overwrites fresh login cookies (userToken,
    oauthToken, etc.) with stale values from the editor and logs the user out.
    Never run Playwright import on an initialized profile — even when the
    Cookies DB reads empty (WAL not flushed), Playwright's automation launch
    can wipe history and session data.
    """
    if not cookies_raw.strip():
        return False
    if _profile_is_initialized(profile_data_dir):
        return False
    return _profile_cookie_count(profile_data_dir) == 0


def _has_saved_session(profile_data_dir: str) -> bool:
    """True when Chromium has a previous session to restore."""
    sessions_dir = os.path.join(profile_data_dir, "Default", "Sessions")
    if not os.path.isdir(sessions_dir):
        return False
    try:
        for name in os.listdir(sessions_dir):
            if not name.startswith("Session_"):
                continue
            path = os.path.join(sessions_dir, name)
            if os.path.isfile(path) and os.path.getsize(path) > 100:
                return True
    except Exception:
        pass
    return False


def _mark_clean_exit(profile_data_dir: str) -> None:
    """Mark the profile as cleanly exited so tabs/history restore on next launch."""
    try:
        prefs_path = os.path.join(profile_data_dir, "Default", "Preferences")
        if not os.path.isfile(prefs_path):
            return
        with open(prefs_path, "r", encoding="utf-8") as f:
            prefs = json.load(f)
        if not isinstance(prefs, dict):
            return

        profile = prefs.get("profile")
        if not isinstance(profile, dict):
            profile = {}
        profile["exit_type"] = "Normal"
        profile["exited_cleanly"] = True
        prefs["profile"] = profile

        session = prefs.get("session")
        if not isinstance(session, dict):
            session = {}
        session["restore_on_startup"] = 1
        session.pop("startup_urls", None)
        prefs["session"] = session

        with open(prefs_path, "w", encoding="utf-8") as f:
            json.dump(prefs, f)
    except Exception:
        pass


def _graceful_close_context(context, profile_data_dir: str) -> None:
    """Close Chromium and give it time to flush cookies, history, and sessions."""
    try:
        context.close()
    except Exception:
        pass
    # Playwright kills the browser quickly; wait so SQLite DBs flush to disk.
    time.sleep(2.5)
    _mark_clean_exit(profile_data_dir)


def _close_blank_pages(context) -> None:
    """Drop Playwright's initial about:blank tab after session restore."""
    try:
        pages = list(context.pages)
    except Exception:
        return
    if len(pages) <= 1:
        return
    for page in pages:
        try:
            if page.url in ("about:blank", "chrome://newtab/"):
                page.close()
        except Exception:
            pass


def _wait_until_browser_closed(context, profile_dir: str) -> str:
    """Block until the user closes the browser. Returns 'closed' or 'crashed'."""
    done = threading.Event()

    def _on_close(*_args) -> None:
        done.set()

    try:
        context.on("close", _on_close)
    except Exception:
        pass

    while not done.is_set():
        try:
            context.cookies()  # cheap round-trip; raises when browser is gone
        except Exception:
            return "crashed"
        try:
            if not list(context.pages):
                return "closed"
        except Exception:
            return "crashed"
        _touch_marker(profile_dir)
        time.sleep(0.5)

    return "closed"


def _safe_window_size(screen_w: int, screen_h: int) -> tuple:
    w = max(900, int(screen_w))
    h = max(650, int(screen_h))
    try:
        from screeninfo import get_monitors

        mons = get_monitors()
        if mons:
            mon = max(mons, key=lambda m: m.width * m.height)
            w = min(w, int(mon.width) - 40)
            h = min(h, int(mon.height) - 90)
    except Exception:
        pass
    return w, h


# ---------------------------------------------------------------------------
# Chromium rebrand (Antidetect Profiles)
#
# clark ships a stock "Chromium.app". macOS derives the Dock tile, process name
# and application menu from the bundle FOLDER NAME and CFBundleExecutable, so we
# rename the bundle + its main executable to "Antidetect Profiles" and point
# clark at it via CLARK_BINARY_PATH. Idempotent and re-applied per clark version
# directory (survives clark auto-updates). A Chromium.app symlink is left behind
# so clark's default binary resolution keeps working.
# ---------------------------------------------------------------------------
_BRAND_NAME = "Antidetect Profiles"
_BRAND_BUNDLE_ID = "com.antidetectprofiles.browser"


def _brand_icns_path() -> Optional[str]:
    """Resolve the brand .icns (dev tree or Electron extraResources)."""
    launcher_dir = os.path.dirname(os.path.abspath(__file__))
    candidates = [
        os.path.join(launcher_dir, "..", "..", "assets", "branding", "navigator", "firefox.icns"),
        os.path.join(launcher_dir, "..", "..", "assets", "branding", "firefox.icns"),
        os.path.join(launcher_dir, "..", "..", "..", "assets", "branding", "navigator", "firefox.icns"),
        os.path.join(launcher_dir, "..", "..", "..", "assets", "branding", "firefox.icns"),
    ]
    for c in candidates:
        p = os.path.abspath(c)
        if os.path.isfile(p):
            return p
    return None


def _patch_chromium_info_plist(info_plist: str) -> None:
    try:
        with open(info_plist, "rb") as f:
            data = plistlib.load(f)
        changed = False
        for key in ("CFBundleName", "CFBundleDisplayName", "CFBundleExecutable"):
            if data.get(key) != _BRAND_NAME:
                data[key] = _BRAND_NAME
                changed = True
        if data.get("CFBundleIdentifier") != _BRAND_BUNDLE_ID:
            data["CFBundleIdentifier"] = _BRAND_BUNDLE_ID
            changed = True
        if changed:
            with open(info_plist, "wb") as f:
                plistlib.dump(data, f)
    except Exception:
        pass


def _brand_chromium_bundle(default_exec: str) -> str:
    """Rename clark's Chromium.app to Antidetect Profiles.app; return branded exec."""
    if sys.platform != "darwin" or not default_exec:
        return default_exec
    try:
        app_bundle = os.path.dirname(os.path.dirname(os.path.dirname(default_exec)))
        version_dir = os.path.dirname(os.path.realpath(app_bundle))
        orig = os.path.join(version_dir, "Chromium.app")
        branded = os.path.join(version_dir, f"{_BRAND_NAME}.app")

        # Rename the real bundle -> branded, leaving a Chromium.app symlink.
        if os.path.isdir(orig) and not os.path.islink(orig):
            if not os.path.exists(branded):
                os.rename(orig, branded)
            elif os.path.realpath(orig) != os.path.realpath(branded):
                shutil.rmtree(orig, ignore_errors=True)
            if os.path.isdir(orig) and not os.path.islink(orig):
                shutil.rmtree(orig, ignore_errors=True)
            if not os.path.exists(orig):
                os.symlink(f"{_BRAND_NAME}.app", orig)

        if not os.path.isdir(branded):
            return default_exec

        macos_dir = os.path.join(branded, "Contents", "MacOS")
        branded_exec = os.path.join(macos_dir, _BRAND_NAME)
        src_exec = os.path.join(macos_dir, "Chromium")
        if not os.path.isfile(branded_exec) and os.path.isfile(src_exec):
            shutil.copy2(src_exec, branded_exec)

        _patch_chromium_info_plist(os.path.join(branded, "Contents", "Info.plist"))

        icns = _brand_icns_path()
        if icns:
            dst_icon = os.path.join(branded, "Contents", "Resources", "app.icns")
            try:
                if not os.path.isfile(dst_icon) or os.path.getsize(dst_icon) != os.path.getsize(icns):
                    shutil.copy2(icns, dst_icon)
            except Exception:
                pass

        return branded_exec if os.path.isfile(branded_exec) else default_exec
    except Exception:
        return default_exec


def _monitor_size() -> tuple:
    """Real display size in points (falls back to a safe default)."""
    try:
        from screeninfo import get_monitors

        mons = get_monitors()
        if mons:
            m = max(mons, key=lambda x: x.width * x.height)
            return int(m.width), int(m.height)
    except Exception:
        pass
    return 1440, 900


def _usable_screen_rect() -> tuple:
    """
    Usable desktop area in screen points: (left, top, width, height).

    macOS menu bar + Dock are excluded so the browser window fits fully on
    screen. Profile fingerprint screen size is NOT used here — only the real
    display matters for window bounds.
    """
    mon_w, mon_h = _monitor_size()
    if sys.platform == "darwin":
        menu_bar = 38
        dock = max(64, int(mon_h * 0.07))
    else:
        menu_bar = 30
        dock = 48
    margin = 8
    width = max(900, mon_w - margin * 2)
    height = max(600, mon_h - menu_bar - dock - margin)
    return margin, menu_bar, width, height


def _patch_window_placement_prefs(prefs: dict) -> None:
    """Clamp saved Chromium window bounds so restore never reopens oversized."""
    left, top, win_w, win_h = _usable_screen_rect()
    mon_w, mon_h = _monitor_size()
    browser = prefs.get("browser")
    if not isinstance(browser, dict):
        browser = {}
    browser["window_placement"] = {
        "maximized": False,
        "left": left,
        "top": top,
        "right": left + win_w,
        "bottom": top + win_h,
        "work_area_left": 0,
        "work_area_top": top,
        "work_area_right": mon_w,
        "work_area_bottom": top + win_h,
    }
    prefs["browser"] = browser


def _fit_window(context, page, screen_w: int, screen_h: int) -> None:
    """
    Resize/reposition the Chromium window to fit the real display.

    A persistent Chromium profile restores its last saved window bounds and
    ignores --window-size, so a profile opened once on a bigger monitor can
    reopen larger than this Mac's screen. We force a fitting, fully-visible
    window via CDP on every launch (leaving room for the menu bar and Dock).
    """
    try:
        left, top, win_w, win_h = _usable_screen_rect()

        cdp = context.new_cdp_session(page)
        window_id = cdp.send("Browser.getWindowForTarget")["windowId"]
        try:
            cdp.send(
                "Browser.setWindowBounds",
                {"windowId": window_id, "bounds": {"windowState": "normal"}},
            )
        except Exception:
            pass
        cdp.send(
            "Browser.setWindowBounds",
            {
                "windowId": window_id,
                "bounds": {
                    "left": left,
                    "top": top,
                    "width": win_w,
                    "height": win_h,
                    "windowState": "normal",
                },
            },
        )
        # Ensure 100% zoom — wrong zoom makes Wise layout look broken.
        try:
            cdp.send("Emulation.setPageScaleFactor", {"pageScaleFactor": 1})
        except Exception:
            pass
    except Exception:
        pass


def _fit_window_later(context, page, delay: float = 1.5) -> None:
    """Re-apply window fit after session restore (can override initial bounds)."""

    def _run() -> None:
        time.sleep(delay)
        try:
            target = page
            if target.is_closed():
                pages = list(context.pages)
                if not pages:
                    return
                target = pages[0]
            _fit_window(context, target, 0, 0)
        except Exception:
            pass

    threading.Thread(target=_run, daemon=True).start()


def _seed_chromium_prefs(
    profile_data_dir: str,
    *,
    defer_session_restore: bool = False,
) -> None:
    """
    Enable Chromium's password manager + form autofill and mark a clean exit.

    A Playwright-launched persistent profile leaves the built-in "Save
    password?" bubble and form autofill effectively off, so Chromium never
    offers to save logins or remember typed data (emails, etc.). We seed those
    (non-protected) prefs before launch to restore normal Chrome behavior, and
    force a clean exit state so the "Chrome didn't shut down correctly / restore
    pages?" nag doesn't appear. Applied every launch, merging into existing
    prefs so the fingerprint and other profile state are preserved.

    When defer_session_restore is True (session cookies sidecar present), start
    on the NTP so Chromium does not navigate Wise before CDP cookie import.
    Tabs are restored via CDP after cookies are injected.
    """
    try:
        default_dir = os.path.join(profile_data_dir, "Default")
        os.makedirs(default_dir, exist_ok=True)
        prefs_path = os.path.join(default_dir, "Preferences")

        prefs: dict = {}
        if os.path.isfile(prefs_path):
            try:
                with open(prefs_path, "r", encoding="utf-8") as f:
                    loaded = json.load(f)
                if isinstance(loaded, dict):
                    prefs = loaded
            except Exception:
                prefs = {}

        # Offer to save passwords + auto sign-in (root-level, non-protected).
        prefs["credentials_enable_service"] = True
        prefs["credentials_enable_autosignin"] = True

        profile = prefs.get("profile")
        if not isinstance(profile, dict):
            profile = {}
        profile["password_manager_enabled"] = True
        profile["exit_type"] = "Normal"
        profile["exited_cleanly"] = True
        profile["name"] = _BRAND_NAME
        prefs["profile"] = profile

        # restore_on_startup: 1 = last session, 4 = urls, 5 = NTP.
        # Defer Chromium's own restore when we must import cookies first.
        session = prefs.get("session")
        if not isinstance(session, dict):
            session = {}
        session["restore_on_startup"] = 5 if defer_session_restore else 1
        session.pop("startup_urls", None)
        prefs["session"] = session

        autofill = prefs.get("autofill")
        if not isinstance(autofill, dict):
            autofill = {}
        autofill["profile_enabled"] = True
        autofill["credit_card_enabled"] = True
        prefs["autofill"] = autofill

        # Keep offering to save passwords; unsigned Chromium still needs our
        # cleartext vault, but these prefs keep the built-in UI enabled.
        prefs["credentials_enable_service"] = True
        prefs["credentials_enable_autosignin"] = True
        password_manager = prefs.get("password_manager")
        if not isinstance(password_manager, dict):
            password_manager = {}
        password_manager["profile_store_login_database_was_migrated"] = True
        prefs["password_manager"] = password_manager

        # Do not keep Chromium alive in the background after the last window closes.
        background_mode = prefs.get("background_mode")
        if not isinstance(background_mode, dict):
            background_mode = {}
        background_mode["enabled"] = False
        prefs["background_mode"] = background_mode

        _patch_window_placement_prefs(prefs)

        with open(prefs_path, "w", encoding="utf-8") as f:
            json.dump(prefs, f)
    except Exception:
        pass


def _prepare_clark_stealth() -> None:
    """Drop flags that break normal Chrome behavior on macOS."""
    if sys.platform != "darwin":
        return
    try:
        import clarkbrowser.browser as _cb

        _orig_stealth = _cb.get_default_stealth_args
        if not getattr(_orig_stealth, "_cpm_no_sandbox_filtered", False):
            def _stealth_filtered():
                # Keep --use-mock-keychain: unsigned clark Chromium cannot use
                # the real macOS keychain ("Encryption is not available").
                return [a for a in _orig_stealth() if a != "--no-sandbox"]

            _stealth_filtered._cpm_no_sandbox_filtered = True
            _cb.get_default_stealth_args = _stealth_filtered
    except Exception:
        pass


def _chromium_executable() -> str:
    if sys.platform == "darwin" and not os.environ.get("CLARK_BINARY_PATH"):
        try:
            from clarkbrowser import ensure_binary

            branded = _brand_chromium_bundle(ensure_binary())
            if branded:
                os.environ["CLARK_BINARY_PATH"] = branded
        except Exception:
            pass
    if os.environ.get("CLARK_BINARY_PATH"):
        return os.environ["CLARK_BINARY_PATH"]
    from clarkbrowser import ensure_binary

    return ensure_binary()


def _resolve_chrome_args(
    user_args: List[str],
    timezone: Optional[str],
    locale: Optional[str],
    webrtc_policy: Optional[str],
) -> List[str]:
    from clarkbrowser.browser import _resolve_args

    return _resolve_args(
        user_args,
        stealth_args=True,
        timezone=timezone,
        locale=locale,
        network_profile=None,
        webrtc_policy=webrtc_policy,
        webgpu_policy="coherent",
        headless=False,
    )


# Stripe CDN hosts only. When a residential/datacenter proxy returns an HTML
# block page or wrong MIME for these, Chromium's Opaque Response Blocking
# (ORB) rejects the body and the page shows "Failed to load Stripe.js".
# PayPal and other payment UIs stay on the proxy.
# See: chromestatus ORB v0.2; SO 77358280 (ORB = wrong Content-Type body).
_PAYMENT_CDN_PROXY_BYPASS = (
    "*.stripe.com",
    "js.stripe.com",
    "m.stripe.com",
    "m.stripe.network",
    "r.stripe.com",
    "api.stripe.com",
    "hooks.stripe.com",
    "checkout.stripe.com",
    "q.stripe.com",
    "<local>",
)


def _payment_cdn_compatibility_args(*, has_proxy: bool) -> List[str]:
    """
    Best-effort Chromium args so Stripe billing CDNs can load.

    Web consensus (2024–2026): ORB cannot be turned off in modern Chromium —
    OpaqueResponseBlockingV0{1,2} were removed after shipping in M125
    (crrev.com/c/5173407). Historical CORB disable flags from
    chromium.org/Home/chromium-security/corb-for-developers are kept as
    harmless no-ops on Chrome 125+ (unknown features are ignored).

    Real-world Stripe.js ORB failures are usually wrong MIME / HTML block
    pages on the proxy path (SO 77358280), not a toggleable Chromium bug.
    When a profile proxy is set, bypass payment CDN hosts so those scripts
    use the direct path. Fingerprint flags are left intact — we do NOT use
    --disable-web-security (detectable / breaks stealth).
    """
    # Documented CORB disable flags. Removed as effective controls years ago;
    # ignored if unknown — safe to pass on current clark Chromium.
    args = [
        "--disable-features=CrossSiteDocumentBlockingAlways,"
        "CrossSiteDocumentBlockingIfIsolating,"
        "OpaqueResponseBlockingV01,"
        "OpaqueResponseBlockingV02,"
        "OpaqueResponseBlockingErrorsForAllFetches",
    ]
    if has_proxy:
        # Semicolon-separated per Chromium --proxy-bypass-list docs.
        bypass = ";".join(_PAYMENT_CDN_PROXY_BYPASS)
        args.append(f"--proxy-bypass-list={bypass}")
    return args


def _merge_disable_features(existing: Optional[str], extra: str) -> str:
    """Union comma-separated feature names from two --disable-features values."""
    parts: List[str] = []
    seen: set = set()
    for blob in (existing or "", extra or ""):
        for name in blob.split(","):
            name = name.strip()
            if not name or name in seen:
                continue
            seen.add(name)
            parts.append(name)
    return ",".join(parts)


def _apply_payment_cdn_args(resolved: List[str], *, has_proxy: bool) -> List[str]:
    """
    Always attach payment-CDN compatibility flags; merge --disable-features
    with any stealth/user values (clark _resolve_args replaces whole keys).
    """
    compat = _payment_cdn_compatibility_args(has_proxy=has_proxy)
    by_key: Dict[str, str] = {}
    for a in resolved:
        by_key[a.split("=", 1)[0]] = a

    for a in compat:
        key = a.split("=", 1)[0]
        if key == "--disable-features":
            extra = a.split("=", 1)[1] if "=" in a else ""
            prev = by_key.get("--disable-features", "")
            prev_val = prev.split("=", 1)[1] if "=" in prev else ""
            by_key[key] = f"--disable-features={_merge_disable_features(prev_val, extra)}"
        elif key not in by_key:
            by_key[key] = a
        # Existing --proxy-bypass-list from the caller wins; we only fill gaps.

    return list(by_key.values())


def _build_chromium_command(
    profile_data_dir: str,
    chrome_args: List[str],
    proxy_dict: Optional[dict],
    timezone: Optional[str],
    locale: Optional[str],
    webrtc_policy: Optional[str],
) -> List[str]:
    resolved = _resolve_chrome_args(chrome_args, timezone, locale, webrtc_policy)
    has_proxy = bool(proxy_dict and proxy_dict.get("server"))
    if has_proxy:
        resolved.append(f"--proxy-server={proxy_dict['server']}")
    # ORB disable-features always; proxy-bypass only when a proxy is configured.
    resolved = _apply_payment_cdn_args(resolved, has_proxy=has_proxy)
    return [
        _chromium_executable(),
        f"--user-data-dir={profile_data_dir}",
        *resolved,
    ]


def _import_cookies_if_needed(
    profile_data_dir: str,
    cookies_raw: str,
    launch_kwargs: dict,
) -> None:
    """One-shot Playwright import for empty profiles only; browser runs natively after."""
    if not _should_import_config_cookies(profile_data_dir, cookies_raw):
        return
    cookies_list = _parse_cookies(cookies_raw)
    if not cookies_list:
        return
    from clarkbrowser import launch_persistent_context

    ctx = launch_persistent_context(**launch_kwargs)
    try:
        ctx.add_cookies(cookies_list)
    except Exception as cookie_err:
        print(json.dumps({
            "success": False,
            "error": f"Invalid cookies: {cookie_err}",
        }))
        try:
            ctx.close()
        except Exception:
            pass
        sys.exit(1)
    try:
        ctx.close()
    except Exception:
        pass
    # Let Chromium flush imported cookies before the real (native) launch.
    time.sleep(3.0)


_PERSIST_DB_NAMES = ("History", "Cookies", "Login Data", "Web Data")
_SESSION_COOKIES_FILE = "cpm-session-cookies.json"
_SESSION_TABS_FILE = "cpm-session-tabs.json"
_CREDENTIALS_FILE = "cpm-credentials.json"
_AUTOFILL_FILE = "cpm-autofill.json"
# Wise device-trust cookies — must survive export or relaunch looks like a new device.
_WISE_CRITICAL_COOKIE_NAMES = frozenset({"deviceTokenTW", "appToken"})
# profile_data_dir -> critical cookie names observed mid-session (must not vanish on export)
_seen_wise_critical: Dict[str, set] = {}


def _session_cookies_path(profile_data_dir: str) -> str:
    return os.path.join(profile_data_dir, _SESSION_COOKIES_FILE)


def _session_tabs_path(profile_data_dir: str) -> str:
    return os.path.join(profile_data_dir, _SESSION_TABS_FILE)


def _credentials_path(profile_data_dir: str) -> str:
    return os.path.join(profile_data_dir, _CREDENTIALS_FILE)


def _autofill_path(profile_data_dir: str) -> str:
    return os.path.join(profile_data_dir, _AUTOFILL_FILE)


def _pick_free_port() -> int:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.bind(("127.0.0.1", 0))
        return int(sock.getsockname()[1])


def _wait_for_cdp(port: int, timeout: float = 25.0) -> bool:
    deadline = time.time() + timeout
    url = f"http://127.0.0.1:{port}/json/version"
    while time.time() < deadline:
        try:
            with urlopen(url, timeout=1.0) as resp:
                data = json.loads(resp.read().decode("utf-8", "replace"))
                if data.get("webSocketDebuggerUrl"):
                    return True
        except Exception:
            pass
        time.sleep(0.25)
    return False


def _with_cdp_page_session(port: int, fn):
    """Run fn(session, context) against a page CDP session; return fn's result.

    Never creates a new tab — that was crashing/confusing the browsing session.
    """
    from playwright.sync_api import sync_playwright

    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(f"http://127.0.0.1:{port}")
        try:
            context = browser.contexts[0] if browser.contexts else None
            if context is None:
                raise RuntimeError("No browser context")
            pages = [pg for pg in context.pages if not (pg.url or "").startswith("devtools://")]
            if not pages:
                raise RuntimeError("No open pages")
            page = pages[0]
            session = context.new_cdp_session(page)
            try:
                session.send("Network.enable")
            except Exception:
                pass
            return fn(session, context)
        finally:
            try:
                browser.close()
            except Exception:
                pass


def _persist_live_session(port: Optional[int], profile_data_dir: str, *, reinstall_hooks: bool = False) -> None:
    """Export cookies/passwords/autofill + snapshot DBs in one CDP connection."""
    if not port:
        _snapshot_profile_dbs(profile_data_dir)
        return
    try:
        def _bundle(session, context):
            # Cookies
            try:
                raw = (session.send("Network.getAllCookies") or {}).get("cookies") or []
            except Exception:
                raw = []
            cookies: List[dict] = []
            for c in raw:
                if not isinstance(c, dict) or not c.get("name"):
                    continue
                expires = c.get("expires")
                if expires is None:
                    expires = -1
                same_site = c.get("sameSite") or "Lax"
                if same_site not in ("Strict", "Lax", "None"):
                    same_site = "Lax"
                cookies.append({
                    "name": c["name"],
                    "value": c.get("value") or "",
                    "domain": c.get("domain") or "",
                    "path": c.get("path") or "/",
                    "expires": expires,
                    "httpOnly": bool(c.get("httpOnly")),
                    "secure": bool(c.get("secure")),
                    "sameSite": same_site,
                })

            # Open tab URLs for cookie-first restore on next launch
            tab_urls: List[str] = []
            for page in list(context.pages):
                try:
                    url = (page.url or "").strip()
                except Exception:
                    continue
                if url.startswith("http://") or url.startswith("https://"):
                    if url not in tab_urls:
                        tab_urls.append(url)

            # Forms / credentials from open pages
            creds: List[dict] = []
            fills: List[dict] = []
            for page in list(context.pages):
                try:
                    url = page.url or ""
                    if url.startswith("chrome://") or url.startswith("devtools://") or url.startswith("chrome-error://"):
                        continue
                    data = page.evaluate(_CAPTURE_FORMS_JS)
                    if isinstance(data, dict):
                        for c in data.get("credentials") or []:
                            if isinstance(c, dict) and c.get("password"):
                                creds.append(c)
                        for a in data.get("autofill") or []:
                            if isinstance(a, dict) and a.get("value"):
                                fills.append(a)
                    try:
                        stashed = page.evaluate(
                            "() => { try { return JSON.parse(sessionStorage.getItem('__cpm_form_vault')||'null'); } catch(e) { return null; } }"
                        )
                        if isinstance(stashed, dict):
                            for c in stashed.get("credentials") or []:
                                if isinstance(c, dict) and c.get("password"):
                                    creds.append(c)
                            for a in stashed.get("autofill") or []:
                                if isinstance(a, dict) and a.get("value"):
                                    fills.append(a)
                    except Exception:
                        pass
                except Exception:
                    continue

            if reinstall_hooks:
                credentials = _merge_credentials(
                    _load_json_list(_credentials_path(profile_data_dir), "credentials"),
                    creds,
                )
                autofill = _merge_autofill(
                    _load_json_list(_autofill_path(profile_data_dir), "autofill"),
                    fills,
                )
                script = _autofill_bootstrap_js(credentials, autofill)
                try:
                    context.add_init_script(script)
                except Exception:
                    pass
                for page in list(context.pages):
                    url = ""
                    try:
                        url = page.url or ""
                    except Exception:
                        pass
                    if url.startswith("chrome://") or url.startswith("devtools://") or url.startswith("chrome-error://"):
                        continue
                    try:
                        page.evaluate(script)
                    except Exception:
                        pass

            return {
                "cookies": cookies,
                "credentials": creds,
                "autofill": fills,
                "tabs": tab_urls,
            }

        bundled = _with_cdp_page_session(port, _bundle) or {}
        cookies = bundled.get("cookies") or []
        if cookies:
            _note_wise_critical_cookies(profile_data_dir, cookies)
            _save_session_cookies(profile_data_dir, cookies)
        tabs = bundled.get("tabs") or []
        if tabs:
            _save_session_tabs(profile_data_dir, tabs)
        if bundled.get("credentials"):
            merged = _merge_credentials(
                _load_json_list(_credentials_path(profile_data_dir), "credentials"),
                bundled["credentials"],
            )
            _save_json_list(_credentials_path(profile_data_dir), "credentials", merged)
        if bundled.get("autofill"):
            merged_af = _merge_autofill(
                _load_json_list(_autofill_path(profile_data_dir), "autofill"),
                bundled["autofill"],
            )
            _save_json_list(_autofill_path(profile_data_dir), "autofill", merged_af)
    except Exception:
        pass
    _snapshot_profile_dbs(profile_data_dir)


def _export_cookies_via_cdp(port: int) -> List[dict]:
    """Read decrypted cookies from the live browser (survives rotating OSCrypt keys)."""
    try:
        def _read(session, _context):
            result = session.send("Network.getAllCookies")
            return result.get("cookies") or []

        raw = _with_cdp_page_session(port, _read)
        out: List[dict] = []
        for c in raw:
            if not isinstance(c, dict) or not c.get("name"):
                continue
            expires = c.get("expires")
            if expires is None:
                expires = -1
            same_site = c.get("sameSite") or "Lax"
            if same_site not in ("Strict", "Lax", "None"):
                same_site = "Lax"
            out.append({
                "name": c["name"],
                "value": c.get("value") or "",
                "domain": c.get("domain") or "",
                "path": c.get("path") or "/",
                "expires": expires,
                "httpOnly": bool(c.get("httpOnly")),
                "secure": bool(c.get("secure")),
                "sameSite": same_site,
            })
        return out
    except Exception:
        return []


def _import_cookies_via_cdp(port: int, cookies: List[dict]) -> int:
    """Inject cleartext cookies into the live browser after launch."""
    if not cookies:
        return 0

    def _write(session, _context):
        ok = 0
        for c in cookies:
            if not isinstance(c, dict) or not c.get("name"):
                continue
            domain = (c.get("domain") or "").strip()
            path = c.get("path") or "/"
            params: Dict[str, Any] = {
                "name": c["name"],
                "value": c.get("value") or "",
                "path": path,
                "httpOnly": bool(c.get("httpOnly")),
                "secure": bool(c.get("secure")),
            }
            if domain:
                params["domain"] = domain
            else:
                continue
            expires = c.get("expires")
            if isinstance(expires, (int, float)) and expires > 0:
                params["expires"] = float(expires)
            same_site = c.get("sameSite") or "Lax"
            if same_site in ("Strict", "Lax", "None"):
                params["sameSite"] = same_site
            try:
                result = session.send("Network.setCookie", params)
                if result.get("success", True):
                    ok += 1
                    continue
            except Exception:
                pass
            try:
                host = domain.lstrip(".")
                scheme = "https" if params.get("secure", True) else "http"
                retry = {
                    "name": params["name"],
                    "value": params["value"],
                    "url": f"{scheme}://{host}{path}",
                    "path": path,
                    "httpOnly": params["httpOnly"],
                    "secure": params["secure"],
                }
                if "expires" in params:
                    retry["expires"] = params["expires"]
                if "sameSite" in params:
                    retry["sameSite"] = params["sameSite"]
                result = session.send("Network.setCookie", retry)
                if result.get("success", True):
                    ok += 1
            except Exception:
                pass
        return ok

    try:
        return int(_with_cdp_page_session(port, _write) or 0)
    except Exception:
        return 0


def _reload_open_pages(port: int) -> None:
    """Reload tabs so newly imported cookies apply to already-restored sessions."""
    try:
        def _reload(_session, context):
            for page in list(context.pages):
                try:
                    page.reload(wait_until="domcontentloaded", timeout=15000)
                except Exception:
                    try:
                        page.reload(timeout=10000)
                    except Exception:
                        pass

        _with_cdp_page_session(port, _reload)
    except Exception:
        pass


def _wise_critical_names(cookies: List[dict]) -> set:
    return {
        str(c.get("name"))
        for c in cookies
        if isinstance(c, dict) and c.get("name") in _WISE_CRITICAL_COOKIE_NAMES
    }


def _note_wise_critical_cookies(profile_data_dir: str, cookies: List[dict]) -> None:
    present = _wise_critical_names(cookies)
    if not present:
        return
    seen = _seen_wise_critical.setdefault(profile_data_dir, set())
    seen.update(present)


def _save_session_cookies(profile_data_dir: str, cookies: List[dict]) -> None:
    if not cookies:
        return
    present = _wise_critical_names(cookies)
    previously = _seen_wise_critical.get(profile_data_dir) or set()
    # Also treat cookies already on disk as "seen" so a mid-session drop is caught.
    try:
        previously = previously | _wise_critical_names(_load_session_cookies(profile_data_dir))
    except Exception:
        pass
    missing = previously - present
    if missing:
        msg = (
            f"CRITICAL: Wise auth cookies missing from export "
            f"(were present mid-session): {sorted(missing)}. "
            f"Relaunch may trigger new-device verification."
        )
        print(msg, file=sys.stderr, flush=True)
        print(json.dumps({"success": False, "warning": msg}), flush=True)
    path = _session_cookies_path(profile_data_dir)
    try:
        with open(path, "w", encoding="utf-8") as f:
            json.dump({"version": 1, "cookies": cookies}, f)
    except Exception:
        pass


def _load_session_cookies(profile_data_dir: str) -> List[dict]:
    path = _session_cookies_path(profile_data_dir)
    if not os.path.isfile(path):
        return []
    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        cookies = data.get("cookies") if isinstance(data, dict) else data
        return cookies if isinstance(cookies, list) else []
    except Exception:
        return []


def _save_session_tabs(profile_data_dir: str, urls: List[str]) -> None:
    clean = []
    for u in urls:
        s = str(u or "").strip()
        if (s.startswith("http://") or s.startswith("https://")) and s not in clean:
            clean.append(s)
    if not clean:
        return
    path = _session_tabs_path(profile_data_dir)
    try:
        with open(path, "w", encoding="utf-8") as f:
            json.dump({"version": 1, "tabs": clean}, f)
    except Exception:
        pass


def _load_session_tabs(profile_data_dir: str) -> List[str]:
    path = _session_tabs_path(profile_data_dir)
    if not os.path.isfile(path):
        return []
    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        tabs = data.get("tabs") if isinstance(data, dict) else data
        if not isinstance(tabs, list):
            return []
        return [str(u).strip() for u in tabs if str(u).strip().startswith("http")]
    except Exception:
        return []


def _extract_urls_from_chromium_sessions(profile_data_dir: str) -> List[str]:
    """Best-effort URL scrape from Chromium Session_* binaries (fallback)."""
    import re

    candidates = [
        os.path.join(profile_data_dir, "Default", "Sessions"),
        os.path.join(profile_data_dir, "Default", "Sessions.cpm-parked"),
    ]
    found: List[str] = []
    seen = set()
    for sessions_dir in candidates:
        if not os.path.isdir(sessions_dir):
            continue
        try:
            names = sorted(
                (n for n in os.listdir(sessions_dir) if n.startswith("Session_")),
                reverse=True,
            )
        except Exception:
            continue
        for name in names[:3]:
            path = os.path.join(sessions_dir, name)
            try:
                if not os.path.isfile(path) or os.path.getsize(path) < 100:
                    continue
                data = open(path, "rb").read()
            except Exception:
                continue
            for match in re.findall(rb"https?://[^\x00-\x1f\x7f-\xff]{4,400}", data):
                try:
                    s = match.decode("ascii", "ignore")
                except Exception:
                    continue
                for stop in (" ", '"', "'", "\\", "\t", "<", ">"):
                    if stop in s:
                        s = s.split(stop, 1)[0]
                if not s.startswith("http"):
                    continue
                if any(
                    bad in s
                    for bad in (
                        "favicon",
                        "chrome://",
                        "challenges.cloudflare.com",
                        "autoLogout=true",
                        "/login?",
                    )
                ):
                    continue
                if s in seen:
                    continue
                seen.add(s)
                found.append(s)
            if found:
                return found[:8]
    return found[:8]


def _tabs_to_restore(profile_data_dir: str) -> List[str]:
    tabs = _load_session_tabs(profile_data_dir)
    if tabs:
        return tabs
    return _extract_urls_from_chromium_sessions(profile_data_dir)


def _restore_tabs_via_cdp(port: int, urls: List[str]) -> int:
    """Open saved tabs after cookie import (replaces --restore-last-session)."""
    clean: List[str] = []
    for u in urls:
        s = str(u or "").strip()
        if (s.startswith("http://") or s.startswith("https://")) and s not in clean:
            clean.append(s)
    if not clean:
        return 0

    def _restore(session, context):
        pages = []
        try:
            pages = [
                pg
                for pg in context.pages
                if not (pg.url or "").startswith("devtools://")
            ]
        except Exception:
            pages = []
        opened = 0
        # Navigate the initial blank/NTP tab to the first URL.
        first = clean[0]
        if pages:
            try:
                cur = (pages[0].url or "").strip()
                if cur in ("", "about:blank", "chrome://newtab/", "chrome://new-tab-page/"):
                    pages[0].goto(first, wait_until="domcontentloaded", timeout=20000)
                    opened += 1
                elif first not in cur:
                    session.send("Target.createTarget", {"url": first})
                    opened += 1
                else:
                    opened += 1
            except Exception:
                try:
                    session.send("Target.createTarget", {"url": first})
                    opened += 1
                except Exception:
                    pass
        else:
            try:
                session.send("Target.createTarget", {"url": first})
                opened += 1
            except Exception:
                pass
        for url in clean[1:]:
            try:
                session.send("Target.createTarget", {"url": url})
                opened += 1
            except Exception:
                pass
        return opened

    try:
        return int(_with_cdp_page_session(port, _restore) or 0)
    except Exception:
        return 0


def _has_session_cookies_file(profile_data_dir: str) -> bool:
    """True when the cleartext cookie sidecar exists and has at least one cookie."""
    return bool(_load_session_cookies(profile_data_dir))


def _park_chromium_sessions(profile_data_dir: str) -> Optional[str]:
    """
    Move Default/Sessions aside so Chromium cannot restore tabs before CDP
    cookie import. Returns the parked path (or None).
    """
    sessions = os.path.join(profile_data_dir, "Default", "Sessions")
    if not os.path.isdir(sessions):
        return None
    parked = sessions + ".cpm-parked"
    try:
        if os.path.exists(parked):
            shutil.rmtree(parked, ignore_errors=True)
        os.rename(sessions, parked)
        return parked
    except Exception:
        return None


def _unpark_chromium_sessions(profile_data_dir: str, parked: Optional[str]) -> None:
    """Restore parked Sessions after cookie-first tab restore (best-effort)."""
    if not parked or not os.path.isdir(parked):
        return
    sessions = os.path.join(profile_data_dir, "Default", "Sessions")
    try:
        if not os.path.exists(sessions):
            os.rename(parked, sessions)
        else:
            # Live session already writing; drop the park.
            shutil.rmtree(parked, ignore_errors=True)
    except Exception:
        pass


def _load_json_list(path: str, key: str) -> List[dict]:
    if not os.path.isfile(path):
        return []
    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        if isinstance(data, dict):
            items = data.get(key) or []
        else:
            items = data
        return [x for x in items if isinstance(x, dict)] if isinstance(items, list) else []
    except Exception:
        return []


def _save_json_list(path: str, key: str, items: List[dict]) -> None:
    try:
        with open(path, "w", encoding="utf-8") as f:
            json.dump({"version": 1, key: items}, f, indent=2)
    except Exception:
        pass


def _looks_like_email(value: str) -> bool:
    v = (value or "").strip()
    return ("@" in v) and ("." in v.split("@")[-1]) and (" " not in v) and len(v) >= 5


def _looks_like_hex_id(value: str) -> bool:
    v = (value or "").strip().lower()
    if len(v) < 16 or len(v) > 64:
        return False
    return all(c in "0123456789abcdef" for c in v)


def _is_placeholder_password(password: str) -> bool:
    p = (password or "").strip().lower()
    return p in {"password", "pass", "123456", "••••••••", "********", "placeholder"}


def _credential_quality(item: dict) -> int:
    """Higher is better — used to pick which login to autofill."""
    user = str(item.get("username") or "").strip()
    password = str(item.get("password") or "")
    score = 0
    if _looks_like_email(user):
        score += 100
    elif user and not _looks_like_hex_id(user):
        score += 40
    if password and not _is_placeholder_password(password):
        score += 50
    if len(password) >= 6:
        score += 10
    if _looks_like_hex_id(user) or not user:
        score -= 80
    if _is_placeholder_password(password):
        score -= 100
    return score


def _is_useful_credential(item: dict) -> bool:
    origin = str(item.get("origin") or item.get("url") or "").strip()
    user = str(item.get("username") or "").strip()
    password = str(item.get("password") or "")
    if not origin or not password:
        return False
    if _is_placeholder_password(password):
        return False
    if _looks_like_hex_id(user):
        return False
    # Require a real username/email for login autofill.
    if not user:
        return False
    if len(password) < 4:
        return False
    return True


def _merge_credentials(existing: List[dict], new_items: List[dict]) -> List[dict]:
    by_key: Dict[str, dict] = {}
    for item in existing + new_items:
        if not _is_useful_credential(item):
            continue
        origin = str(item.get("origin") or item.get("url") or "").strip()
        user = str(item.get("username") or "").strip()
        password = str(item.get("password") or "")
        key = f"{origin}::{user.lower()}"
        candidate = {
            "origin": origin,
            "username": user,
            "password": password,
            "updatedAt": int(item.get("updatedAt") or time.time()),
        }
        prev = by_key.get(key)
        if prev is None or _credential_quality(candidate) >= _credential_quality(prev):
            by_key[key] = candidate
    return sorted(by_key.values(), key=_credential_quality, reverse=True)


def _is_useful_autofill(item: dict) -> bool:
    name = str(item.get("name") or item.get("autocomplete") or "").strip().lower()
    value = str(item.get("value") or "").strip()
    input_type = str(item.get("type") or "text").strip().lower()
    ac = str(item.get("autocomplete") or "").strip().lower()
    if not value or len(value) > 200:
        return False
    if input_type in ("password", "hidden", "submit", "button", "checkbox", "radio", "file", "range", "color"):
        return False
    if _looks_like_hex_id(value):
        return False
    if ac in ("off", "new-password", "current-password"):
        # Still allow email/username fields even if autocomplete=off.
        if not any(x in name or x in ac for x in ("email", "user", "login", "tel", "phone", "name")):
            if input_type not in ("email", "tel"):
                return False
    # Skip anonymous junk with no field name.
    if not name and input_type not in ("email", "tel"):
        return False
    # Skip quantity / proxy config noise.
    if any(x in name for x in ("quantity", "rental", "location", "proxyservice", "period", "amount")):
        return False
    return True


def _merge_autofill(existing: List[dict], new_items: List[dict]) -> List[dict]:
    by_key: Dict[str, dict] = {}
    for item in existing + new_items:
        if not _is_useful_autofill(item):
            continue
        name = str(item.get("name") or item.get("autocomplete") or "").strip().lower()
        value = str(item.get("value") or "").strip()
        input_type = str(item.get("type") or "text").strip().lower()
        origin = str(item.get("origin") or "").strip()
        key = f"{origin}::{name}::{input_type}"
        by_key[key] = {
            "origin": origin,
            "name": name,
            "type": input_type,
            "value": value,
            "autocomplete": str(item.get("autocomplete") or ""),
            "updatedAt": int(time.time()),
        }
    items = sorted(by_key.values(), key=lambda x: x.get("updatedAt", 0), reverse=True)
    return items[:200]


_CAPTURE_FORMS_JS = """() => {
  const origin = location.origin || '';
  const host = location.hostname || '';
  if (!host || host.startsWith('chrome') || host.startsWith('devtools')) {
    return {credentials: [], autofill: []};
  }
  const isHexId = (v) => {
    const s = (v || '').trim().toLowerCase();
    return s.length >= 16 && s.length <= 64 && /^[0-9a-f]+$/.test(s);
  };
  const isEmail = (v) => {
    const s = (v || '').trim();
    return s.includes('@') && s.includes('.') && !s.includes(' ') && s.length >= 5;
  };
  const credentials = [];
  const autofill = [];
  const inputs = Array.from(document.querySelectorAll('input, textarea'));
  const passwordInputs = inputs.filter(i => (i.type || '').toLowerCase() === 'password' && i.value);
  for (const pw of passwordInputs) {
    const form = pw.form || pw.closest('form');
    let username = '';
    const candidates = form
      ? Array.from(form.querySelectorAll('input'))
      : inputs;
    // Prefer email-type fields first.
    const ranked = candidates.slice().sort((a, b) => {
      const score = (el) => {
        const t = (el.type || 'text').toLowerCase();
        const n = ((el.name || '') + ' ' + (el.id || '') + ' ' + (el.autocomplete || '') + ' ' + (el.placeholder || '')).toLowerCase();
        let s = 0;
        if (t === 'email') s += 50;
        if (n.includes('email') || n.includes('e-mail') || n.includes('adresse')) s += 40;
        if (n.includes('user') || n.includes('login')) s += 20;
        return s;
      };
      return score(b) - score(a);
    });
    for (const el of ranked) {
      const t = (el.type || 'text').toLowerCase();
      const n = ((el.name || '') + ' ' + (el.id || '') + ' ' + (el.autocomplete || '') + ' ' + (el.placeholder || '')).toLowerCase();
      if (t === 'password' || t === 'hidden') continue;
      if (!(t === 'email' || t === 'text' || t === 'tel' || n.includes('user') || n.includes('email') || n.includes('login') || n.includes('adresse'))) {
        continue;
      }
      if (!el.value) continue;
      if (isHexId(el.value)) continue;
      username = el.value;
      break;
    }
    const password = pw.value || '';
    if (!password || password.toLowerCase() === 'password') continue;
    if (!username) continue;
    credentials.push({origin, username, password});
  }
  for (const el of inputs) {
    const t = (el.type || 'text').toLowerCase();
    if (t === 'password' || t === 'hidden' || t === 'submit' || t === 'button' || t === 'checkbox' || t === 'radio' || t === 'file') continue;
    const value = (el.value || '').trim();
    if (!value || isHexId(value)) continue;
    const name = (el.name || el.id || '').toLowerCase();
    const ac = (el.autocomplete || '').toLowerCase();
    const placeholder = (el.placeholder || '').toLowerCase();
    const useful =
      t === 'email' || t === 'tel' ||
      name.includes('email') || name.includes('user') || name.includes('login') || name.includes('phone') || name.includes('tel') ||
      name.includes('name') || ac.includes('email') || ac.includes('username') || ac.includes('name') ||
      placeholder.includes('email') || placeholder.includes('e-mail');
    if (!useful) continue;
    autofill.push({
      origin,
      name,
      type: t,
      value,
      autocomplete: el.autocomplete || '',
    });
  }
  return {credentials, autofill};
}"""


def _autofill_bootstrap_js(credentials: List[dict], autofill: List[dict]) -> str:
    # Only ship high-quality credentials to the page script.
    clean_creds = sorted(
        [c for c in credentials if _is_useful_credential(c)],
        key=_credential_quality,
        reverse=True,
    )
    clean_fill = [a for a in autofill if _is_useful_autofill(a)]
    payload = json.dumps({"credentials": clean_creds, "autofill": clean_fill})
    return f"""
(() => {{
  const DATA = {payload};
  const host = () => (location.hostname || '').toLowerCase();
  const origin = () => location.origin || '';
  const matchHost = (entryOrigin) => {{
    try {{
      const h = host();
      if (!h) return false;
      const u = new URL(entryOrigin);
      const eh = (u.hostname || '').toLowerCase();
      return h === eh || h.endsWith('.' + eh) || eh.endsWith('.' + h);
    }} catch (e) {{
      return (entryOrigin || '').includes(host());
    }}
  }};
  const isEmail = (v) => {{
    const s = (v || '').trim();
    return s.includes('@') && s.includes('.') && !s.includes(' ') && s.length >= 5;
  }};
  const pickCred = (creds) => {{
    const ranked = creds.slice().sort((a, b) => {{
      const score = (c) => {{
        let s = 0;
        if (isEmail(c.username || '')) s += 100;
        if ((c.username || '').length > 2) s += 20;
        if ((c.password || '').length >= 6) s += 20;
        if (!(c.username || '').trim()) s -= 100;
        return s;
      }};
      return score(b) - score(a);
    }});
    return ranked[0] || null;
  }};
  const setNativeValue = (el, value) => {{
    const proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
    const desc = Object.getOwnPropertyDescriptor(proto, 'value');
    if (desc && desc.set) desc.set.call(el, value);
    else el.value = value;
    el.dispatchEvent(new Event('input', {{bubbles: true}}));
    el.dispatchEvent(new Event('change', {{bubbles: true}}));
  }};
  const fill = () => {{
    try {{
      const inputs = Array.from(document.querySelectorAll('input, textarea'));
      if (!inputs.length) return;
      const creds = (DATA.credentials || []).filter(c => matchHost(c.origin));
      const cred = pickCred(creds);
      if (cred) {{
        const pw = inputs.find(i => (i.type || '').toLowerCase() === 'password');
        if (pw && cred.password) setNativeValue(pw, cred.password);
        if (cred.username) {{
          const user = inputs.find(i => {{
            const t = (i.type || 'text').toLowerCase();
            const n = ((i.name || '') + ' ' + (i.id || '') + ' ' + (i.autocomplete || '') + ' ' + (i.placeholder || '')).toLowerCase();
            if (t === 'password' || t === 'hidden') return false;
            return t === 'email' || n.includes('email') || n.includes('e-mail') || n.includes('user') || n.includes('login') || n.includes('adresse');
          }});
          if (user) setNativeValue(user, cred.username);
        }}
      }}
      // Only autofill email/tel/name-like fields — never random shop form IDs.
      for (const item of (DATA.autofill || [])) {{
        if (!matchHost(item.origin) && item.origin !== origin()) continue;
        if (!isEmail(item.value) && item.type !== 'email' && item.type !== 'tel') {{
          const n = (item.name || '') + ' ' + (item.autocomplete || '');
          if (!(n.includes('name') || n.includes('phone') || n.includes('tel'))) continue;
        }}
        const el = inputs.find(i => {{
          const t = (i.type || 'text').toLowerCase();
          if (t === 'password') return false;
          if (i.value) return false;
          const n = ((i.name || '') + ' ' + (i.id || '') + ' ' + (i.autocomplete || '') + ' ' + (i.placeholder || '')).toLowerCase();
          if (item.type === 'email' || isEmail(item.value)) {{
            return t === 'email' || n.includes('email') || n.includes('e-mail') || n.includes('adresse');
          }}
          if (item.type === 'tel' || (item.name || '').includes('phone')) {{
            return t === 'tel' || n.includes('phone') || n.includes('tel');
          }}
          return false;
        }});
        if (el && item.value) setNativeValue(el, item.value);
      }}
    }} catch (e) {{}}
  }};
  fill();
  document.addEventListener('DOMContentLoaded', fill, true);
  setTimeout(fill, 400);
  setTimeout(fill, 1200);
  setTimeout(fill, 2500);
  const stash = () => {{
    try {{
      const payload = (typeof window.__cpmCaptureForms === 'function')
        ? window.__cpmCaptureForms()
        : null;
      if (!payload) return;
      const prev = JSON.parse(sessionStorage.getItem('__cpm_form_vault') || '{{"credentials":[],"autofill":[]}}');
      const mergeCred = (arr, item) => {{
        if (!item || !item.password || !item.username) return arr;
        if ((item.password || '').toLowerCase() === 'password') return arr;
        const key = (item.origin || '') + '::' + (item.username || '');
        const out = arr.filter(x => ((x.origin||'') + '::' + (x.username||'')) !== key);
        out.push(item);
        return out;
      }};
      let credentials = prev.credentials || [];
      let autofill = prev.autofill || [];
      for (const c of (payload.credentials || [])) credentials = mergeCred(credentials, c);
      for (const a of (payload.autofill || [])) {{
        if (!a || !a.value) continue;
        autofill = autofill.filter(x => !(x.origin===a.origin && x.name===a.name && x.value===a.value));
        autofill.push(a);
      }}
      sessionStorage.setItem('__cpm_form_vault', JSON.stringify({{credentials, autofill}}));
    }} catch (e) {{}}
  }};
  window.__cpmCaptureForms = {_CAPTURE_FORMS_JS.strip()};
  document.addEventListener('change', stash, true);
  document.addEventListener('submit', stash, true);
  window.addEventListener('beforeunload', stash);
}})();
"""


def _capture_forms_via_cdp(port: int) -> Dict[str, List[dict]]:
    """Read filled password/username/autofill fields from open pages."""
    empty: Dict[str, List[dict]] = {"credentials": [], "autofill": []}
    try:
        def _capture(_session, context):
            creds: List[dict] = []
            fills: List[dict] = []
            for page in list(context.pages):
                try:
                    url = page.url or ""
                    if url.startswith("chrome://") or url.startswith("devtools://"):
                        continue
                    data = page.evaluate(_CAPTURE_FORMS_JS)
                    if not isinstance(data, dict):
                        continue
                    for c in data.get("credentials") or []:
                        if isinstance(c, dict) and c.get("password"):
                            creds.append(c)
                    for a in data.get("autofill") or []:
                        if isinstance(a, dict) and a.get("value"):
                            fills.append(a)
                    # Also pull stashed vault from sessionStorage
                    try:
                        stashed = page.evaluate(
                            "() => { try { return JSON.parse(sessionStorage.getItem('__cpm_form_vault')||'null'); } catch(e) { return null; } }"
                        )
                        if isinstance(stashed, dict):
                            for c in stashed.get("credentials") or []:
                                if isinstance(c, dict) and c.get("password"):
                                    creds.append(c)
                            for a in stashed.get("autofill") or []:
                                if isinstance(a, dict) and a.get("value"):
                                    fills.append(a)
                    except Exception:
                        pass
                except Exception:
                    continue
            return {"credentials": creds, "autofill": fills}

        result = _with_cdp_page_session(port, _capture)
        return result if isinstance(result, dict) else empty
    except Exception:
        return empty


def _install_autofill_hooks(port: int, profile_data_dir: str) -> None:
    """Inject autofill/password restore script into all current and future documents."""
    credentials = _load_json_list(_credentials_path(profile_data_dir), "credentials")
    autofill = _load_json_list(_autofill_path(profile_data_dir), "autofill")
    script = _autofill_bootstrap_js(credentials, autofill)
    try:
        def _install(_session, context):
            try:
                context.add_init_script(script)
            except Exception:
                pass
            for page in list(context.pages):
                url = ""
                try:
                    url = page.url or ""
                except Exception:
                    pass
                if url.startswith("chrome://") or url.startswith("devtools://") or url.startswith("chrome-error://"):
                    continue
                try:
                    page.add_init_script(script)
                except Exception:
                    pass
                try:
                    page.evaluate(script)
                except Exception:
                    pass

        _with_cdp_page_session(port, _install)
    except Exception:
        pass


def _db_snapshot_dir(profile_data_dir: str) -> str:
    return os.path.join(profile_data_dir, ".cpm-db-snapshot")


def _sqlite_table_count(db_path: str, table: str) -> int:
    if not os.path.isfile(db_path):
        return 0
    try:
        import sqlite3

        conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1.0)
        try:
            return int(conn.execute(f"SELECT count(*) FROM {table}").fetchone()[0])
        finally:
            conn.close()
    except Exception:
        return 0


def _snapshot_profile_dbs(profile_data_dir: str) -> None:
    """
    Copy Chromium SQLite DBs (plus WAL) while the browser is still alive.

    clark-browser's Chromium build clears History/Cookies on process exit.
    Snapshots taken during the session are restored after quit so profiles
    behave like normal Chrome (only the fingerprint differs).
    """
    default_dir = os.path.join(profile_data_dir, "Default")
    if not os.path.isdir(default_dir):
        return
    dest = _db_snapshot_dir(profile_data_dir)
    try:
        os.makedirs(dest, exist_ok=True)
        for base in _PERSIST_DB_NAMES:
            for suffix in ("", "-wal", "-shm", "-journal"):
                src = os.path.join(default_dir, base + suffix)
                if os.path.isfile(src) and os.path.getsize(src) > 0:
                    shutil.copy2(src, os.path.join(dest, base + suffix))
    except Exception:
        pass


def _restore_profile_dbs_if_wiped(profile_data_dir: str) -> None:
    """If clark wiped History/Cookies on exit, put the last good snapshot back."""
    snap = _db_snapshot_dir(profile_data_dir)
    default_dir = os.path.join(profile_data_dir, "Default")
    if not os.path.isdir(snap) or not os.path.isdir(default_dir):
        return

    history_live = os.path.join(default_dir, "History")
    history_snap = os.path.join(snap, "History")
    cookies_live = os.path.join(default_dir, "Cookies")
    cookies_snap = os.path.join(snap, "Cookies")

    need_history = (
        os.path.isfile(history_snap)
        and _sqlite_table_count(history_snap, "urls") > 0
        and _sqlite_table_count(history_live, "urls") == 0
    )
    need_cookies = (
        os.path.isfile(cookies_snap)
        and _sqlite_table_count(cookies_snap, "cookies") > 0
        and _sqlite_table_count(cookies_live, "cookies") == 0
    )
    need_logins = (
        os.path.isfile(os.path.join(snap, "Login Data"))
        and _sqlite_table_count(os.path.join(snap, "Login Data"), "logins") > 0
        and _sqlite_table_count(os.path.join(default_dir, "Login Data"), "logins") == 0
    )
    if not (need_history or need_cookies or need_logins):
        return

    try:
        for base in _PERSIST_DB_NAMES:
            for suffix in ("", "-wal", "-shm", "-journal"):
                src = os.path.join(snap, base + suffix)
                dst = os.path.join(default_dir, base + suffix)
                if os.path.isfile(src):
                    shutil.copy2(src, dst)
                elif os.path.isfile(dst) and suffix in ("-wal", "-shm", "-journal"):
                    try:
                        os.remove(dst)
                    except Exception:
                        pass
    except Exception:
        pass


def _flush_after_browser_exit(profile_data_dir: str) -> None:
    """Wait briefly, then restore DBs if clark wiped them on exit."""
    time.sleep(2.0)
    _restore_profile_dbs_if_wiped(profile_data_dir)
    _mark_clean_exit(profile_data_dir)


def _macos_window_counter_bin() -> Optional[str]:
    """Compile (once) and return a CoreGraphics window counter for a pid."""
    if sys.platform != "darwin":
        return None
    launcher_dir = os.path.dirname(os.path.abspath(__file__))
    source = os.path.join(launcher_dir, "macos_window_count.swift")
    binary = os.path.join(launcher_dir, ".macos_window_count_bin")
    if not os.path.isfile(source):
        return None
    try:
        if not os.path.isfile(binary) or os.path.getmtime(binary) < os.path.getmtime(source):
            subprocess.run(
                ["swiftc", "-O", "-o", binary, source],
                capture_output=True,
                timeout=30,
                check=True,
            )
        return binary
    except Exception:
        return None


def _macos_chromium_window_count(pid: int) -> int:
    """Return visible window count for a Chromium pid, or -1 when unknown."""
    if sys.platform != "darwin":
        return -1

    counter = _macos_window_counter_bin()
    if counter:
        try:
            result = subprocess.run(
                [counter, str(pid)],
                capture_output=True,
                text=True,
                timeout=2,
            )
            if result.returncode == 0 and result.stdout.strip().isdigit():
                return int(result.stdout.strip())
        except Exception:
            pass

    try:
        result = subprocess.run(
            [
                "osascript",
                "-e",
                f'tell application "System Events" to count windows of '
                f'(first process whose unix id is {pid})',
            ],
            capture_output=True,
            text=True,
            timeout=3,
        )
        if result.returncode != 0:
            return -1
        return int(result.stdout.strip())
    except Exception:
        return -1


def _graceful_quit_chromium(
    proc: subprocess.Popen,
    timeout: float = 15.0,
    profile_data_dir: Optional[str] = None,
    debug_port: Optional[int] = None,
) -> None:
    """Quit Chromium after exporting cleartext cookies (OSCrypt key does not persist)."""
    if profile_data_dir:
        _persist_live_session(debug_port, profile_data_dir)
    if proc.poll() is not None:
        return
    try:
        proc.send_signal(signal.SIGTERM)
        proc.wait(timeout=timeout)
        return
    except subprocess.TimeoutExpired:
        pass
    except Exception:
        pass
    try:
        proc.kill()
        proc.wait(timeout=5)
    except Exception:
        pass


def _wait_for_chromium_process(
    proc: subprocess.Popen,
    profile_data_dir: str,
    debug_port: Optional[int] = None,
) -> int:
    """
    Block until Chromium exits.

    On macOS, closing the red X can leave Chromium running with no windows.
    Only auto-quit after we have *actually seen* at least one window, then
    observed zero windows for several consecutive checks. Never assume windows
    existed just because time passed — that was killing browsers during startup
    (looked like a crash on Launch).
    """
    launched_at = time.time()
    last_snapshot = 0.0
    had_windows = False
    zero_window_checks = 0
    while proc.poll() is None:
        _touch_marker(profile_data_dir)
        now = time.time()
        # Persist cookies/history ~every 5s from first CDP connect (not after 8s/20s).
        if debug_port and now - last_snapshot >= 5.0:
            try:
                _persist_live_session(debug_port, profile_data_dir, reinstall_hooks=False)
            except Exception:
                pass
            last_snapshot = now
        if sys.platform == "darwin":
            # Grace period: never auto-quit in the first 15s while Chromium starts.
            if now - launched_at < 15.0:
                time.sleep(0.5)
                continue

            window_count = _macos_chromium_window_count(proc.pid)
            if window_count > 0:
                had_windows = True
                zero_window_checks = 0
            elif had_windows and window_count == 0:
                # Require ~3s of no windows (6 * 0.5s) before quitting.
                zero_window_checks += 1
                if zero_window_checks >= 6:
                    _graceful_quit_chromium(
                        proc,
                        profile_data_dir=profile_data_dir,
                        debug_port=debug_port,
                    )
                    break
            elif window_count == -1:
                # Unknown (permissions) — do not treat as closed.
                zero_window_checks = 0
            else:
                # Still starting / never saw a window — keep waiting.
                zero_window_checks = 0
        time.sleep(0.5)
    if proc.poll() is None:
        try:
            _persist_live_session(debug_port, profile_data_dir)
        except Exception:
            pass
        try:
            proc.wait(timeout=20)
        except subprocess.TimeoutExpired:
            proc.kill()
            proc.wait(timeout=5)
    return proc.returncode if proc.returncode is not None else 0


def _fingerprint_seed_int(config: dict) -> int:
    """
    Map the profile's stored seed to clark's 10000..99999 fingerprint range.

    A stable per-profile int yields a stable, unique C++ fingerprint (canvas,
    WebGL, audio, hardware) so multiple accounts never share an identity.
    """
    raw = str(config.get("fingerprintSeed") or config.get("profileId") or "").strip()
    if not raw:
        raw = config.get("profileDataDir", "clark")
    digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
    return 10000 + (int(digest, 16) % 90000)


def _settle_pages(context, timeout: float = 4.0) -> List:
    deadline = time.time() + timeout
    last = -1
    stable_since = time.time()
    pages: List = []
    while time.time() < deadline:
        try:
            pages = list(context.pages)
        except Exception:
            pages = []
        if pages and len(pages) == last:
            if time.time() - stable_since >= 0.4:
                return pages
        else:
            last = len(pages)
            stable_since = time.time()
        time.sleep(0.15)
    return pages


def main():
    if len(sys.argv) < 2:
        print(json.dumps({"success": False, "error": "Missing config JSON"}))
        sys.exit(1)

    try:
        config = json.loads(sys.argv[1])
    except json.JSONDecodeError as e:
        print(json.dumps({"success": False, "error": f"Invalid JSON: {e}"}))
        sys.exit(1)

    try:
        import clarkbrowser  # noqa: F401
    except ImportError:
        print(json.dumps({
            "success": False,
            "error": "clark-browser not installed. Run: pip install -U clark-browser && python -m clarkbrowser fetch",
        }))
        sys.exit(1)

    _prepare_clark_stealth()

    profile_data_dir = config.get("profileDataDir", "")
    if not profile_data_dir or not os.path.isabs(profile_data_dir):
        print(json.dumps({"success": False, "error": "Invalid profileDataDir"}))
        sys.exit(1)
    os.makedirs(profile_data_dir, exist_ok=True)

    # Proxy (Playwright ProxySettings dict) --------------------------------
    proxy_config = config.get("proxy")
    proxy_dict = None
    if proxy_config:
        ptype = proxy_config.get("type", "http")
        host = proxy_config.get("host", "")
        port = proxy_config.get("port", 8080)
        user = proxy_config.get("username", "")
        pwd = proxy_config.get("password", "")
        if host and port:
            proxy_dict = {"server": f"{ptype}://{host}:{port}"}
            if user or pwd:
                proxy_dict["username"] = str(user or "")
                proxy_dict["password"] = str(pwd or "")

    use_local_ip = bool(config.get("useLocalIp"))
    if not use_local_ip and not proxy_dict:
        use_local_ip = True
    if use_local_ip:
        proxy_dict = None

    # Locale / timezone -----------------------------------------------------
    locale = config.get("locale", "en-US")
    languages = config.get("languages", [locale])
    if isinstance(languages, str):
        languages = [s.strip() for s in languages.split(",") if s.strip()]
    accept_lang = ",".join(languages) if languages else locale
    timezone = config.get("timezone") or None

    # Fingerprint flags -----------------------------------------------------
    os_family = config.get("osFamily", "macos")
    platform_map = {"macos": "macos", "windows": "windows", "linux": "linux"}
    fp_platform = platform_map.get(os_family, "macos")

    # Use the real fitted window size for BOTH the window and the fingerprint.
    # If profile says 1366×768 but the MacBook window is ~1496×868, Wise/CSS
    # layout breaks (huge sidebar, empty gaps). Screen size in the fingerprint
    # must match what the user actually sees.
    _, _, win_w, win_h = _usable_screen_rect()
    screen_w, screen_h = win_w, win_h
    seed = _fingerprint_seed_int(config)

    restore_session = _has_saved_session(profile_data_dir)
    # When cleartext session cookies exist, do NOT let Chromium restore Wise/tabs
    # before CDP import — that race is what triggers Wise "new device" SMS.
    cookie_first_restore = _has_session_cookies_file(profile_data_dir)

    chrome_args = [
        f"--fingerprint={seed}",
        f"--fingerprint-platform={fp_platform}",
        f"--fingerprint-screen-width={screen_w}",
        f"--fingerprint-screen-height={screen_h}",
        f"--accept-lang={accept_lang}",
        f"--window-size={win_w},{win_h}",
        # Unsigned clark Chromium cannot use the real keychain; without these
        # password save fails with "Encryption is not available".
        # Cookie/password ciphertext still won't decrypt next launch (mock key
        # is ephemeral) — we persist cleartext cookies via CDP instead.
        "--password-store=basic",
        "--use-mock-keychain",
    ]
    if restore_session and not cookie_first_restore:
        chrome_args.append("--restore-last-session")

    debug_port = _pick_free_port()
    chrome_args.append(f"--remote-debugging-port={debug_port}")
    chrome_args.append("--remote-allow-origins=*")

    # WebRTC: keep it coherent with the proxy route so the real IP never leaks.
    webrtc_mode = config.get("webrtcMode", "proxy")
    webrtc_policy = None
    if proxy_dict and webrtc_mode != "real":
        webrtc_policy = "proxy-coherent"

    # Enable password-save + autofill and clamp window bounds before launch.
    # With cookie-first restore, start on NTP so no site loads before cookies.
    _seed_chromium_prefs(profile_data_dir, defer_session_restore=cookie_first_restore)

    # Playwright is only used for a one-shot cookie import on empty profiles.
    import_launch_kwargs: Dict = {
        "user_data_dir": profile_data_dir,
        "headless": False,
        "proxy": proxy_dict,
        "args": chrome_args,
        "locale": accept_lang or locale,
        "timezone": timezone,
        "viewport": None,
        "webrtc_policy": webrtc_policy,
        "webgpu_policy": "coherent",
    }
    if sys.platform == "darwin":
        import_launch_kwargs["chromium_sandbox"] = True
        try:
            import clarkbrowser.config as _cfg

            _cfg.IGNORE_DEFAULT_ARGS = [
                "--enable-automation",
                "--enable-unsafe-swiftshader",
                "--use-mock-keychain",
            ]
        except Exception:
            pass
    if not use_local_ip and config.get("geolocationMode") == "custom":
        lat = config.get("geoLatitude")
        lon = config.get("geoLongitude")
        acc = config.get("geoAccuracy", 100)
        if lat is not None and lon is not None:
            import_launch_kwargs["geolocation"] = {
                "latitude": float(lat),
                "longitude": float(lon),
                "accuracy": float(acc or 100),
            }
            import_launch_kwargs["permissions"] = ["geolocation"]
    import_launch_kwargs = {k: v for k, v in import_launch_kwargs.items() if v is not None}

    cookies_raw = (config.get("cookies") or "").strip()
    _import_cookies_if_needed(profile_data_dir, cookies_raw, import_launch_kwargs)

    # Park Chromium Sessions so it cannot race-navigate Wise before CDP import.
    parked_sessions: Optional[str] = None
    if cookie_first_restore:
        parked_sessions = _park_chromium_sessions(profile_data_dir)

    cmd = _build_chromium_command(
        profile_data_dir,
        chrome_args,
        proxy_dict,
        timezone,
        accept_lang or locale,
        webrtc_policy,
    )

    browser_proc: Optional[subprocess.Popen] = None
    exit_code = 0

    def _handle_sigterm(_signum, _frame) -> None:
        _remove_marker(profile_data_dir)
        proc = browser_proc
        if proc is not None and proc.poll() is None:
            _graceful_quit_chromium(
                proc,
                profile_data_dir=profile_data_dir,
                debug_port=debug_port,
            )
        _unpark_chromium_sessions(profile_data_dir, parked_sessions)
        _flush_after_browser_exit(profile_data_dir)
        os._exit(0)

    try:
        signal.signal(signal.SIGTERM, _handle_sigterm)
        signal.signal(signal.SIGINT, _handle_sigterm)
    except Exception:
        pass

    try:
        browser_proc = subprocess.Popen(cmd, start_new_session=True)
        _touch_marker(profile_data_dir)

        # Re-inject cleartext cookies from the previous session BEFORE any
        # restored Wise/tab navigation. Encrypted Cookies SQLite values cannot
        # be decrypted after relaunch because the mock keychain OSCrypt key is
        # ephemeral. When cookie_first_restore is set we started blank (no
        # --restore-last-session); import cookies, then open tabs via CDP.
        if _wait_for_cdp(debug_port, timeout=20.0):
            saved = _load_session_cookies(profile_data_dir)
            if not saved and cookies_raw:
                # Fall back to profile-editor cookies on first migration.
                try:
                    saved = _parse_cookies(cookies_raw)
                except Exception:
                    saved = []
            if saved:
                _note_wise_critical_cookies(profile_data_dir, saved)
                imported = _import_cookies_via_cdp(debug_port, saved)
                print(
                    json.dumps({
                        "event": "cdp_cookie_import",
                        "imported": imported,
                        "cookie_first_restore": cookie_first_restore,
                    }),
                    flush=True,
                )
                if cookie_first_restore:
                    tabs = _tabs_to_restore(profile_data_dir)
                    if tabs:
                        opened = _restore_tabs_via_cdp(debug_port, tabs)
                        print(
                            json.dumps({
                                "event": "cdp_tab_restore",
                                "opened": opened,
                                "tabs": tabs[:8],
                            }),
                            flush=True,
                        )
                else:
                    # Chromium already restored tabs; reload so cookies apply.
                    time.sleep(0.5)
                    _reload_open_pages(debug_port)
            # Restore saved passwords + typed form data (cleartext vault).
            _install_autofill_hooks(debug_port, profile_data_dir)
            # Capture an early snapshot once the browser is up.
            time.sleep(0.5)
            _persist_live_session(debug_port, profile_data_dir, reinstall_hooks=False)

        exit_code = _wait_for_chromium_process(
            browser_proc,
            profile_data_dir,
            debug_port=debug_port,
        )
        if exit_code not in (0, -15, 143):
            print(json.dumps({
                "success": False,
                "error": f"Browser exited unexpectedly (code {exit_code})",
            }))
    except Exception as e:
        print(json.dumps({"success": False, "error": str(e)}))
        exit_code = 1
    finally:
        _remove_marker(profile_data_dir)
        _unpark_chromium_sessions(profile_data_dir, parked_sessions)
        _flush_after_browser_exit(profile_data_dir)
        _force_exit_after(2.0, exit_code)


if __name__ == "__main__":
    main()
