"""Browser cookie extraction for last30days.

Extracts cookies from local browser databases (Firefox, Chrome, Brave, Safari)
to enable zero-config authentication for services like X/Twitter.

Only uses Python stdlib — no external dependencies.
"""

import configparser
import functools
import logging
import platform
import shutil
import sqlite3
import tempfile
from pathlib import Path
from typing import Dict, List, Optional

logger = logging.getLogger(__name__)


@functools.lru_cache(maxsize=1)
def _is_wsl() -> bool:
    """Detect if running under Windows Subsystem for Linux.

    Cached after the first call since /proc/version doesn't change at runtime.
    """
    try:
        return "microsoft" in Path("/proc/version").read_text().lower()
    except OSError:
        return False


def _get_wsl_firefox_profiles_dir() -> Optional[Path]:
    """Find Firefox profiles directory on the Windows host from WSL.

    Scans /mnt/c/Users/*/AppData/Roaming/Mozilla/Firefox for real user
    directories (skips Public, Default, etc.).
    """
    mnt_users = Path("/mnt/c/Users")
    if not mnt_users.is_dir():
        return None
    skip = {"Public", "Default", "Default User", "All Users"}
    try:
        for user_dir in sorted(mnt_users.iterdir()):
            if user_dir.name in skip or not user_dir.is_dir():
                continue
            ff_dir = user_dir / "AppData" / "Roaming" / "Mozilla" / "Firefox"
            if ff_dir.is_dir():
                return ff_dir
    except OSError:
        pass
    return None


def _get_firefox_profiles_dir() -> Optional[Path]:
    """Return the Firefox profiles directory for the current platform, or None."""
    system = platform.system()
    if system == "Darwin":
        path = Path.home() / "Library" / "Application Support" / "Firefox"
    elif system == "Linux":
        path = Path.home() / ".mozilla" / "firefox"
    else:
        # Windows: %APPDATA%\Mozilla\Firefox — best-effort
        appdata = Path.home() / "AppData" / "Roaming" / "Mozilla" / "Firefox"
        path = appdata
    return path if path.is_dir() else None


def _find_default_profile(profiles_dir: Path) -> Optional[Path]:
    """Parse profiles.ini to find the default profile directory.

    Looks for a section with Default=1. Falls back to the first profile
    directory found on disk if profiles.ini is missing or malformed.
    """
    ini_path = profiles_dir / "profiles.ini"

    if ini_path.is_file():
        try:
            config = configparser.ConfigParser()
            config.read(str(ini_path), encoding="utf-8")

            # First pass: Install* section (Firefox >= 67 format, takes priority)
            for section in config.sections():
                if section.startswith("Install") and config.has_option(section, "Default"):
                    raw = config.get(section, "Default")
                    candidate = profiles_dir / raw
                    if candidate.is_dir():
                        return candidate

            # Second pass: Profile section with Default=1
            for section in config.sections():
                if section.startswith("Profile") and config.has_option(section, "Default") and config.get(section, "Default") == "1":
                    return _resolve_profile_path(profiles_dir, config, section)

            # Third pass: first Profile section that exists on disk
            for section in config.sections():
                if section.startswith("Profile"):
                    resolved = _resolve_profile_path(profiles_dir, config, section)
                    if resolved and resolved.is_dir():
                        return resolved
        except (configparser.Error, OSError) as exc:
            logger.debug("Failed to parse profiles.ini: %s", exc)

    # Fallback: scan directory for anything that looks like a profile
    return _fallback_find_profile(profiles_dir)


def _resolve_profile_path(
    profiles_dir: Path, config: configparser.ConfigParser, section: str
) -> Optional[Path]:
    """Resolve a profile path from a ConfigParser section."""
    if not config.has_option(section, "Path"):
        return None
    raw_path = config.get(section, "Path")
    is_relative = config.has_option(section, "IsRelative") and config.get(section, "IsRelative") == "1"
    if is_relative:
        candidate = profiles_dir / raw_path
    else:
        candidate = Path(raw_path)
    return candidate if candidate.is_dir() else None


def _fallback_find_profile(profiles_dir: Path) -> Optional[Path]:
    """Find the first directory that contains cookies.sqlite."""
    try:
        for child in sorted(profiles_dir.iterdir()):
            if child.is_dir() and (child / "cookies.sqlite").is_file():
                return child
    except OSError:
        pass
    return None


