"""
On-demand CSS registry — parses tw.css at startup into a lookup table.

Maps HTML class names (e.g. "flex", "sm:hidden", "h-[60vh]") to their
CSS rule text.  The server uses this to send only the CSS rules needed
for each response, instead of the full Tailwind bundle.

Usage::

    load_css_registry("/path/to/tw.css")
    rules = lookup_rules({"flex", "p-2", "sm:hidden"})
    preamble = get_preamble()
"""
from __future__ import annotations

import hashlib
import re
from collections import OrderedDict
from pathlib import Path
from typing import Sequence


# ---------------------------------------------------------------------------
# Module state
# ---------------------------------------------------------------------------

_REGISTRY: dict[str, str] = {}       # class name → CSS rule text
_RULE_ORDER: dict[str, int] = {}     # class name → source order index
_PREAMBLE: str = ""                   # base/reset CSS (sent once per page)
_ALL_RULES: str = ""                  # full concatenated rules (for Jinja fallback)

# Hash cache: maps 8-char hex hash → frozenset of class names
_CSS_HASH_CACHE: OrderedDict[str, frozenset[str]] = OrderedDict()
_CSS_HASH_CACHE_MAX = 1000


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def load_css_registry(
    path: str | Path,
    *,
    extra_css: Sequence[str | Path] = (),
    url_rewrites: dict[str, str] | None = None,
) -> None:
    """Parse a Tailwind v3 CSS file and populate the registry.

    Parameters
    ----------
    path:
        Path to the main Tailwind CSS file (tw.css).
    extra_css:
        Additional CSS files to include in the preamble (inlined verbatim).
        These are loaded in order and prepended before the Tailwind rules.
    url_rewrites:
        Dict of ``{old_prefix: new_prefix}`` applied to all extra CSS files.
        e.g. ``{"../webfonts/": "/static/fontawesome/webfonts/"}``
    """
    global _PREAMBLE, _ALL_RULES
    _REGISTRY.clear()
    _RULE_ORDER.clear()

    css_path = Path(path)
    css = css_path.read_text(encoding="utf-8")
    rewrites = url_rewrites or {}

    # Load extra CSS files into a combined prefix
    sibling_css = ""
    for extra in extra_css:
        p = Path(extra)
        if p.exists():
            content = p.read_text(encoding="utf-8")
            for old, new in rewrites.items():
                content = content.replace(old, new)
            sibling_css += content

    # Split into preamble (resets, vars) and utility rules.
    # Tailwind v3 minified structure:
    #   - Custom property defaults (*,:after,:before{--tw-...})
    #   - Base resets (html, body, etc.)
    #   - Utility classes (.flex{...}, .hidden{...})
    #   - Responsive variants (@media ...{.sm\:...{...}})
    #
    # We treat everything before the first utility class selector as preamble.
    # A utility class selector starts with "." followed by a word char or backslash.

    preamble_parts: list[str] = []
    utility_css = css

    # Find first bare utility class (not inside a reset block)
    # Heuristic: the preamble ends at the first rule whose selector is ONLY
    # a class (starts with . and no *, :, html, body, etc.)
    #
    # More robust: walk rules and detect when selectors become single-class.
    rules = _split_rules(css)
    preamble_end = 0
    for i, rule in enumerate(rules):
        sel = _extract_selector(rule)
        if sel and _is_utility_selector(sel):
            preamble_end = i
            break

    _PREAMBLE = sibling_css + "".join(rules[:preamble_end])
    _index_rules(rules[preamble_end:])
    _ALL_RULES = _PREAMBLE + "".join(
        _REGISTRY[k] for k in sorted(_REGISTRY, key=lambda k: _RULE_ORDER.get(k, 0))
    )


def get_preamble() -> str:
    """Return the preamble CSS (resets + custom property defaults)."""
    return _PREAMBLE


def get_all_css() -> str:
    """Return preamble + all utility rules (for Jinja fallback pages)."""
    return _ALL_RULES


def lookup_rules(classes: set[str]) -> str:
    """Return concatenated CSS for a set of class names, preserving source order."""
    found = [(name, _RULE_ORDER.get(name, 0)) for name in classes if name in _REGISTRY]
    found.sort(key=lambda t: t[1])
    return "".join(_REGISTRY[name] for name, _ in found)


