"""Cookie management — safe Reddit cookie extraction + persistence.

Strategy:
- Never attach to a live user browser process or reuse the user's live profile.
- Silent refresh copies a local browser profile to a temporary workspace.
- Windows interactive login launches a dedicated real Chrome/Edge profile and reads cookies via CDP.
"""

from __future__ import annotations

import json
import os
import shutil
import socket
import sqlite3
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from urllib.request import urlopen

import anyio

from .console import console_print
from .config import (
    BROWSER_PROFILE,
    COOKIE_AGE_WARN_DAYS,
    COOKIES_PATH,
    ensure_config_dir,
    load_meta,
    save_meta,
)

BROWSER_START_TIMEOUT_SECONDS = 20.0
LOGIN_WAIT_TIMEOUT_SECONDS = 180.0
SOURCE_PROFILE_POLL_INTERVAL_MS = 5000
GUIDED_LOGIN_POLL_INTERVAL_SECONDS = 2.0
BROWSER_SHUTDOWN_WAIT_SECONDS = 5
CLEANUP_RETRY_COUNT = 5
CLEANUP_RETRY_DELAY_SECONDS = 0.5
BROWSER_PROCESS_NAMES = ("chrome", "msedge")
COOKIE_DB_CANDIDATE_PATHS = (
    Path("Cookies"),
    Path("Network") / "Cookies",
)
OPTIONAL_PROFILE_DIRS = {
    "Cache",
    "Code Cache",
    "GPUCache",
    "ShaderCache",
    "GrShaderCache",
    "GraphiteDawnCache",
    "Crashpad",
}
PROFILE_SUPPORT_FILES = {
    Path("Preferences"),
    Path("Secure Preferences"),
}
CRITICAL_PROFILE_FILES = {
    "Cookies",
    "Cookies-journal",
    "Cookies-shm",
    "Cookies-wal",
}
PLAYWRIGHT_REINSTALL_HINT = "uv tool install --upgrade --reinstall 5mghost-rover"


@dataclass(slots=True)
class BrowserProfileCandidate:
    browser_label: str
    executable_path: Path
    user_data_dir: Path
    profile_name: str
    profile_dir: Path
    cookies_db: Path
    reddit_cookie_rows: int
    cookies_mtime: float
    cookie_probe_error: str | None = None


@dataclass(slots=True)
class CookieProbeResult:
    reddit_cookie_rows: int
    error: str | None = None


@dataclass(slots=True)
class CookieValidationResult:
    valid: bool
    status_code: int | None
    final_url: str | None
    content_type: str | None
    response_snippet: str | None
    cookie_names: list[str]
    error: str | None = None


async def setup_cookies(*, silent: bool = False, shutdown_browser: bool = False) -> dict[str, str]:
    """Get Reddit cookies via copied local browser profile.

    Args:
        silent: If True, suppress print output (used for auto-refresh).

    Returns the cookies dict on success.
    """
    ensure_config_dir()
    if shutdown_browser:
        _shutdown_browser_processes_if_requested(silent=silent)
    if sys.platform == "win32" and not silent:
        cookies = await _collect_reddit_cookies_via_guided_browser_login(silent=silent)
    else:
        cookies = await _collect_reddit_cookies_via_playwright(silent=silent)

    # Validate BEFORE persisting — don't save bad cookies.
    # Why: failed setup needs actionable diagnostics, not a generic false.
    validation = await _validate_cookies(cookies)
    if not validation.valid:
        raise RuntimeError(_format_cookie_validation_failure(validation))

    # Persist cookies only after validation passes
    _save_cookies(cookies)
    save_meta({"cookies_created_at": time.time()})

    if not silent:
        console_print("✅ Reddit cookies saved and validated successfully!")
    return cookies


async def auto_refresh_cookies() -> dict[str, str] | None:
    """Automatically refresh cookies if expired or missing.

    Called by server.py at startup and by reddit_client on 403.
    Uses copied local browser profile + headless CDP read.
    Returns new cookies on success, None on failure.
    """
    try:
        return await setup_cookies(silent=True)
    except Exception:
        return None


