"""Extract page-level metadata from a focused desktop web browser.

The browser's window title is the reliable signal — every major browser
formats it as ``"<page title> - <Browser Name>"``, so stripping the suffix
gives us the page title for free.

URL extraction was attempted via UI Automation (UIA), but Chromium-based
browsers (Chrome/Edge/Brave/Vivaldi) keep their accessibility tree dormant
unless a screen reader is active or ``--force-renderer-accessibility`` is
set — neither is something we want to require from end users. The UIA
machinery is still here behind a feature flag in case a future caller
opts into the accessibility-flag path; by default we just return the
page title and leave ``url=None``.

Other platforms (macOS via AppleScript, Linux via AT-SPI) are out of scope
for this iteration.
"""

from __future__ import annotations

import logging
import os
import platform
import threading
from dataclasses import dataclass

logger = logging.getLogger(__name__)

# UIA URL extraction is opt-in because Chromium browsers keep their
# accessibility tree dormant unless the user starts the browser with
# ``--force-renderer-accessibility`` (or a screen reader is running).
# Without that, `FindAll` throws and we'd burn 5s per probe retrying.
# Set MEDIA_SERVER_BROWSER_UIA=1 to enable; default off.
_UIA_ENABLED = os.environ.get("MEDIA_SERVER_BROWSER_UIA", "").lower() in (
    "1", "true", "yes", "on"
)


# Known browser executables (lowercase, .exe-stripped). Used to decide
# whether to spend the UIA query budget on this foreground process.
BROWSER_PROCESS_HINTS: frozenset[str] = frozenset({
    "chrome",
    "msedge",
    "firefox",
    "brave",
    "opera",
    "vivaldi",
    "yandex",
    "browser",  # Yandex Browser sometimes reports as browser.exe
    "arc",
    "thorium",
})


@dataclass(frozen=True)
class BrowserPageInfo:
    url: str | None = None
    page_title: str | None = None


_EMPTY = BrowserPageInfo()


def is_browser_process(process_name: str | None) -> bool:
    """Return True when ``process_name`` looks like a supported browser."""
    if not process_name:
        return False
    base = process_name.lower()
    if base.endswith(".exe"):
        base = base[:-4]
    return base in BROWSER_PROCESS_HINTS


def _strip_browser_suffix(title: str | None, process_name: str | None) -> str | None:
    """Pull the page title out of the browser's window title.

    Most browsers format their window title as ``"<page> - <Browser Name>"``.
    We strip the trailing suffix so consumers get the page title alone. If
    the suffix can't be matched, return the raw title unchanged.
    """
    if not title:
        return None
    suffixes = (
        " - Google Chrome",
        " — Google Chrome",
        " - Microsoft Edge",
        " - Microsoft Edge",
        " — Mozilla Firefox",
        " - Mozilla Firefox",
        " - Brave",
        " - Opera",
        " - Vivaldi",
        " - Yandex",
    )
    for s in suffixes:
        if title.endswith(s):
            return title[: -len(s)].strip() or None
    return title


# ─── UIA lookup (Windows) ───────────────────────────────────────────

# UIA control type / property constants we need. Avoiding the full
# UIAutomationClient typelib generation — those constants are stable.
_UIA_EditControlTypeId = 50004
_UIA_ControlTypePropertyId = 30003
_UIA_ValueValuePropertyId = 30045
_UIA_NamePropertyId = 30005
_UIA_ValuePatternId = 10002
_TreeScope_Descendants = 4
_PropertyConditionFlags_IgnoreCase = 1


# Lazy import + per-thread COM init.
_uia_lock = threading.Lock()
_uia_singleton = None
_uia_load_error: str | None = None
_uia_thread_local = threading.local()


def _ensure_com() -> None:
    """Initialise COM on the current thread (idempotent per thread)."""
    if getattr(_uia_thread_local, "initialised", False):
        return
    try:
        import comtypes  # type: ignore

        # COINIT_APARTMENTTHREADED is required by UIA; comtypes' default
        # CoInitializeEx already passes that flag.
        comtypes.CoInitialize()
        _uia_thread_local.initialised = True
    except Exception as e:
        logger.debug("CoInitialize failed: %s", e)