def scan_classes_from_sx(source: str) -> set[str]:
    """Extract class names from :class "..." patterns in sx source text.

    Works on both component definitions and page sx source.
    Also picks up classes from :class (str ...) concatenation patterns
    and ``;; @css class1 class2 ...`` comment annotations for dynamically
    constructed class names that the regex can't infer.
    """
    classes: set[str] = set()
    # Match :class "value" — the common case
    for m in re.finditer(r':class\s+"([^"]*)"', source):
        classes.update(m.group(1).split())
    # Match :class (str "a" " b" ...) — string concatenation
    for m in re.finditer(r':class\s+\(str\s+((?:"[^"]*"\s*)+)\)', source):
        for s in re.findall(r'"([^"]*)"', m.group(1)):
            classes.update(s.split())
    # Match ;; @css class1 class2 ... — explicit hints for dynamic classes
    for m in re.finditer(r';\s*@css\s+(.+)', source):
        classes.update(m.group(1).split())
    return classes


def register_generated_rule(style_val: Any) -> None:
    """Register a generated StyleValue's CSS rules in the registry.

    This allows generated class names (``sx-a3f2c1``) to flow through
    the existing ``lookup_rules()`` → ``SX-Css`` delta pipeline.
    """
    from .style_dict import CHILD_SELECTOR_ATOMS
    cn = style_val.class_name
    if cn in _REGISTRY:
        return  # already registered

    parts: list[str] = []

    # Base declarations
    if style_val.declarations:
        parts.append(f".{cn}{{{style_val.declarations}}}")

    # Pseudo-class rules
    for sel, decls in style_val.pseudo_rules:
        if sel.startswith("::"):
            parts.append(f".{cn}{sel}{{{decls}}}")
        elif "&" in sel:
            # group-hover pattern: ":is(.group:hover) &" → .group:hover .sx-abc
            expanded = sel.replace("&", f".{cn}")
            parts.append(f"{expanded}{{{decls}}}")
        else:
            parts.append(f".{cn}{sel}{{{decls}}}")

    # Media-query rules
    for query, decls in style_val.media_rules:
        parts.append(f"@media {query}{{.{cn}{{{decls}}}}}")

    # Keyframes
    for _name, kf_rule in style_val.keyframes:
        parts.append(kf_rule)

    rule_text = "".join(parts)
    order = len(_RULE_ORDER) + 10000  # after all tw.css rules
    _REGISTRY[cn] = rule_text
    _RULE_ORDER[cn] = order


def registry_loaded() -> bool:
    """True if the registry has been populated."""
    return bool(_REGISTRY)


def store_css_hash(classes: set[str] | frozenset[str]) -> str:
    """Compute an 8-char hex hash of the class set, store in cache, return it."""
    fs = frozenset(classes)
    key = hashlib.sha256(",".join(sorted(fs)).encode()).hexdigest()[:8]
    # Move to end (LRU) or insert
    _CSS_HASH_CACHE[key] = fs
    _CSS_HASH_CACHE.move_to_end(key)
    # Evict oldest if over limit
    while len(_CSS_HASH_CACHE) > _CSS_HASH_CACHE_MAX:
        _CSS_HASH_CACHE.popitem(last=False)
    return key


def lookup_css_hash(h: str) -> set[str] | None:
    """Look up a class set by its hash. Returns None on cache miss."""
    fs = _CSS_HASH_CACHE.get(h)
    if fs is not None:
        _CSS_HASH_CACHE.move_to_end(h)
        return set(fs)
    return None


# ---------------------------------------------------------------------------
# Internals
# ---------------------------------------------------------------------------

def _split_rules(css: str) -> list[str]:
    """Split minified CSS into individual top-level rules using brace tracking.

    Each returned string is a complete rule including its braces, e.g.:
      ".flex{display:flex}"
      "@media (min-width:640px){.sm\\:hidden{display:none}}"
    """
    rules: list[str] = []
    depth = 0
    start = 0
    i = 0
    while i < len(css):
        ch = css[i]
        if ch == '{':
            depth += 1
        elif ch == '}':
            depth -= 1
            if depth == 0:
                rules.append(css[start:i + 1])
                start = i + 1
        i += 1
    # Trailing content (unlikely in valid CSS)
    if start < len(css):
        tail = css[start:].strip()
        if tail:
            rules.append(tail)
    return rules