def are_cookies_expired() -> bool:
    """Check if reddit_session cookie has expired (via JWT exp claim)."""
    remaining = get_session_expiry_days()
    if remaining is None:
        # Can't determine — assume valid unless proven otherwise
        return False
    return remaining <= 0


def load_cookies() -> dict[str, str] | None:
    """Load cookies from disk, or return None if not found."""
    if not COOKIES_PATH.exists():
        return None
    # Auto-fix permissions if too open (group/other readable)
    if sys.platform != "win32":
        try:
            mode = COOKIES_PATH.stat().st_mode & 0o777
            if mode & 0o077:
                console_print(
                    f"⚠️ cookies.json permissions too open ({oct(mode)}), fixing to 0o600",
                )
                COOKIES_PATH.chmod(0o600)
        except OSError:
            pass
    try:
        data = json.loads(COOKIES_PATH.read_text("utf-8"))
        if isinstance(data, dict) and data:
            return data
        return None
    except (json.JSONDecodeError, OSError):
        return None


async def check_cookies_valid(cookies: dict[str, str]) -> bool:
    """Verify cookies by hitting /api/me.json — True if authenticated.

    Uses curl_cffi (not httpx) to match Chrome TLS fingerprint.
    """
    return (await _validate_cookies(cookies)).valid


async def _validate_cookies(cookies: dict[str, str]) -> CookieValidationResult:
    """Return structured diagnostics for Reddit cookie validation."""
    cookie_names = sorted(name for name in cookies if name)
    try:
        from curl_cffi.requests import AsyncSession

        async with AsyncSession(impersonate="chrome") as session:
            resp = await session.get(
                "https://www.reddit.com/api/me.json",
                cookies=cookies,
                timeout=15,
            )
            content_type = resp.headers.get("content-type")
            payload = resp.text
            response_snippet = _truncate_response_preview(payload)

            if resp.status_code != 200:
                return CookieValidationResult(
                    valid=False,
                    status_code=resp.status_code,
                    final_url=str(resp.url),
                    content_type=content_type,
                    response_snippet=response_snippet,
                    cookie_names=cookie_names,
                    error=None,
                )

            data = json.loads(payload)
            valid = data.get("kind") == "t2" or bool(data.get("data", {}).get("name"))
            return CookieValidationResult(
                valid=valid,
                status_code=resp.status_code,
                final_url=str(resp.url),
                content_type=content_type,
                response_snippet=response_snippet if not valid else None,
                cookie_names=cookie_names,
                error=None,
            )
    except Exception as exc:
        return CookieValidationResult(
            valid=False,
            status_code=None,
            final_url="https://www.reddit.com/api/me.json",
            content_type=None,
            response_snippet=None,
            cookie_names=cookie_names,
            error=str(exc),
        )


def _truncate_response_preview(payload: str, *, limit: int = 240) -> str | None:
    compact = " ".join(payload.split())
    if not compact:
        return None
    if len(compact) <= limit:
        return compact
    return f"{compact[:limit]}..."


def _format_cookie_validation_failure(result: CookieValidationResult) -> str:
    parts = ["Cookies were obtained but validation failed."]

    if result.status_code is not None:
        status = f"Reddit /api/me.json returned status {result.status_code}"
        if result.content_type:
            status += f" ({result.content_type})"
        parts.append(status + ".")

    if result.final_url:
        parts.append(f"Final URL: {result.final_url}.")

    if result.cookie_names:
        parts.append(f"Cookie names seen: {', '.join(result.cookie_names)}.")

    if result.response_snippet:
        parts.append(f"Response preview: {result.response_snippet}.")

    if result.error:
        parts.append(f"Validation error: {result.error}.")

    parts.append("Please check your Reddit account and try again.")
    return " ".join(parts)


def get_cookie_age_days() -> float | None:
    """Return cookie age in days, or None if unknown."""
    meta = load_meta()
    created = meta.get("cookies_created_at")
    if created is None:
        return None
    return (time.time() - created) / 86400