def _query_cookies_db(
    db_path: Path, domain: str, cookie_names: List[str]
) -> Optional[Dict[str, str]]:
    """Copy the cookies database to a temp file and query it.

    Firefox locks cookies.sqlite while running, so we copy first.
    Returns {name: value} dict or None if no matching cookies found.
    """
    if not db_path.is_file():
        return None

    tmp_fd = None
    tmp_path = None
    try:
        tmp_fd, tmp_path = tempfile.mkstemp(suffix=".sqlite")
        shutil.copy2(str(db_path), tmp_path)

        conn = sqlite3.connect(tmp_path)
        try:
            # Build parameterized query — SQLite doesn't support array params,
            # so we build the IN clause with individual placeholders.
            placeholders = ",".join("?" for _ in cookie_names)
            query = (
                f"SELECT name, value FROM moz_cookies "
                f"WHERE host LIKE ? AND name IN ({placeholders})"
            )
            # domain pattern: match .x.com, x.com, etc.
            domain_pattern = f"%{domain}"
            params = [domain_pattern] + list(cookie_names)

            cursor = conn.execute(query, params)
            rows = cursor.fetchall()
        finally:
            conn.close()

        if not rows:
            return None
        return {name: value for name, value in rows}

    except (sqlite3.Error, OSError) as exc:
        logger.debug("Failed to query cookies database %s: %s", db_path, exc)
        return None
    finally:
        if tmp_path:
            try:
                Path(tmp_path).unlink(missing_ok=True)
            except OSError:
                pass
        if tmp_fd is not None:
            try:
                import os
                os.close(tmp_fd)
            except OSError:
                pass


def _try_firefox_dir(profiles_dir: Path, domain: str, cookie_names: List[str]) -> Optional[Dict[str, str]]:
    """Try to extract cookies from a Firefox profiles directory."""
    profile_path = _find_default_profile(profiles_dir)
    if profile_path is None:
        logger.debug("No Firefox profile found in %s", profiles_dir)
        return None
    return _query_cookies_db(profile_path / "cookies.sqlite", domain, cookie_names)


def extract_firefox_cookies(
    domain: str, cookie_names: List[str]
) -> Optional[Dict[str, str]]:
    """Extract cookies from Firefox for the given domain and cookie names.

    Finds the default Firefox profile, copies cookies.sqlite to a temp file
    (to avoid lock conflicts), and queries for the requested cookies.

    On WSL2, falls back to Windows Firefox if native Linux Firefox has no
    matching cookies. Windows Firefox cookies are unencrypted, so this works
    without DPAPI or any Windows-side helpers.

    Args:
        domain: The cookie domain to match (e.g. ".x.com"). Matched with LIKE %domain.
        cookie_names: List of cookie names to extract (e.g. ["auth_token", "ct0"]).

    Returns:
        Dict of {cookie_name: cookie_value} or None if extraction fails.
    """
    profiles_dir = _get_firefox_profiles_dir()
    if profiles_dir is not None:
        result = _try_firefox_dir(profiles_dir, domain, cookie_names)
        if result is not None:
            return result

    if platform.system() == "Linux" and _is_wsl():
        wsl_dir = _get_wsl_firefox_profiles_dir()
        if wsl_dir is not None:
            logger.debug("Trying Windows Firefox via WSL: %s", wsl_dir)
            return _try_firefox_dir(wsl_dir, domain, cookie_names)

    if profiles_dir is None:
        logger.debug("Firefox profiles directory not found")
    return None


def extract_chrome_cookies(
    domain: str, cookie_names: List[str]
) -> Optional[Dict[str, str]]:
    """Extract cookies from Chrome for the given domain and cookie names.

    macOS only — uses Keychain + system openssl for AES-128-CBC decryption.
    Linux/Windows not supported (Chrome uses platform-specific encryption).

    Returns:
        Dict of {cookie_name: cookie_value} or None if extraction fails.
    """
    if platform.system() != "Darwin":
        logger.debug("Chrome cookie extraction only supported on macOS")
        return None
    try:
        from .chrome_cookies import extract_chrome_cookies_macos
        return extract_chrome_cookies_macos(domain, cookie_names)
    except Exception as exc:
        logger.debug("Chrome cookie extraction failed: %s", exc)
        return None