def _get_uia():
    """Return the IUIAutomation singleton, or None if unavailable."""
    global _uia_singleton, _uia_load_error
    if _uia_singleton is not None:
        return _uia_singleton
    if _uia_load_error is not None:
        return None
    with _uia_lock:
        if _uia_singleton is not None:
            return _uia_singleton
        try:
            import comtypes.client  # type: ignore

            # CLSID for CUIAutomation. Using GetActiveObject would fail,
            # so we cocreate. comtypes.client.CreateObject keeps the COM
            # plumbing tidy.
            _uia_singleton = comtypes.client.CreateObject(
                "{ff48dba4-60ef-4201-aa87-54103eef594e}",
                interface=comtypes.client.GetModule(
                    "UIAutomationCore.dll"
                ).IUIAutomation,
            )
            return _uia_singleton
        except Exception as e:
            _uia_load_error = str(e)
            logger.info("UIA unavailable; browser URL extraction disabled: %s", e)
            return None


def _find_address_bar_value(hwnd: int) -> str | None:
    """Walk the UIA tree under ``hwnd`` looking for the URL Edit control.

    Strategy: find every descendant Edit control, then pick the first one
    whose Name contains an address-bar hint, or — failing that — the first
    one whose value parses as a URL-ish string. Browsers expose extra Edit
    controls (search bars, find-in-page) so name matching is the reliable
    signal; the URL-ish fallback covers locale variants we haven't seen.
    """
    _ensure_com()
    uia = _get_uia()
    if uia is None:
        return None

    try:
        element = uia.ElementFromHandle(hwnd)
        if not element:
            return None

        # Build a condition matching ControlType=Edit, then enumerate.
        edit_condition = uia.CreatePropertyCondition(
            _UIA_ControlTypePropertyId, _UIA_EditControlTypeId
        )
        edits = element.FindAll(_TreeScope_Descendants, edit_condition)
        count = edits.Length if edits else 0
        if count == 0:
            return None

        # Hints (lowercase) used to identify the address bar by its Name
        # property. Covers en-US plus a few common locales / browsers.
        name_hints = (
            "address",          # Chrome/Edge: "Address and search bar"
            "адрес",            # Chrome ru: "Адресная строка и строка поиска"
            "адресная",
            "search with",      # Firefox: "Search with Google or enter address"
            "поиск или ввод",   # Firefox ru
            "url",
            "location",
        )

        # First pass: name-based match (high confidence).
        candidates: list[tuple[int, str]] = []
        for i in range(count):
            edit = edits.GetElement(i)
            try:
                name = (edit.CurrentName or "").lower()
            except Exception:
                name = ""
            try:
                value = edit.GetCurrentPropertyValue(_UIA_ValueValuePropertyId)
            except Exception:
                value = None
            if value is None:
                continue
            value_str = str(value)
            for h in name_hints:
                if h in name:
                    return value_str
            candidates.append((i, value_str))

        # Second pass: URL-ish fallback. Pick the first candidate that
        # looks like a URL; this catches browser/locale combos we haven't
        # listed above.
        for _i, v in candidates:
            lv = v.lower()
            if (
                lv.startswith("http://")
                or lv.startswith("https://")
                or lv.startswith("about:")
                or lv.startswith("chrome://")
                or lv.startswith("edge://")
                or lv.startswith("brave://")
                or lv.startswith("file://")
                or lv.startswith("ftp://")
            ):
                return v

        return None
    except Exception as e:
        logger.debug("UIA address-bar lookup failed: %s", e)
        return None


# ─── Per-(hwnd, title) cache ────────────────────────────────────────

_cache_lock = threading.Lock()
_cache_key: tuple[int | None, str | None] = (None, None)
_cache_value: BrowserPageInfo = _EMPTY


def get_browser_page(
    *,
    hwnd: int | None,
    process_name: str | None,
    window_title: str | None,
) -> BrowserPageInfo:
    """Return the URL + page title for the foreground browser tab, if any.

    Callers pass the already-resolved foreground HWND/title/process_name so
    this service doesn't re-walk Win32 to find them. Returns ``_EMPTY`` for
    non-browser processes or when UIA can't resolve the URL.
    """
    if not is_browser_process(process_name):
        return _EMPTY
    if platform.system() != "Windows":
        # macOS/Linux paths not implemented in this iteration.
        return _EMPTY
    if not hwnd:
        return _EMPTY

    global _cache_key, _cache_value
    key = (hwnd, window_title)
    with _cache_lock:
        if key == _cache_key and _cache_value is not _EMPTY:
            return _cache_value

    url = _find_address_bar_value(hwnd) if _UIA_ENABLED else None
    page_title = _strip_browser_suffix(window_title, process_name)
    info = BrowserPageInfo(url=url, page_title=page_title)

    with _cache_lock:
        _cache_key = key
        _cache_value = info
    return info


def reset_cache() -> None:
    """Reset the cache. Useful in tests."""
    global _cache_key, _cache_value
    with _cache_lock:
        _cache_key = (None, None)
        _cache_value = _EMPTY