def get_session_expiry_days() -> float | None:
    """Return days until reddit_session JWT expires, or None if can't determine."""
    cookies = load_cookies()
    if not cookies:
        return None
    token = cookies.get("reddit_session", "")
    if not token:
        return None
    try:
        import base64

        parts = token.split(".")
        if len(parts) < 2:
            return None
        # JWT payload is base64url-encoded, add padding
        payload_b64 = parts[1] + "=" * (4 - len(parts[1]) % 4)
        payload = json.loads(base64.urlsafe_b64decode(payload_b64))
        exp = payload.get("exp")
        if exp is None:
            return None
        return (exp - time.time()) / 86400
    except Exception:
        return None


def cookies_age_warning() -> str | None:
    """Return a warning string if cookies are expiring soon, else None."""
    remaining = get_session_expiry_days()
    if remaining is not None:
        if remaining <= 0:
            return (
                "⚠️ Reddit session cookie has expired. "
                "Run `reddit-mcp setup-cookies` to re-login."
            )
        if remaining < 30:
            return (
                f"⚠️ Reddit session cookie expires in {remaining:.0f} days. "
                "Consider running `reddit-mcp setup-cookies` to refresh."
            )
        return None

    # Fallback to file age
    age = get_cookie_age_days()
    if age is not None and age > COOKIE_AGE_WARN_DAYS:
        return (
            f"⚠️ Reddit cookies are {age:.0f} days old. "
            "Consider running `reddit-mcp setup-cookies` to refresh them."
        )
    return None


# -- internal -------------------------------------------------------------------


def _save_cookies(cookies: dict[str, str]) -> None:
    ensure_config_dir()
    COOKIES_PATH.write_text(json.dumps(cookies, indent=2), "utf-8")
    try:
        COOKIES_PATH.chmod(0o600)
    except OSError:
        pass


def _is_reddit_domain(domain: str) -> bool:
    normalized = domain.strip().lower()
    return normalized == "reddit.com" or normalized.endswith(".reddit.com")


def _filter_reddit_cookies(all_cookies: list[dict[str, Any]]) -> dict[str, str]:
    filtered: dict[str, str] = {}
    for cookie in all_cookies:
        name = cookie.get("name")
        value = cookie.get("value")
        domain = str(cookie.get("domain", ""))
        if isinstance(name, str) and isinstance(value, str) and _is_reddit_domain(domain):
            filtered[name] = value
    return filtered


async def _collect_reddit_cookies_via_playwright(*, silent: bool) -> dict[str, str]:
    """Collect Reddit cookies by reading copied local browser profiles.

    This keeps the user's live browser session isolated from automation:
    we never attach to an already running browser process.
    """
    try:
        from playwright.async_api import async_playwright
    except ImportError as exc:
        raise RuntimeError(
            "The Playwright Python dependency is missing from this reddit-mcp install. "
            f"Run `{PLAYWRIGHT_REINSTALL_HINT}`."
        ) from exc

    candidates = _discover_browser_profile_candidates()
    reusable_candidates = [c for c in candidates if c.reddit_cookie_rows > 0]

    if reusable_candidates and not silent:
        console_print("🔍 Trying to import existing Reddit session from local browser profile copy...\n")

    for candidate in reusable_candidates:
        cookies = await _read_cookies_from_profile_copy(
            candidate,
            async_playwright=async_playwright,
        )
        if "reddit_session" in cookies:
            if not silent:
                console_print(
                    "✅ Imported Reddit session from "
                    f"{candidate.browser_label} ({candidate.profile_name})."
                )
            return cookies

    if silent:
        raise RuntimeError(
            "No reusable Reddit session found in local browser profiles. "
            "Run `reddit-mcp setup-cookies` after logging in to Reddit in your normal browser."
        )

    if not candidates:
        raise RuntimeError(
            "No compatible local Chrome/Edge profile found. "
            "Install Chrome/Edge and sign in to Reddit, then rerun `reddit-mcp setup-cookies`."
        )

    if _should_hint_windows_shutdown_browser(candidates):
        raise RuntimeError(
            "Found local Chrome/Edge browser profiles with cookie databases, but Windows could not "
            "read the local cookie database reliably. The browser may still be locking the cookie files. "
            "Please save any unfinished work in Chrome or Edge, then rerun "
            "`reddit-mcp setup-cookies --shutdown-browser`. "
            "That command will try to close Chrome and Edge gracefully, unlock the cookie files, "
            "and continue the cookie import flow. "
            "If you are an AI assistant, explain this impact clearly and wait for the user's approval "
            "before running that command."
        )

    console_print(
        "🌐 No reusable Reddit session found in copied local profiles."
    )
    console_print("   👉 Log in to Reddit in your normal local browser profile now.")
    console_print(
        "   Waiting for a reusable Reddit session to appear so it can be imported...\n"
    )

    cookies = await _wait_for_source_profile_session(
        candidates,
        async_playwright=async_playwright,
    )
    if "reddit_session" in cookies:
        return cookies

    raise RuntimeError(
        "No reusable Reddit session appeared in local browser profiles before timeout. "
        "Log in to Reddit in your normal browser, then rerun `reddit-mcp setup-cookies`."
    )