def extract_brave_cookies(
    domain: str, cookie_names: List[str]
) -> Optional[Dict[str, str]]:
    """Extract cookies from Brave for the given domain and cookie names.

    macOS only — Brave uses the same v10 AES-128-CBC encryption as Chrome,
    with a different DB path and Keychain service name ("Brave Safe Storage").
    Tries the Default profile first, then scans numbered Profile directories.

    Returns:
        Dict of {cookie_name: cookie_value} or None if extraction fails.
    """
    if platform.system() != "Darwin":
        logger.debug("Brave cookie extraction only supported on macOS")
        return None
    try:
        from .chrome_cookies import extract_brave_cookies_macos
        return extract_brave_cookies_macos(domain, cookie_names)
    except Exception as exc:
        logger.debug("Brave cookie extraction failed: %s", exc)
        return None


def extract_safari_cookies(
    domain: str, cookie_names: List[str]
) -> Optional[Dict[str, str]]:
    """Extract cookies from Safari for the given domain and cookie names.

    macOS only — parses the unencrypted binary cookie file.

    Returns:
        Dict of {cookie_name: cookie_value} or None if extraction fails.
    """
    if platform.system() != "Darwin":
        logger.debug("Safari cookie extraction only supported on macOS")
        return None
    try:
        from .safari_cookies import extract_safari_cookies_macos
        return extract_safari_cookies_macos(domain, cookie_names)
    except Exception as exc:
        logger.debug("Safari cookie extraction failed: %s", exc)
        return None


def extract_cookies(
    browser: str, domain: str, cookie_names: list[str]
) -> Optional[dict[str, str]]:
    """Extract cookies from the specified browser.

    Args:
        browser: One of 'firefox', 'chrome', 'brave', 'safari', or 'auto'.
            'auto' tries browsers in platform-appropriate order:
            - macOS: Chrome -> Brave -> Firefox -> Safari
            - Linux: Firefox only
        domain: The cookie domain to match (e.g. ".x.com").
        cookie_names: List of cookie names to extract.

    Returns:
        Dict of {cookie_name: cookie_value} or None if extraction fails.
    """
    result = extract_cookies_with_source(browser, domain, cookie_names)
    if result is None:
        return None
    cookies, _browser_name = result
    return cookies


def _extract_firefox_with_source(
    domain: str, cookie_names: List[str]
) -> Optional[tuple[Dict[str, str], str]]:
    """Extract Firefox cookies and report whether they came from native or WSL.

    Returns (cookies, "firefox") for native Linux/macOS Firefox, or
    (cookies, "firefox-wsl") for Windows Firefox accessed via WSL2.
    """
    profiles_dir = _get_firefox_profiles_dir()
    if profiles_dir is not None:
        result = _try_firefox_dir(profiles_dir, domain, cookie_names)
        if result is not None:
            return (result, "firefox")

    if platform.system() == "Linux" and _is_wsl():
        wsl_dir = _get_wsl_firefox_profiles_dir()
        if wsl_dir is not None:
            logger.debug("Trying Windows Firefox via WSL: %s", wsl_dir)
            result = _try_firefox_dir(wsl_dir, domain, cookie_names)
            if result is not None:
                return (result, "firefox-wsl")

    return None


def extract_cookies_with_source(
    browser: str, domain: str, cookie_names: list[str]
) -> Optional[tuple[dict[str, str], str]]:
    """Extract cookies and report which browser they came from.

    Same as extract_cookies() but returns a (cookies, browser_name) tuple
    so callers can track the source.

    Args:
        browser: One of 'firefox', 'chrome', 'brave', 'safari', or 'auto'.
        domain: The cookie domain to match (e.g. ".x.com").
        cookie_names: List of cookie names to extract.

    Returns:
        Tuple of ({cookie_name: cookie_value}, browser_name) or None.
        browser_name is "firefox-wsl" when cookies came from Windows Firefox via WSL2.
    """
    extractors = {
        "firefox": extract_firefox_cookies,
        "chrome": extract_chrome_cookies,
        "brave": extract_brave_cookies,
        "safari": extract_safari_cookies,
    }

    if browser != "auto":
        if browser == "firefox":
            return _extract_firefox_with_source(domain, cookie_names)
        extractor = extractors.get(browser)
        if extractor is None:
            logger.warning("Unknown browser: %s", browser)
            return None
        result = extractor(domain, cookie_names)
        return (result, browser) if result is not None else None

    # Auto mode: try browsers in platform-appropriate order
    system = platform.system()
    if system == "Darwin":
        order = ["chrome", "brave", "firefox", "safari"]
    elif system == "Linux":
        order = ["firefox"]
    else:
        order = ["firefox"]

    for name in order:
        if name == "firefox":
            result = _extract_firefox_with_source(domain, cookie_names)
            if result is not None:
                return result
        else:
            result = extractors[name](domain, cookie_names)
            if result is not None:
                return (result, name)

    return None
