"""
Utility helpers: selector type detection, selector normalisation,
DOM extraction, and fingerprinting.
"""

from __future__ import annotations

import hashlib
import re
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, List, Optional

from bs4 import BeautifulSoup, Tag


class SelectorType(str, Enum):
    CSS = "css"
    XPATH = "xpath"
    ROLE = "role"       # get_by_role   — format: ROLE::NAME
    TEXT = "text"       # get_by_text
    PLACEHOLDER = "placeholder"
    LABEL = "label"     # get_by_label
    TEST_ID = "testid"  # get_by_test_id
    ALT = "alt"         # get_by_alt_text
    TITLE = "title"     # get_by_title


def detect_selector_type(selector: str) -> SelectorType:
    """Auto-detect the selector type from the string format."""
    s = selector.strip()
    if s.startswith("//") or s.startswith("(//") or s.startswith("./"):
        return SelectorType.XPATH
    if "::" in s:
        role = s.split("::")[0].lower()
        known_roles = {
            "button", "link", "textbox", "checkbox", "radio", "combobox",
            "listbox", "option", "heading", "img", "cell", "row", "table",
            "grid", "tab", "tabpanel", "dialog", "alertdialog", "banner",
            "navigation", "main", "region", "search", "form", "article",
            "list", "listitem", "menuitem", "menubar", "menu", "slider",
            "spinbutton", "progressbar", "status", "alert", "tooltip",
        }
        if role in known_roles:
            return SelectorType.ROLE
    return SelectorType.CSS


def is_playwright_role_selector(selector: str) -> bool:
    return detect_selector_type(selector) == SelectorType.ROLE


@dataclass
class ElementFingerprint:
    """Compact descriptor of an element's identity — used for heuristic matching."""

    tag: str = ""
    id: str = ""
    name: str = ""
    classes: List[str] = None   # type: ignore[assignment]
    text: str = ""
    aria_label: str = ""
    aria_role: str = ""
    placeholder: str = ""
    data_testid: str = ""
    type_attr: str = ""
    href: str = ""

    def __post_init__(self) -> None:
        if self.classes is None:
            self.classes = []

    def to_fingerprint_hash(self) -> str:
        parts = [
            self.tag, self.id, self.name,
            " ".join(sorted(self.classes)),
            self.text[:50],
            self.aria_label, self.aria_role,
        ]
        return hashlib.md5("|".join(parts).encode()).hexdigest()[:12]


def extract_fingerprint(element: Tag) -> ElementFingerprint:
    """Extract a compact fingerprint from a BeautifulSoup Tag."""
    return ElementFingerprint(
        tag=element.name or "",
        id=element.get("id", ""),  # type: ignore[arg-type]
        name=element.get("name", ""),  # type: ignore[arg-type]
        classes=element.get("class", []),
        text=element.get_text(strip=True)[:100],
        aria_label=element.get("aria-label", ""),  # type: ignore[arg-type]
        aria_role=element.get("role", ""),  # type: ignore[arg-type]
        placeholder=element.get("placeholder", ""),  # type: ignore[arg-type]
        data_testid=element.get("data-testid", "") or element.get("data-test-id", ""),  # type: ignore[arg-type]
        type_attr=element.get("type", ""),  # type: ignore[arg-type]
        href=element.get("href", ""),  # type: ignore[arg-type]
    )


def strip_dom_for_ai(full_html: str, max_chars: int = 8000) -> str:
    """
    Clean and truncate HTML for AI context:
    - Remove <script>, <style>, SVG inline content, comments
    - Keep interactive and landmark elements
    - Return a compact but meaningful representation
    """
    try:
        soup = BeautifulSoup(full_html, "lxml")
    except Exception:
        return full_html[:max_chars]

    # Remove noise
    for tag in soup.find_all(["script", "style", "noscript", "meta", "link"]):
        tag.decompose()
    for el in soup.find_all(True):
        # Remove SVG paths (very noisy)
        if el.name == "path":
            el.decompose()

    # Pretty-print compactly
    result = soup.prettify()
    if len(result) <= max_chars:
        return result

    # If still too long, extract only interactive elements
    relevant_tags = ["input", "button", "a", "select", "textarea", "label",
                     "form", "nav", "header", "main", "footer", "h1", "h2",
                     "h3", "[role]", "[aria-label]", "[data-testid]"]
    try:
        soup2 = BeautifulSoup(full_html, "lxml")
        for tag in soup2.find_all(["script", "style", "noscript", "meta", "link"]):
            tag.decompose()
        elements = []
        for t in ["input", "button", "a", "select", "textarea", "label"]:
            elements.extend(str(el) for el in soup2.find_all(t)[:50])
        return "\n".join(elements)[:max_chars]
    except Exception:
        return result[:max_chars]


def css_id_from_selector(selector: str) -> Optional[str]:
    """Extract the ID from a CSS selector like '#submit-btn' → 'submit-btn'."""
    m = re.match(r"^#([\w-]+)$", selector.strip())
    return m.group(1) if m else None


def css_classes_from_selector(selector: str) -> List[str]:
    """Extract class names from a CSS selector like '.btn.primary' → ['btn', 'primary']."""
    return re.findall(r"\.([\w-]+)", selector)


def make_css_from_fingerprint(fp: ElementFingerprint) -> List[str]:
    """Generate CSS selector candidates from a fingerprint."""
    candidates: List[str] = []
    if fp.id:
        candidates.append(f"#{fp.id}")
    if fp.data_testid:
        candidates.append(f'[data-testid="{fp.data_testid}"]')
        candidates.append(f'[data-test-id="{fp.data_testid}"]')
    if fp.name:
        candidates.append(f'[name="{fp.name}"]')
    if fp.aria_label:
        candidates.append(f'[aria-label="{fp.aria_label}"]')
    if fp.classes:
        candidates.append("." + ".".join(fp.classes))
    if fp.placeholder:
        candidates.append(f'[placeholder="{fp.placeholder}"]')
    if fp.tag and fp.type_attr:
        candidates.append(f'{fp.tag}[type="{fp.type_attr}"]')
    return candidates