async def _collect_reddit_cookies_via_guided_browser_login(*, silent: bool) -> dict[str, str]:
    """Collect Reddit cookies from a dedicated real local browser profile.

    Why: Reddit login on Windows can reject Playwright-managed Chromium as a
    testing browser. We still isolate state in an app-owned profile, but we
    insist on a real local Chrome/Edge executable so the user signs in through
    the same browser stack they normally use.
    """
    try:
        from playwright.async_api import async_playwright
    except ImportError as exc:
        raise RuntimeError(
            "The Playwright Python dependency is missing from this reddit-mcp install. "
            f"Run `{PLAYWRIGHT_REINSTALL_HINT}`."
        ) from exc

    ensure_config_dir()
    browser_label, executable_path = _select_guided_login_browser_installation()
    BROWSER_PROFILE.mkdir(parents=True, exist_ok=True)

    if not silent:
        console_print(
            "🌐 Opening a dedicated local browser window for Reddit login.\n"
        )
        console_print(
            "   reddit-mcp stores this login in its own local browser profile.\n"
        )
        console_print(
            f"   Using your installed {browser_label} executable with an isolated reddit-mcp profile,\n"
            "   so Reddit sees a real local browser while your normal browser profile stays untouched.\n"
        )
        console_print(
            "   If you are using OpenClaw on a cloud desktop, make sure the user opens the\n"
            "   cloud desktop and signs in to Reddit in the browser window that just opened.\n"
        )

    async with async_playwright() as playwright:
        process = None
        browser = None
        try:
            port = _allocate_debug_port()
            process = _spawn_guided_login_browser_process(
                executable_path=executable_path,
                profile_dir=BROWSER_PROFILE,
                port=port,
            )
            await _wait_for_cdp_ready(port)
            browser = await playwright.chromium.connect_over_cdp(f"http://127.0.0.1:{port}")
            try:
                context = browser.contexts[0] if browser.contexts else await browser.new_context()
                page = context.pages[0] if context.pages else await context.new_page()
                if page.url == "about:blank":
                    await page.goto("https://www.reddit.com/login", wait_until="domcontentloaded")
                return await _wait_for_guided_login_session(
                    context,
                    page,
                )
            finally:
                await browser.close()
        finally:
            if process is not None:
                _terminate_process(process)


def _discover_browser_profile_candidates() -> list[BrowserProfileCandidate]:
    candidates: list[BrowserProfileCandidate] = []
    for browser_label, executable_path, user_data_dir in _known_browser_installations():
        if not executable_path.exists() or not user_data_dir.exists():
            continue
        for profile_name, profile_dir in _list_profile_dirs(user_data_dir):
            cookies_db = _find_cookie_db(profile_dir)
            if cookies_db is None:
                continue
            probe = _probe_reddit_cookie_rows(cookies_db)
            try:
                cookies_mtime = cookies_db.stat().st_mtime
            except OSError:
                cookies_mtime = 0.0
            candidates.append(
                BrowserProfileCandidate(
                    browser_label=browser_label,
                    executable_path=executable_path,
                    user_data_dir=user_data_dir,
                    profile_name=profile_name,
                    profile_dir=profile_dir,
                    cookies_db=cookies_db,
                    reddit_cookie_rows=probe.reddit_cookie_rows,
                    cookies_mtime=cookies_mtime,
                    cookie_probe_error=probe.error,
                )
            )

    # Prefer profiles that already contain Reddit cookies, then by freshness.
    candidates.sort(key=lambda c: (c.reddit_cookie_rows > 0, c.cookies_mtime), reverse=True)
    return candidates


