"""
heal_intel — loader for the crowdsourced healing-intelligence artifact.

The artifact (`stlc_agents/data/heal_intel.json`) is a versioned set of
distilled priors derived from scrubbed `ScrubbedHealEvent` records that
HealingDashboard users have explicitly confirmed and opted-in to upload.

No raw selectors, intent strings, URLs, or DOM text ever enter the
aggregation pipeline — every field in the upload schema is either an
enum, a boolean, an integer, or a UUID. The intent-keyword field is
restricted to a closed allowlist that is *also* the source of truth
for the migration `locator_registrar` vocabulary.

Three call sites consume `load()`:
  • `agent_playwright_generator.server._gen_locator_healer_cls` —
    templates DEFAULT_HEAL_STRATEGY_ORDER and _intentShapeFilter.
  • `agent_playwright_generator.server._gen_locators_from_context_map` —
    ranks candidate attributes per tag.
  • `agent_migration.transformer.locator_registrar` — intent inference.

If the JSON is missing, malformed, or carries a schema version this
module doesn't understand, `load()` returns `_FALLBACK` so callers
keep working with the historical hardcoded defaults.
"""
from __future__ import annotations

import json
from functools import lru_cache
from importlib.resources import files

CURRENT_SCHEMA = 1

# Historical hardcoded defaults — kept in lockstep with the values that lived
# in agent_playwright_generator.server before this module existed. These are
# what callers see when heal_intel.json is missing.
_FALLBACK: dict = {
    "schema": CURRENT_SCHEMA,
    "trained_through": None,
    "sample_size": 0,
    "min_observations_per_rule": 0,
    "intent_keyword_allowlist": [],
    "intent_shapes": {
        "input":    ["input", "field", "textbox", "textarea", "password", "email",
                     "search", "phone", "tel", "number", "date", "select",
                     "dropdown", "combobox", "checkbox", "radio"],
        "button":   ["button"],
        "link":     ["link"],
    },
    "intent_shape_filters": {
        "input":  ":is(input, textarea, select, [contenteditable])",
        "button": ':is(button, [role="button"], input[type="submit"], input[type="button"])',
        "link":   ':is(a, [role="link"])',
    },
    "strategy_priority_by_shape": {
        "generic": ["attribute", "type-hint", "role", "label", "text"],
    },
    "preferred_attribute_order": {
        "*": ["data-testid", "data-test", "id", "aria-label", "role+text", "name"],
    },
    "scrubber": {
        "selector_attribute_allowlist": [
            "data-testid", "data-test", "aria-label", "name", "type", "role",
            "placeholder", "alt", "title", "for",
        ],
        "telemetry_endpoint_default": "https://intel.stlc-agents.dev/v1/heal-events",
        "max_intent_keywords": 4,
        "max_selector_depth": 10,
    },
}


@lru_cache(maxsize=1)
def load() -> dict:
    """Return the heal_intel artifact, or `_FALLBACK` if unavailable.

    Cached for the life of the process — the JSON ships in the wheel and
    doesn't change between calls.
    """
    try:
        raw = files("stlc_agents.data").joinpath("heal_intel.json").read_text()
        data = json.loads(raw)
    except (FileNotFoundError, ModuleNotFoundError, json.JSONDecodeError):
        return _FALLBACK
    if not isinstance(data, dict) or data.get("schema") != CURRENT_SCHEMA:
        return _FALLBACK
    return data


def strategy_order_for(shape: str) -> list[str]:
    """Strategy order for a given intent shape, falling back to `generic`."""
    intel = load()
    table = intel.get("strategy_priority_by_shape") or {}
    return list(table.get(shape) or table.get("generic") or _FALLBACK["strategy_priority_by_shape"]["generic"])


def attribute_order_for(tag: str) -> list[str]:
    """Preferred selector-attribute order for an element tag."""
    intel = load()
    table = intel.get("preferred_attribute_order") or {}
    return list(table.get(tag) or table.get("*") or _FALLBACK["preferred_attribute_order"]["*"])


def intent_keyword_allowlist() -> list[str]:
    """The closed vocabulary used by both the scrubber and the migration."""
    return list(load().get("intent_keyword_allowlist") or [])


def intent_shapes() -> dict[str, list[str]]:
    """Map of intent-shape → keywords that imply that shape."""
    return dict(load().get("intent_shapes") or _FALLBACK["intent_shapes"])


def intent_shape_filters() -> dict[str, str]:
    """Map of intent-shape → CSS `:is(...)` prefix used by LocatorHealer."""
    return dict(load().get("intent_shape_filters") or _FALLBACK["intent_shape_filters"])


def selector_attribute_allowlist() -> list[str]:
    """Attribute names safe to send in scrubbed events (values are NEVER sent)."""
    return list((load().get("scrubber") or {}).get("selector_attribute_allowlist")
                or _FALLBACK["scrubber"]["selector_attribute_allowlist"])


def classify_intent_shape(intent: str) -> str:
    """Map a free-form intent string to one of the known intent shapes.

    Used by the migration registrar to share the same shape inference as
    the runtime `LocatorHealer._intentShapeFilter`. Falls back to 'generic'.
    """
    lower = (intent or "").lower()
    for shape, keywords in intent_shapes().items():
        if any(kw in lower for kw in keywords):
            return shape
    return "generic"


def extract_intent_keywords(intent: str, limit: int | None = None) -> list[str]:
    """Pick up to `limit` words from the closed allowlist that appear in `intent`.

    The output preserves first-appearance order. Anything not in the
    allowlist is dropped — never hashed, never sent. This is the only
    string-typed value any scrubbed event ever carries.
    """
    if limit is None:
        limit = (load().get("scrubber") or {}).get("max_intent_keywords", 4)
    allow = set(intent_keyword_allowlist())
    if not allow:
        return []
    seen: list[str] = []
    for token in (intent or "").lower().split():
        token = "".join(c for c in token if c.isalnum())
        if token in allow and token not in seen:
            seen.append(token)
            if len(seen) >= limit:
                break
    return seen


def telemetry_endpoint_default() -> str:
    return (load().get("scrubber") or {}).get(
        "telemetry_endpoint_default",
        _FALLBACK["scrubber"]["telemetry_endpoint_default"],
    )
