"""YAML-driven mapping adapter for community game integrations.

Allows community contributors to define game adapters as YAML files
without writing Python code. A YAML adapter file specifies:
  - Adapter metadata (name, game, protocol)
  - A list of mappings from JSON paths to standard event types
  - Optional auth configuration

The MappingAdapter class is a concrete GameAdapter whose behavior is
entirely driven by the parsed YAML definition.
"""

import time
from pathlib import Path
from typing import Any

import yaml

from wled_controller.core.game_integration.base_adapter import GameAdapter
from wled_controller.core.game_integration.events import (
    GameEvent,
    is_known_event_type,
)
from wled_controller.utils import get_logger

logger = get_logger(__name__)

# Valid trigger modes for mapping entries
VALID_TRIGGER_MODES = frozenset({"on_change", "on_increase", "on_decrease", "on_value"})


def _resolve_json_path(data: dict[str, Any], path: str) -> Any | None:
    """Resolve a dot-separated JSON path against a nested dict.

    Supports simple dot notation: "player.state.health"
    Returns None if the path cannot be resolved.
    """
    parts = path.split(".")
    current: Any = data
    for part in parts:
        if isinstance(current, dict) and part in current:
            current = current[part]
        else:
            return None
    return current


def validate_adapter_yaml(data: dict[str, Any]) -> list[str]:
    """Validate a parsed YAML adapter definition.

    Args:
        data: Parsed YAML dict.

    Returns:
        List of validation error strings (empty = valid).
    """
    errors: list[str] = []

    # Required top-level fields
    if not isinstance(data.get("name"), str) or not data["name"].strip():
        errors.append("Missing or empty 'name' field")
    if not isinstance(data.get("game"), str) or not data["game"].strip():
        errors.append("Missing or empty 'game' field")
    if data.get("protocol") not in ("webhook", "poll"):
        errors.append("'protocol' must be 'webhook' or 'poll'")

    # Mappings
    mappings = data.get("mappings")
    if not isinstance(mappings, list) or len(mappings) == 0:
        errors.append("'mappings' must be a non-empty list")
        return errors

    for i, mapping in enumerate(mappings):
        prefix = f"mappings[{i}]"
        if not isinstance(mapping, dict):
            errors.append(f"{prefix}: must be a dict")
            continue

        if not isinstance(mapping.get("source_path"), str) or not mapping["source_path"].strip():
            errors.append(f"{prefix}: missing or empty 'source_path'")

        event = mapping.get("event")
        if not isinstance(event, str) or not event.strip():
            errors.append(f"{prefix}: missing or empty 'event'")
        elif not is_known_event_type(event):
            errors.append(f"{prefix}: unknown event type '{event}'")

        trigger = mapping.get("trigger", "on_change")
        if trigger not in VALID_TRIGGER_MODES:
            errors.append(
                f"{prefix}: invalid trigger mode '{trigger}', "
                f"must be one of {sorted(VALID_TRIGGER_MODES)}"
            )

        # min/max are optional but must be numeric if present
        for field in ("min", "max"):
            if field in mapping:
                if not isinstance(mapping[field], (int, float)):
                    errors.append(f"{prefix}: '{field}' must be numeric")

    return errors