def _select_guided_login_browser_installation() -> tuple[str, Path]:
    seen_paths: set[Path] = set()
    for browser_label, executable_path, _user_data_dir in _known_browser_installations():
        if executable_path in seen_paths:
            continue
        seen_paths.add(executable_path)
        if executable_path.exists():
            return browser_label, executable_path
    raise RuntimeError(
        "reddit-mcp needs a local Chrome or Edge installation for Windows cookie setup. "
        "Install Chrome or Edge, then rerun `reddit-mcp setup-cookies`."
    )


def _should_hint_windows_shutdown_browser(
    candidates: list[BrowserProfileCandidate],
) -> bool:
    if sys.platform != "win32" or not candidates:
        return False
    if any(candidate.cookie_probe_error for candidate in candidates):
        return True
    # Why: live Chrome/Edge can keep cookie updates in active profile state that
    # never becomes reusable from copied snapshots until the browser shuts down.
    return bool(_list_running_windows_browser_processes())


def _known_browser_installations() -> list[tuple[str, Path, Path]]:
    home = Path.home()

    if sys.platform == "darwin":
        return [
            (
                "Google Chrome",
                Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
                home / "Library/Application Support/Google/Chrome",
            ),
            (
                "Microsoft Edge",
                Path("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
                home / "Library/Application Support/Microsoft Edge",
            ),
            (
                "Chromium",
                Path("/Applications/Chromium.app/Contents/MacOS/Chromium"),
                home / "Library/Application Support/Chromium",
            ),
        ]

    if sys.platform.startswith("linux"):
        return [
            ("Google Chrome", Path("/usr/bin/google-chrome"), home / ".config/google-chrome"),
            ("Chromium", Path("/usr/bin/chromium"), home / ".config/chromium"),
            ("Microsoft Edge", Path("/usr/bin/microsoft-edge"), home / ".config/microsoft-edge"),
        ]

    local_app_data = Path(os.environ.get("LOCALAPPDATA", ""))
    program_files = Path(os.environ.get("PROGRAMFILES", r"C:\\Program Files"))
    program_files_x86 = Path(os.environ.get("PROGRAMFILES(X86)", r"C:\\Program Files (x86)"))
    if local_app_data:
        return [
            (
                "Google Chrome",
                program_files / "Google/Chrome/Application/chrome.exe",
                local_app_data / "Google/Chrome/User Data",
            ),
            (
                "Google Chrome",
                program_files_x86 / "Google/Chrome/Application/chrome.exe",
                local_app_data / "Google/Chrome/User Data",
            ),
            (
                "Microsoft Edge",
                program_files / "Microsoft/Edge/Application/msedge.exe",
                local_app_data / "Microsoft/Edge/User Data",
            ),
            (
                "Microsoft Edge",
                program_files_x86 / "Microsoft/Edge/Application/msedge.exe",
                local_app_data / "Microsoft/Edge/User Data",
            ),
        ]

    return []


def _list_profile_dirs(user_data_dir: Path) -> list[tuple[str, Path]]:
    items: list[tuple[str, Path]] = []
    default = user_data_dir / "Default"
    if default.is_dir():
        items.append(("Default", default))

    for entry in sorted(user_data_dir.glob("Profile *")):
        if entry.is_dir():
            items.append((entry.name, entry))

    guest = user_data_dir / "Guest Profile"
    if guest.is_dir():
        items.append(("Guest Profile", guest))

    return items


def _probe_reddit_cookie_rows(cookies_db: Path) -> CookieProbeResult:
    try:
        with sqlite3.connect(str(cookies_db)) as conn:
            row = conn.execute(
                "select count(*) from cookies where host_key like '%reddit.com%'"
            ).fetchone()
            return CookieProbeResult(reddit_cookie_rows=int(row[0]) if row else 0)
    except (sqlite3.Error, OSError) as exc:
        return CookieProbeResult(reddit_cookie_rows=0, error=str(exc))


def _find_cookie_db(profile_dir: Path) -> Path | None:
    for relative_path in COOKIE_DB_CANDIDATE_PATHS:
        cookies_db = profile_dir / relative_path
        if cookies_db.exists():
            return cookies_db
    return None


def _shutdown_browser_processes_if_requested(*, silent: bool) -> None:
    if sys.platform != "win32":
        if not silent:
            console_print("ℹ️ `--shutdown-browser` is mainly for Windows profile-lock issues. Continuing.")
        return

    if not silent:
        console_print("🛑 Closing Chrome/Edge before copying the browser profile...\n")

    running = _list_running_windows_browser_processes()
    if not running:
        return

    _run_windows_browser_shutdown()
    remaining = _list_running_windows_browser_processes()
    if remaining:
        raise RuntimeError(
            "Could not fully close browser processes automatically on Windows: "
            f"{', '.join(remaining)}. "
            "Close Chrome/Edge manually, then rerun `reddit-mcp setup-cookies`."
        )


def _list_running_windows_browser_processes() -> list[str]:
    if sys.platform != "win32":
        return []

    quoted_names = ", ".join(f"'{name}'" for name in BROWSER_PROCESS_NAMES)
    script = (
        f"$names=@({quoted_names});"
        "$targets=Get-Process -ErrorAction SilentlyContinue | "
        "Where-Object { $names -contains $_.ProcessName } | "
        "Select-Object -ExpandProperty ProcessName -Unique;"
        "if ($targets) { $targets | ConvertTo-Json -Compress } else { '[]' }"
    )
    result = subprocess.run(
        ["powershell", "-NoProfile", "-Command", script],
        capture_output=True,
        text=True,
        timeout=15,
        check=False,
    )
    if result.returncode != 0:
        return []

    raw = result.stdout.strip() or "[]"
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return []
    if isinstance(data, str):
        return [data]
    if isinstance(data, list):
        return [name for name in data if isinstance(name, str)]
    return []


def _run_windows_browser_shutdown() -> None:
    script = f"""
$names=@({", ".join(f"'{name}'" for name in BROWSER_PROCESS_NAMES)});
$targets=Get-Process -ErrorAction SilentlyContinue | Where-Object {{ $names -contains $_.ProcessName }};
foreach ($p in $targets) {{
  try {{
    if ($p.MainWindowHandle -ne 0) {{
      $null = $p.CloseMainWindow();
    }}
  }} catch {{}}
}}
Start-Sleep -Seconds {BROWSER_SHUTDOWN_WAIT_SECONDS}
$remaining=Get-Process -ErrorAction SilentlyContinue | Where-Object {{ $names -contains $_.ProcessName }};
foreach ($p in $remaining) {{
  try {{
    Stop-Process -Id $p.Id -ErrorAction SilentlyContinue;
  }} catch {{}}
}}
Start-Sleep -Seconds 1
"""
    subprocess.run(
        ["powershell", "-NoProfile", "-Command", script],
        capture_output=True,
        text=True,
        timeout=30,
        check=False,
    )


async def _wait_for_source_profile_session(
    candidates: list[BrowserProfileCandidate],
    *,
    async_playwright: Any,
) -> dict[str, str]:
    deadline = time.monotonic() + LOGIN_WAIT_TIMEOUT_SECONDS
    while time.monotonic() < deadline:
        for candidate in candidates:
            cookies = await _read_cookies_from_profile_copy(
                candidate,
                async_playwright=async_playwright,
            )
            if "reddit_session" in cookies:
                return cookies
        await anyio.sleep(SOURCE_PROFILE_POLL_INTERVAL_MS / 1000)
    return {}


async def _read_cookies_from_profile_copy(
    candidate: BrowserProfileCandidate,
    *,
    async_playwright: Any,
) -> dict[str, str]:
    workspace = Path(tempfile.mkdtemp(prefix="reddit-mcp-profile-copy-"))
    try:
        _copy_profile_workspace(candidate, workspace)

        process, port = _spawn_browser_from_workspace(
            candidate,
            workspace,
        )

        try:
            await _wait_for_cdp_ready(port)

            async with async_playwright() as playwright:
                browser = await playwright.chromium.connect_over_cdp(
                    f"http://127.0.0.1:{port}"
                )
                try:
                    context = browser.contexts[0] if browser.contexts else await browser.new_context()
                    return _filter_reddit_cookies(await context.cookies("https://www.reddit.com"))
                finally:
                    await browser.close()
        finally:
            _terminate_process(process)
    finally:
        _cleanup_workspace(workspace, silent=False)


def _copy_profile_workspace(candidate: BrowserProfileCandidate, workspace: Path) -> None:
    target_profile_dir = workspace / candidate.profile_name
    target_profile_dir.mkdir(parents=True, exist_ok=True)

    for relative_path in _profile_paths_to_copy(candidate):
        source_path = candidate.profile_dir / relative_path
        if not source_path.exists():
            continue
        destination_path = target_profile_dir / relative_path
        destination_path.parent.mkdir(parents=True, exist_ok=True)
        try:
            shutil.copy2(source_path, destination_path)
        except PermissionError as exc:
            if _is_critical_profile_path(relative_path):
                raise _copy_locked_profile_error(candidate, relative_path) from exc
        except OSError as exc:
            if _is_critical_profile_path(relative_path):
                raise RuntimeError(
                    f"Failed to copy required browser profile file '{relative_path}'. "
                    "Run `reddit-mcp setup-cookies` again after closing your browser."
                ) from exc

    local_state = candidate.user_data_dir / "Local State"
    if local_state.exists():
        try:
            shutil.copy2(local_state, workspace / "Local State")
        except PermissionError as exc:
            raise _copy_locked_profile_error(candidate, Path("Local State")) from exc


def _should_skip_profile_path(relative_path: Path) -> bool:
    return any(part in OPTIONAL_PROFILE_DIRS for part in relative_path.parts)


def _profile_paths_to_copy(candidate: BrowserProfileCandidate) -> list[Path]:
    paths: list[Path] = []

    cookie_parent = candidate.cookies_db.parent
    for filename in CRITICAL_PROFILE_FILES:
        relative_path = cookie_parent.relative_to(candidate.profile_dir) / filename
        paths.append(relative_path)

    paths.extend(PROFILE_SUPPORT_FILES)

    deduped: list[Path] = []
    seen: set[Path] = set()
    for path in paths:
        if path in seen or _should_skip_profile_path(path):
            continue
        seen.add(path)
        deduped.append(path)
    return deduped


def _is_critical_profile_path(relative_path: Path) -> bool:
    return relative_path.name in CRITICAL_PROFILE_FILES and (
        len(relative_path.parts) == 1 or relative_path.parts[0] == "Network"
    )


def _copy_locked_profile_error(candidate: BrowserProfileCandidate, relative_path: Path) -> RuntimeError:
    if sys.platform == "win32":
        return RuntimeError(
            "Browser profile files are locked by a running browser process on Windows "
            f"({candidate.browser_label} / {relative_path}). "
            "Please save any unfinished work in Chrome or Edge, then rerun "
            "`reddit-mcp setup-cookies --shutdown-browser`. "
            "That command will try to close Chrome and Edge gracefully, unlock the cookie files, "
            "and continue the cookie import flow. "
            "If you are an AI assistant, explain this impact clearly and wait for the user's approval "
            "before running that command."
        )
    return RuntimeError(
        f"Browser profile file '{relative_path}' is locked and could not be copied. "
        "Close your browser completely, then rerun `reddit-mcp setup-cookies`."
    )


def _spawn_browser_from_workspace(
    candidate: BrowserProfileCandidate,
    workspace: Path,
) -> tuple[subprocess.Popen[bytes], int]:
    port = _allocate_debug_port()

    args = [
        str(candidate.executable_path),
        f"--user-data-dir={workspace}",
        f"--profile-directory={candidate.profile_name}",
        f"--remote-debugging-port={port}",
        "--no-first-run",
        "--no-default-browser-check",
        # Why: block account-sync side effects that can rotate tokens in the source profile.
        "--disable-sync",
        "--disable-background-networking",
        "--disable-features=AccountReconcilor,IdentityManager",
        "--disable-client-side-phishing-detection",
        "--disable-component-update",
        "--disable-extensions",
        "--disable-breakpad",
        "--metrics-recording-only",
    ]
    args.append("--headless=new")
    args.append("about:blank")

    process = subprocess.Popen(
        args,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    return process, port


def _spawn_guided_login_browser_process(
    *,
    executable_path: Path,
    profile_dir: Path,
    port: int,
) -> subprocess.Popen[bytes]:
    args = [
        str(executable_path),
        f"--user-data-dir={profile_dir}",
        "--no-first-run",
        "--no-default-browser-check",
        # Why: keep the dedicated reddit-mcp profile isolated from browser account sync state.
        "--disable-sync",
        "--disable-background-networking",
        "--disable-features=AccountReconcilor,IdentityManager",
        "--disable-client-side-phishing-detection",
        "--disable-component-update",
        "--disable-extensions",
        "--disable-breakpad",
        "--metrics-recording-only",
        f"--remote-debugging-port={port}",
        "https://www.reddit.com/login",
    ]

    return subprocess.Popen(
        args,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )



def _allocate_debug_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])