def _extract_selector(rule: str) -> str:
    """Extract the selector portion before the first '{' in a rule."""
    brace = rule.find('{')
    return rule[:brace].strip() if brace >= 0 else ""


def _is_utility_selector(sel: str) -> bool:
    """Check if a selector looks like a single utility class (.flex, .\\!p-2, etc).

    Returns False for resets (*,:before,:after{...}), element selectors (html,body),
    and @media / @keyframes wrappers (handled separately).
    """
    if sel.startswith('@'):
        return False
    # Must start with a dot and be a single class
    if not sel.startswith('.'):
        return False
    # Exclude selectors with spaces (descendant combinator, .prose :where(...))
    if ' ' in sel:
        return False
    return True


def _css_selector_to_class(selector: str) -> str:
    """Convert a CSS selector to an HTML class name by unescaping.

    .sm\\:hidden  → sm:hidden
    .h-\\[60vh\\] → h-[60vh]
    .\\!p-2       → !p-2
    .hover\\:text-stone-700:hover → hover:text-stone-700
    """
    # Strip leading dot
    name = selector.lstrip('.')
    # Strip trailing pseudo-class/element (:hover, :focus, ::placeholder, etc.)
    # But don't strip escaped colons (\:) — those are part of the class name.
    # Unescaped colon = pseudo-class boundary.
    # Find the first unescaped colon (not preceded by backslash)
    result = []
    i = 0
    while i < len(name):
        if name[i] == '\\' and i + 1 < len(name):
            result.append(name[i + 1])
            i += 2
        elif name[i] == ':':
            break  # pseudo-class — stop here
        elif name[i] == '[':
            break  # attribute selector — stop here
        else:
            result.append(name[i])
            i += 1
    return "".join(result)


def _index_rules(rules: list[str]) -> None:
    """Index utility rules into _REGISTRY and _RULE_ORDER."""
    order = len(_RULE_ORDER)

    for rule in rules:
        sel = _extract_selector(rule)

        if sel.startswith('@media'):
            # Responsive/container wrapper — extract inner rules
            _index_media_block(rule, order)
            order += 1
            continue

        if sel.startswith('@'):
            # @keyframes, @supports, etc — skip
            continue

        if not sel.startswith('.'):
            continue

        # Compound selectors like ".prose :where(p)..." → key on root class "prose"
        if ' ' in sel:
            root_sel = sel.split(' ', 1)[0]
            class_name = _css_selector_to_class(root_sel)
            if class_name:
                # Append to existing entry
                _REGISTRY[class_name] = _REGISTRY.get(class_name, "") + rule
                if class_name not in _RULE_ORDER:
                    _RULE_ORDER[class_name] = order
            order += 1
            continue

        class_name = _css_selector_to_class(sel)
        if class_name:
            _REGISTRY[class_name] = _REGISTRY.get(class_name, "") + rule
            if class_name not in _RULE_ORDER:
                _RULE_ORDER[class_name] = order
            order += 1


def _index_media_block(rule: str, base_order: int) -> None:
    """Index individual class rules inside an @media block.

    For example:
        @media (min-width:640px){.sm\\:hidden{display:none}.sm\\:flex{display:flex}}

    Each inner rule gets stored wrapped in the @media query.
    """
    # Extract the @media wrapper and inner content
    first_brace = rule.find('{')
    if first_brace < 0:
        return
    media_prefix = rule[:first_brace + 1]  # "@media (min-width:640px){"
    # Inner content is between first { and last }
    inner = rule[first_brace + 1:]
    if inner.endswith('}'):
        inner = inner[:-1]  # strip the closing } of @media

    # Split inner content into individual rules
    inner_rules = _split_rules(inner)
    for i, inner_rule in enumerate(inner_rules):
        sel = _extract_selector(inner_rule)
        class_name = _css_selector_to_class(sel)
        if class_name:
            # Wrap in the @media block
            _REGISTRY[class_name] = media_prefix + inner_rule + "}"
            _RULE_ORDER[class_name] = base_order + i
