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

Strategy:
- Never attach to a live user browser process.
- Always copy a local browser profile to a temporary workspace first.
- Launch browser against the copied workspace and read cookies through CDP.
"""

from __future__ import annotations

import asyncio
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

from .config import (
    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
COOKIE_POLL_INTERVAL_MS = 2000


@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


async def setup_cookies(*, silent: 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()
    cookies = await _collect_reddit_cookies_via_playwright(silent=silent)

    # Validate BEFORE persisting — don't save bad cookies
    valid = await check_cookies_valid(cookies)
    if not valid:
        raise RuntimeError(
            "Cookies were obtained but validation failed. "
            "Please check your Reddit account and try again."
        )

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

    if not silent:
        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
    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.
    """
    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,
            )
            if resp.status_code != 200:
                return False
            data = json.loads(resp.text)
            return data.get("kind") == "t2" or bool(data.get("data", {}).get("name"))
    except Exception:
        return False


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(
            "Playwright is required for cookie setup. "
            "Run `pip install playwright && playwright install chromium`."
        ) 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:
        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,
            interactive=False,
            async_playwright=async_playwright,
        )
        if "reddit_session" in cookies:
            if not silent:
                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` interactively to log in once."
        )

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

    primary = interactive_candidates[0]
    print(
        "🌐 Opening copied browser profile for manual Reddit login: "
        f"{primary.browser_label} ({primary.profile_name})"
    )
    print("   (Your real browser profile is not used directly.)\n")

    cookies = await _read_cookies_from_profile_copy(
        primary,
        interactive=True,
        async_playwright=async_playwright,
    )
    if "reddit_session" in cookies:
        return cookies

    raise RuntimeError(
        "Timed out waiting for Reddit login in copied profile. "
        "Please run `reddit-mcp setup-cookies` and complete login in the opened window."
    )


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 = profile_dir / "Cookies"
            if not cookies_db.exists():
                continue
            reddit_cookie_rows = _count_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=reddit_cookie_rows,
                    cookies_mtime=cookies_mtime,
                )
            )

    # 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 _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 _count_reddit_cookie_rows(cookies_db: Path) -> int:
    try:
        with sqlite3.connect(str(cookies_db)) as conn:
            row = conn.execute(
                "select count(*) from cookies where host_key like '%reddit.com%'"
            ).fetchone()
            return int(row[0]) if row else 0
    except sqlite3.Error:
        return 0


async def _read_cookies_from_profile_copy(
    candidate: BrowserProfileCandidate,
    *,
    interactive: bool,
    async_playwright: Any,
) -> dict[str, str]:
    with tempfile.TemporaryDirectory(prefix="reddit-mcp-profile-copy-") as temp_dir:
        workspace = Path(temp_dir)
        _copy_profile_workspace(candidate, workspace)

        process, port = _spawn_browser_from_workspace(
            candidate,
            workspace,
            headless=not interactive,
        )

        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()
                    if not interactive:
                        return _filter_reddit_cookies(await context.cookies("https://www.reddit.com"))

                    page = context.pages[0] if context.pages else await context.new_page()
                    await page.goto("https://www.reddit.com", wait_until="domcontentloaded")
                    print("   👉 If needed, log in to Reddit in the opened copied-profile window.")
                    print("   (Waiting up to 3 minutes...)\n")

                    deadline = time.monotonic() + LOGIN_WAIT_TIMEOUT_SECONDS
                    while time.monotonic() < deadline:
                        cookies = _filter_reddit_cookies(
                            await context.cookies("https://www.reddit.com")
                        )
                        if "reddit_session" in cookies:
                            return cookies
                        await page.wait_for_timeout(COOKIE_POLL_INTERVAL_MS)
                    return {}
                finally:
                    await browser.close()
        finally:
            _terminate_process(process)


def _copy_profile_workspace(candidate: BrowserProfileCandidate, workspace: Path) -> None:
    target_profile_dir = workspace / candidate.profile_name
    ignore = shutil.ignore_patterns(
        "Cache",
        "Code Cache",
        "GPUCache",
        "ShaderCache",
        "GrShaderCache",
        "GraphiteDawnCache",
        "Crashpad",
    )

    shutil.copytree(
        candidate.profile_dir,
        target_profile_dir,
        dirs_exist_ok=True,
        ignore=ignore,
    )

    local_state = candidate.user_data_dir / "Local State"
    if local_state.exists():
        shutil.copy2(local_state, workspace / "Local State")


def _spawn_browser_from_workspace(
    candidate: BrowserProfileCandidate,
    workspace: Path,
    *,
    headless: bool,
) -> 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",
    ]
    if headless:
        args.append("--headless=new")
    args.append("about:blank")

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


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 asyncio.sleep(0.5)

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


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:
        process.kill()