class MappingAdapter(GameAdapter):
    """A game adapter whose behavior is defined by a YAML mapping file.

    Unlike built-in adapters (classmethods on a class), MappingAdapter
    instances carry per-adapter state from their YAML definition.
    The classmethod interface is implemented by delegating to instance
    data stored on dynamically created subclasses.
    """

    ADAPTER_TYPE = "mapping"
    DISPLAY_NAME = "YAML Mapping Adapter"
    GAME_NAME = "Generic"
    SUPPORTED_EVENTS: list[str] = []

    def __init__(self, definition: dict[str, Any]) -> None:
        self._definition = definition
        self._name: str = definition["name"]
        self._game: str = definition["game"]
        self._protocol: str = definition["protocol"]
        self._mappings: list[dict[str, Any]] = definition["mappings"]
        self._auth: dict[str, Any] = definition.get("auth", {})
        self._supported_events = list({m["event"] for m in self._mappings})

    @property
    def name(self) -> str:
        return self._name

    @property
    def game(self) -> str:
        return self._game

    @property
    def protocol(self) -> str:
        return self._protocol

    @property
    def supported_events(self) -> list[str]:
        return list(self._supported_events)

    def parse_payload(  # type: ignore[override]
        self,
        payload: dict[str, Any],
        adapter_config: dict[str, Any],
        prev_state: dict[str, Any],
    ) -> tuple[list[GameEvent], dict[str, Any]]:
        """Parse a JSON payload using the YAML mapping definitions.

        For continuous events, values are normalized to 0.0-1.0 using
        the mapping's min/max range. For triggers, diff-based detection
        compares against prev_state.
        """
        events: list[GameEvent] = []
        new_state = dict(prev_state)
        adapter_id = adapter_config.get("adapter_id", self._name)
        now = time.monotonic()

        for mapping in self._mappings:
            source_path: str = mapping["source_path"]
            event_type: str = mapping["event"]
            trigger_mode: str = mapping.get("trigger", "on_change")
            range_min: float = float(mapping.get("min", 0))
            range_max: float = float(mapping.get("max", 100))

            raw_value = _resolve_json_path(payload, source_path)
            if raw_value is None:
                continue

            try:
                numeric_value = float(raw_value)
            except (TypeError, ValueError):
                # Non-numeric: treat presence as a trigger with value 1.0
                events.append(
                    GameEvent(
                        adapter_id=adapter_id,
                        event_type=event_type,
                        value=1.0,
                        raw_data={"source_path": source_path, "raw": raw_value},
                        timestamp=now,
                    )
                )
                continue

            prev_value = prev_state.get(source_path)
            new_state[source_path] = numeric_value

            # Determine whether to emit based on trigger mode
            should_emit = False
            if trigger_mode == "on_change":
                should_emit = prev_value is None or numeric_value != prev_value
            elif trigger_mode == "on_increase":
                should_emit = prev_value is not None and numeric_value > prev_value
            elif trigger_mode == "on_decrease":
                should_emit = prev_value is not None and numeric_value < prev_value
            elif trigger_mode == "on_value":
                # Always emit when value is present
                should_emit = True

            if not should_emit:
                continue

            # Normalize to 0.0-1.0
            range_span = range_max - range_min
            if range_span > 0:
                normalized = max(0.0, min(1.0, (numeric_value - range_min) / range_span))
            else:
                normalized = 1.0

            events.append(
                GameEvent(
                    adapter_id=adapter_id,
                    event_type=event_type,
                    value=normalized,
                    raw_data={"source_path": source_path, "raw": numeric_value},
                    timestamp=now,
                )
            )

        return events, new_state

    def validate_auth(  # type: ignore[override]
        self,
        headers: dict[str, str],
        payload: dict[str, Any],
        adapter_config: dict[str, Any],
    ) -> bool:
        """Validate authentication using the YAML auth config.

        Supports 'header' auth type: checks that a specified header
        matches an expected value from adapter_config.
        """
        auth_type = self._auth.get("type")
        if not auth_type:
            # No auth configured — accept all
            return True

        if auth_type == "header":
            header_name = self._auth.get("header", "")
            expected_key = "auth_token"
            expected_value = adapter_config.get(expected_key, "")
            actual_value = headers.get(header_name, "")
            return bool(expected_value and actual_value == expected_value)

        logger.warning(f"Unknown auth type '{auth_type}' in mapping adapter '{self._name}'")
        return False

    def get_config_schema(self) -> dict[str, Any]:  # type: ignore[override]
        """Return config schema based on YAML auth requirements."""
        properties: dict[str, Any] = {}
        if self._auth.get("type") == "header":
            properties["auth_token"] = {
                "type": "string",
                "title": "Auth Token",
                "description": f"Value for the {self._auth.get('header', '')} header",
            }
        return {"type": "object", "properties": properties}

    def get_setup_instructions(self) -> str:  # type: ignore[override]
        """Return setup instructions from the YAML definition."""
        return self._definition.get(
            "setup_instructions",
            f"Configure {self._game} to send data to the webhook endpoint.",
        )


def load_adapter_from_yaml(path: str | Path) -> MappingAdapter:
    """Load a MappingAdapter from a YAML file.

    Args:
        path: Path to the YAML adapter definition file.

    Returns:
        A configured MappingAdapter instance.

    Raises:
        FileNotFoundError: If the file does not exist.
        ValueError: If the YAML is invalid.
    """
    path = Path(path)
    if not path.exists():
        raise FileNotFoundError(f"Adapter YAML file not found: {path}")

    with open(path, encoding="utf-8") as f:
        data = yaml.safe_load(f)

    if not isinstance(data, dict):
        raise ValueError(f"Adapter YAML must be a dict, got {type(data).__name__}")

    errors = validate_adapter_yaml(data)
    if errors:
        raise ValueError(
            f"Invalid adapter YAML '{path.name}':\n" + "\n".join(f"  - {e}" for e in errors)
        )

    adapter = MappingAdapter(data)
    logger.info(f"Loaded mapping adapter '{adapter.name}' for {adapter.game} from {path.name}")
    return adapter
