"""Action abstract base class and ActionOutput for the oedix agent framework.

Defines the Action ABC that all execution units (thinking, response, tool,
planner, organizer, etc.) must inherit from.  See design doc §3.3.
"""

from __future__ import annotations

import uuid
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import TYPE_CHECKING, Any, AsyncGenerator, Optional

from oedix.core.context import Context
from oedix.core.events import Event, EventType

if TYPE_CHECKING:
    from oedix.core.config import AgentConfig


# ------------------------------------------------------------------
# ActionStatus — 4-state lifecycle (simplified from legacy 7-state)
# ------------------------------------------------------------------


class ActionStatus(str, Enum):
    """Action execution lifecycle states."""

    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"


# ------------------------------------------------------------------
# ActionOutput — standardised execution result
# ------------------------------------------------------------------


@dataclass
class ActionOutput:
    """Standardised output from an Action execution.

    Attributes:
        result: Arbitrary result data from the action.
        context_entries: New Context items to append to the AgentQueue
            context after this action completes.
        summary: Human-readable summary of what this action did
            (used for history display and ActionSet history tracking).
    """

    result: Any = None
    context_entries: list[Context] = field(default_factory=list)
    summary: str = ""

    def to_dict(self) -> dict:
        """Serialize to a plain dict for client consumption."""
        result = self.result
        if not isinstance(result, (dict, list, str, int, float, bool, type(None))):
            result = str(result)
        return {
            "result": result,
            "context_entries": [c.to_dict() for c in self.context_entries],
            "summary": self.summary,
        }


# ------------------------------------------------------------------
# Action ABC
# ------------------------------------------------------------------


class Action(ABC):
    """Abstract base class for all execution units in the oedix pipeline.

    **Subclass contract**:

    1. Define ``type`` class variable (e.g. ``type = "thinking"``).
    2. Implement ``execute(context)`` — async generator yielding Events.
    3. Implement ``describe()`` — one-line capability description.

    **Provided by base class**:

    - ``run()``  — lifecycle wrapper (status transitions, error handling).
    - ``emit()`` — create :class:`Event` bound to this action's IDs.
    - ``call_llm()`` — stream LLM responses via the injected client.
    - ``get_action_set()`` — :class:`ActionSetHandle` proxy.
    - ``get_agent_queue()`` — :class:`AgentQueueHandle` proxy.
    """

    type: str = ""  # Override in every subclass

    def __init__(
        self,
        config: Optional[AgentConfig] = None,
        **kwargs: Any,
    ) -> None:
        self.id: str = uuid.uuid4().hex[:12]
        self.status: ActionStatus = ActionStatus.PENDING
        self.input_context: list[Context] = []
        self.output: Optional[ActionOutput] = None
        self.created_at: datetime = datetime.now(timezone.utc)
        self.completed_at: Optional[datetime] = None
        self.config = config
        self.params: dict[str, Any] = kwargs

        # Injected by ActionSet before execution
        self._action_set: Any = None
        self._agent_queue: Any = None
        self._llm_client: Any = None
        self._action_loader: Any = None

    # ------------------------------------------------------------------
    # Abstract methods (subclass must implement)
    # ------------------------------------------------------------------

    @abstractmethod
    async def execute(
        self, context: list[Context]
    ) -> AsyncGenerator[Event, None]:
        """Core execution logic.

        Receives the current context snapshot and yields SSE Events.
        Must set ``self.output`` (ActionOutput) before returning.
        """
        ...  # pragma: no cover
        # Make the method a generator so subclasses know the protocol.
        yield  # type: ignore[misc]

    @abstractmethod
    def describe(self) -> str:
        """Return a human-readable description of this Action's capabilities.

        Used by the thinking action to discover and select available
        actions at runtime.
        """
        ...  # pragma: no cover

    def usage(self) -> str:
        """Return detailed usage guide for this action.

        Describes when to use this action, what parameters it accepts,
        constraints, and examples.  The thinking action includes this
        in the LLM prompt so the model can make informed decisions.

        Override in subclasses to provide action-specific guidance.
        The default implementation returns an empty string, which
        means only ``describe()`` will be shown.
        """
        return ""

    # ------------------------------------------------------------------
    # Execution wrapper
    # ------------------------------------------------------------------

    async def run(
        self, context: list[Context]
    ) -> AsyncGenerator[Event, None]:
        """Lifecycle wrapper around :meth:`execute`.

        Manages ``status`` transitions and catches unhandled exceptions.
        Subclasses should **not** override this — implement
        :meth:`execute` instead.
        """
        self.status = ActionStatus.RUNNING
        self.input_context = list(context)
        try:
            async for event in self.execute(context):
                yield event
            self.status = ActionStatus.COMPLETED
            self.completed_at = datetime.now(timezone.utc)
        except Exception as e:
            self.status = ActionStatus.FAILED
            self.completed_at = datetime.now(timezone.utc)
            self.output = ActionOutput(
                result={"error": str(e)},
                summary=f"Action '{self.type}' failed: {e}",
            )
            yield self.emit(EventType.ERROR, {"message": str(e)})

    # ------------------------------------------------------------------
    # Base helper methods
    # ------------------------------------------------------------------

    def emit(
        self,
        event_type: EventType,
        data: Optional[dict[str, Any]] = None,
    ) -> Event:
        """Create an :class:`Event` bound to this action's hierarchy IDs."""
        return Event(
            type=event_type,
            data=data or {},
            queue_id=(
                self._agent_queue.id if self._agent_queue else None
            ),
            set_id=self._action_set.id if self._action_set else None,
            action_id=self.id,
        )

    async def call_llm(
        self,
        messages: list[dict[str, str]],
        *,
        provider: Optional[str] = None,
        model: Optional[str] = None,
        **kwargs: Any,
    ) -> AsyncGenerator[str, None]:
        """Stream LLM responses via the injected client.

        Args:
            messages: Chat-format message list.
            provider: Override the default LLM provider.
            model: Override the default LLM model.
            **kwargs: Extra parameters forwarded to the LLM client.

        Yields:
            Text chunks from the LLM stream.
        """
        if self._llm_client is None:
            raise RuntimeError("LLM client not injected into Action")
        async for chunk in self._llm_client.stream(
            messages=messages,
            provider=provider,
            model=model,
            **kwargs,
        ):
            yield chunk

    def get_action_set(self):
        """Return an :class:`ActionSetHandle` for the owning ActionSet.

        Raises:
            RuntimeError: If the action has not been attached to an
                ActionSet yet.
        """
        if self._action_set is None:
            raise RuntimeError("Action not attached to an ActionSet")
        from oedix.core.handles import ActionSetHandle

        return ActionSetHandle(self._action_set)

    def get_agent_queue(self):
        """Return an :class:`AgentQueueHandle` for the owning AgentQueue.

        Raises:
            RuntimeError: If the action has not been attached to an
                AgentQueue yet.
        """
        if self._agent_queue is None:
            raise RuntimeError("Action not attached to an AgentQueue")
        from oedix.core.handles import AgentQueueHandle

        return AgentQueueHandle(self._agent_queue)

    # ------------------------------------------------------------------
    # Serialisation / display
    # ------------------------------------------------------------------

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "type": self.type,
            "status": self.status.value,
            "params": self.params,
            "created_at": self.created_at.isoformat(),
            "completed_at": (
                self.completed_at.isoformat() if self.completed_at else None
            ),
            "output": self.output.to_dict() if self.output else None,
        }

    def __repr__(self) -> str:
        return (
            f"<{self.__class__.__name__} "
            f"id={self.id} type={self.type} "
            f"status={self.status.value}>"
        )
