"""Parse generic webhook payloads into ServiceEvent objects using JSONPath mappings."""

from __future__ import annotations

import logging
from collections import OrderedDict
from datetime import datetime, timezone
from typing import Any

from jsonpath_ng import parse as jsonpath_parse
from jsonpath_ng.exceptions import JsonPathParserError

from notify_bridge_core.models.events import EventType, ServiceEvent
from notify_bridge_core.providers.base import ServiceProviderType

_LOGGER = logging.getLogger(__name__)

# Bounded LRU cache for compiled JSONPath expressions (max 1024 entries).
# Expressions come from provider configs so the set is finite in practice,
# but a bound prevents unbounded growth in long-lived processes.
_JSONPATH_CACHE_MAX = 1024
_JSONPATH_CACHE: OrderedDict[str, Any] = OrderedDict()


def _compile_jsonpath(expression: str) -> Any | None:
    """Compile a JSONPath expression, returning cached result or None on error."""
    if expression in _JSONPATH_CACHE:
        _JSONPATH_CACHE.move_to_end(expression)
        return _JSONPATH_CACHE[expression]
    try:
        compiled = jsonpath_parse(expression)
    except (JsonPathParserError, ValueError, TypeError, AttributeError) as exc:
        _LOGGER.warning("Invalid JSONPath expression '%s': %s", expression, exc)
        compiled = None
    _JSONPATH_CACHE[expression] = compiled
    if len(_JSONPATH_CACHE) > _JSONPATH_CACHE_MAX:
        _JSONPATH_CACHE.popitem(last=False)
    return compiled


def _extract_value(compiled_path: Any, data: dict[str, Any], default: Any = "") -> Any:
    """Extract a single value from data using a compiled JSONPath expression."""
    if compiled_path is None:
        return default
    matches = compiled_path.find(data)
    if not matches:
        return default
    return matches[0].value


def parse_webhook(
    payload: dict[str, Any],
    provider_name: str,
    config: dict[str, Any],
    *,
    headers: dict[str, str] | None = None,
) -> ServiceEvent | None:
    """Parse a generic webhook payload into a ServiceEvent.

    Uses JSONPath mappings from the provider config to extract template variables.
    The full payload is always available as ``raw_payload`` in the extra dict.

    Args:
        payload: Parsed JSON body of the webhook.
        provider_name: Display name of the ServiceProvider instance.
        config: Provider configuration containing payload_mappings, etc.
        headers: Optional HTTP headers (made available for event_type_path extraction).

    Returns:
        A ServiceEvent, or None if parsing fails critically.
    """
    # Defensive: upstream callers should pass a dict, but tolerate non-dict
    # payloads by coercing to an empty mapping rather than raising.
    if not isinstance(payload, dict):
        payload = {}
    # Build a combined data dict so JSONPath can reference headers too
    data: dict[str, Any] = {**payload}
    if headers:
        data["headers"] = headers

    # Resolve event type from config path or default
    event_type_raw = "webhook_received"
    event_type_path = config.get("event_type_path", "")
    if event_type_path:
        compiled = _compile_jsonpath(event_type_path)
        extracted = _extract_value(compiled, data, "webhook_received")
        if isinstance(extracted, str) and extracted:
            event_type_raw = extracted

    # Resolve collection from config path or default
    collection_id = ""
    collection_name = ""
    collection_path = config.get("collection_path", "")
    if collection_path:
        compiled = _compile_jsonpath(collection_path)
        extracted = _extract_value(compiled, data, "")
        if isinstance(extracted, str):
            collection_id = extracted
            collection_name = extracted

    # Apply payload_mappings to extract named variables
    extra: dict[str, Any] = {}
    mappings = config.get("payload_mappings", [])
    for mapping in mappings:
        variable = mapping.get("variable", "")
        jsonpath_expr = mapping.get("jsonpath", "")
        default = mapping.get("default", "")
        if not variable or not jsonpath_expr:
            continue
        compiled = _compile_jsonpath(jsonpath_expr)
        extra[variable] = _extract_value(compiled, data, default)

    # Always include raw payload and event type raw
    extra["raw_payload"] = payload
    extra["event_type_raw"] = event_type_raw

    return ServiceEvent(
        event_type=EventType.WEBHOOK_RECEIVED,
        provider_type=ServiceProviderType.WEBHOOK,
        provider_name=provider_name,
        collection_id=collection_id,
        collection_name=collection_name,
        timestamp=datetime.now(timezone.utc),
        extra=extra,
    )