async def _wait_for_cdp_ready(port: int) -> None:
    deadline = time.monotonic() + BROWSER_START_TIMEOUT_SECONDS
    endpoint = f"http://127.0.0.1:{port}/json/version"
    while time.monotonic() < deadline:
        try:
            with urlopen(endpoint, timeout=2) as response:
                payload = json.loads(response.read().decode("utf-8"))
                if payload.get("webSocketDebuggerUrl"):
                    return
        except Exception:
            pass
        await anyio.sleep(0.5)

    raise RuntimeError("Timed out waiting for browser CDP endpoint")


async def _wait_for_guided_login_session(
    context: Any,
    page: Any,
) -> dict[str, str]:
    client = await context.new_cdp_session(page)
    deadline = time.monotonic() + LOGIN_WAIT_TIMEOUT_SECONDS

    while time.monotonic() < deadline:
        if page.is_closed():
            raise RuntimeError(
                "The guided browser window was closed before Reddit login completed. "
                "Run `reddit-mcp setup-cookies` again and finish the login in that window."
            )

        try:
            result = await client.send("Network.getAllCookies")
        except Exception as exc:
            raise RuntimeError(
                "Lost access to the guided browser session before Reddit login completed. "
                "Run `reddit-mcp setup-cookies` again."
            ) from exc

        cookies = _filter_reddit_cookies(result.get("cookies", []))
        if "reddit_session" in cookies:
            return cookies

        await anyio.sleep(GUIDED_LOGIN_POLL_INTERVAL_SECONDS)

    raise RuntimeError(
        "Timed out waiting for Reddit login in the dedicated browser window. "
        "Run `reddit-mcp setup-cookies` again and complete the login there."
    )


def _terminate_process(process: subprocess.Popen[bytes]) -> None:
    if process.poll() is not None:
        return
    process.terminate()
    try:
        process.wait(timeout=5)
    except subprocess.TimeoutExpired:
        if sys.platform == "win32":
            subprocess.run(
                ["taskkill", "/PID", str(process.pid), "/T", "/F"],
                capture_output=True,
                text=True,
                timeout=15,
                check=False,
            )
            return
        process.kill()


def _cleanup_workspace(workspace: Path, *, silent: bool) -> None:
    if not workspace.exists():
        return

    attempts = CLEANUP_RETRY_COUNT if sys.platform == "win32" else 1
    last_error: PermissionError | None = None

    for attempt in range(attempts):
        try:
            shutil.rmtree(workspace)
            return
        except PermissionError as exc:
            last_error = exc
            if attempt == attempts - 1:
                break
            time.sleep(CLEANUP_RETRY_DELAY_SECONDS)

    if last_error and not silent:
        console_print(
            "⚠️ Could not fully remove the temporary browser workspace on Windows because "
            "the copied browser profile was still shutting down. The cookie import result "
            "is preserved, but some temp files may be cleaned up later by the OS."
        )
