"""
Pipeline core - Unified Stage abstraction and PipelineContext.

This module provides the foundation for a clean, dependency-managed pipeline:
- Stage: Base class for all pipeline components (sources, effects, displays, cameras)
- PipelineContext: Dependency injection context for runtime data exchange
- Capability system: Explicit capability declarations with duck-typing support
- DataType: PureData-style inlet/outlet typing for validation
"""

from abc import ABC, abstractmethod
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from engine.pipeline.params import PipelineParams


class DataType(Enum):
    """PureData-style data types for inlet/outlet validation.

    Each type represents a specific data format that flows through the pipeline.
    This enables compile-time-like validation of connections.

    Examples:
        SOURCE_ITEMS: List[SourceItem] - raw items from sources
        ITEM_TUPLES: List[tuple] - (title, source, timestamp) tuples
        TEXT_BUFFER: List[str] - rendered ANSI buffer for display
        RAW_TEXT: str - raw text strings
        PIL_IMAGE: PIL Image object
    """

    SOURCE_ITEMS = auto()  # List[SourceItem] - from DataSource
    ITEM_TUPLES = auto()  # List[tuple] - (title, source, ts)
    TEXT_BUFFER = auto()  # List[str] - ANSI buffer
    RAW_TEXT = auto()  # str - raw text
    PIL_IMAGE = auto()  # PIL Image object
    ANY = auto()  # Accepts any type
    NONE = auto()  # No data (terminator)


@dataclass
class StageConfig:
    """Configuration for a single stage."""

    name: str
    category: str
    enabled: bool = True
    optional: bool = False
    params: dict[str, Any] = field(default_factory=dict)


class Stage(ABC):
    """Abstract base class for all pipeline stages.

    A Stage is a single component in the rendering pipeline. Stages can be:
    - Sources: Data providers (headlines, poetry, pipeline viz)
    - Effects: Post-processors (noise, fade, glitch, hud)
    - Displays: Output backends (terminal, pygame, websocket)
    - Cameras: Viewport controllers (vertical, horizontal, omni)
    - Overlays: UI elements that compose on top (HUD)

    Stages declare:
    - capabilities: What they provide to other stages
    - dependencies: What they need from other stages
    - stage_type: Category of stage (source, effect, overlay, display)
    - render_order: Execution order within category
    - is_overlay: If True, output is composited on top, not passed downstream

    Duck-typing is supported: any class with the required methods can act as a Stage.
    """

    name: str
    category: str  # "source", "effect", "overlay", "display", "camera"
    optional: bool = False  # If True, pipeline continues even if stage fails

    @property
    def stage_type(self) -> str:
        """Category of stage for ordering.

        Valid values: "source", "effect", "overlay", "display", "camera"
        Defaults to category for backwards compatibility.
        """
        return self.category

    @property
    def render_order(self) -> int:
        """Execution order within stage_type group.

        Higher values execute later. Useful for ordering overlays
        or effects that need specific execution order.
        """
        return 0

    @property
    def is_overlay(self) -> bool:
        """If True, this stage's output is composited on top of the buffer.

        Overlay stages don't pass their output to the next stage.
        Instead, their output is layered on top of the final buffer.
        Use this for HUD, status displays, and similar UI elements.
        """
        return False

    @property
    def inlet_types(self) -> set[DataType]:
        """Return set of data types this stage accepts.

        PureData-style inlet typing. If the connected upstream stage's
        outlet_type is not in this set, the pipeline will raise an error.

        Examples:
            - Source stages: {DataType.NONE} (no input needed)
            - Transform stages: {DataType.ITEM_TUPLES, DataType.TEXT_BUFFER}
            - Display stages: {DataType.TEXT_BUFFER}
        """
        return {DataType.ANY}

    @property
    def outlet_types(self) -> set[DataType]:
        """Return set of data types this stage produces.

        PureData-style outlet typing. Downstream stages must accept
        this type in their inlet_types.

        Examples:
            - Source stages: {DataType.SOURCE_ITEMS}
            - Transform stages: {DataType.TEXT_BUFFER}
            - Display stages: {DataType.NONE} (consumes data)
        """
        return {DataType.ANY}

    @property
    def capabilities(self) -> set[str]:
        """Return set of capabilities this stage provides.

        Examples:
            - "source.headlines"
            - "effect.noise"
            - "display.output"
            - "camera"
        """
        return {f"{self.category}.{self.name}"}

    @property
    def dependencies(self) -> set[str]:
        """Return set of capability names this stage needs.

        Examples:
            - {"display.output"}
            - {"source.headlines"}
            - {"camera"}
        """
        return set()

    @property
    def stage_dependencies(self) -> set[str]:
        """Return set of stage names this stage must connect to directly.

        This allows explicit stage-to-stage dependencies, useful for enforcing
        pipeline structure when capability matching alone is insufficient.

        Examples:
            - {"viewport_filter"}  # Must connect to viewport_filter stage
            - {"camera_update"}    # Must connect to camera_update stage

        NOTE: These are stage names (as added to pipeline), not capabilities.
        """
        return set()

    def init(self, ctx: "PipelineContext") -> bool:
        """Initialize stage with pipeline context.

        Args:
            ctx: PipelineContext for accessing services

        Returns:
            True if initialization succeeded, False otherwise
        """
        return True

    @abstractmethod
    def process(self, data: Any, ctx: "PipelineContext") -> Any:
        """Process input data and return output.

        Args:
            data: Input data from previous stage (or initial data for first stage)
            ctx: PipelineContext for accessing services and state

        Returns:
            Processed data for next stage
        """
        ...

    def cleanup(self) -> None:  # noqa: B027
        """Clean up resources when pipeline shuts down."""
        pass

    def get_config(self) -> StageConfig:
        """Return current configuration of this stage."""
        return StageConfig(
            name=self.name,
            category=self.category,
            optional=self.optional,
        )

    def set_enabled(self, enabled: bool) -> None:
        """Enable or disable this stage."""
        self._enabled = enabled  # type: ignore[attr-defined]

    def is_enabled(self) -> bool:
        """Check if stage is enabled."""
        return getattr(self, "_enabled", True)


@dataclass
class StageResult:
    """Result of stage processing, including success/failure info."""

    success: bool
    data: Any
    error: str | None = None
    stage_name: str = ""


class PipelineContext:
    """Dependency injection context passed through the pipeline.

    Provides:
    - services: Named services (display, config, event_bus, etc.)
    - state: Runtime state shared between stages
    - params: PipelineParams for animation-driven config

    Services can be injected at construction time or lazily resolved.
    """

    def __init__(
        self,
        services: dict[str, Any] | None = None,
        initial_state: dict[str, Any] | None = None,
    ):
        self.services: dict[str, Any] = services or {}
        self.state: dict[str, Any] = initial_state or {}
        self._params: PipelineParams | None = None

        # Lazy resolvers for common services
        self._lazy_resolvers: dict[str, Callable[[], Any]] = {
            "config": self._resolve_config,
            "event_bus": self._resolve_event_bus,
        }

    def _resolve_config(self) -> Any:
        from engine.config import get_config

        return get_config()

    def _resolve_event_bus(self) -> Any:
        from engine.eventbus import get_event_bus

        return get_event_bus()

    def get(self, key: str, default: Any = None) -> Any:
        """Get a service or state value by key.

        First checks services, then state, then lazy resolution.
        """
        if key in self.services:
            return self.services[key]
        if key in self.state:
            return self.state[key]
        if key in self._lazy_resolvers:
            try:
                return self._lazy_resolvers[key]()
            except Exception:
                return default
        return default

    def set(self, key: str, value: Any) -> None:
        """Set a service or state value."""
        self.services[key] = value

    def set_state(self, key: str, value: Any) -> None:
        """Set a runtime state value."""
        self.state[key] = value

    def get_state(self, key: str, default: Any = None) -> Any:
        """Get a runtime state value."""
        return self.state.get(key, default)

    @property
    def params(self) -> "PipelineParams | None":
        """Get current pipeline params (for animation)."""
        return self._params

    @params.setter
    def params(self, value: "PipelineParams") -> None:
        """Set pipeline params (from animation controller)."""
        self._params = value

    def has_capability(self, capability: str) -> bool:
        """Check if a capability is available."""
        return capability in self.services or capability in self._lazy_resolvers


class StageError(Exception):
    """Raised when a stage fails to process."""

    def __init__(self, stage_name: str, message: str, is_optional: bool = False):
        self.stage_name = stage_name
        self.message = message
        self.is_optional = is_optional
        super().__init__(f"Stage '{stage_name}' failed: {message}")


def create_stage_error(
    stage_name: str, error: Exception, is_optional: bool = False
) -> StageError:
    """Helper to create a StageError from an exception."""
    return StageError(stage_name, str(error), is_optional)
