"""AgentQueue — top-level pipeline orchestrator for the oedix framework.

Manages the full lifecycle of a user query: context initialisation,
ActionSet queue processing, and the fixed concluding ActionSet(Response).

See design doc §3.1 for the full specification.
"""

from __future__ import annotations

import uuid
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Optional

from oedix.core.action import Action, ActionOutput
from oedix.core.action_set import ActionSet, ActionSetStatus
from oedix.core.context import Context
from oedix.core.events import Event, EventType

if TYPE_CHECKING:
    from oedix.core.config import AgentConfig


class AgentQueue:
    """Top-level pipeline orchestrator (§3.1).

    Processes a user query through a sequence of ActionSets, each
    targeting a sub-goal. After all queued ActionSets complete (or the
    ``max_queue_count`` limit is reached), a fixed ``ActionSet(Response)``
    is executed to produce the final answer.

    Attributes:
        id: Unique identifier (UUID).
        query: Original user query string.
        context: Current context array (mutated during execution).
        history: Completed ActionSet list (append-only).
        queue: Pending ActionSet list (FIFO, mutable by Actions).
        max_queue_count: System limit on ActionSet iterations.
        config: :class:`AgentConfig` instance.
    """

    def __init__(
        self,
        query: str,
        config: AgentConfig,
        *,
        instruction: str = "",
        system_prompt: str = "",
    ) -> None:
        self.id: str = uuid.uuid4().hex[:12]
        self.query: str = query
        self.config = config
        self.max_queue_count: int = config.max_queue_count
        self.created_at: datetime = datetime.now(timezone.utc)

        # Context: initialised with UserQuery + optional Instruction
        self.context: list[Context] = [Context.user_query(query)]
        if system_prompt:
            self.context.insert(0, Context.system(system_prompt))
        if instruction:
            self.context.append(Context.instruction(instruction))

        # ActionSet queues
        self.history: list[ActionSet] = []
        self.queue: list[ActionSet] = []

        # Injected dependencies
        self._llm_client: Any = None
        self._thinking_factory: Optional[Callable[[], Action]] = None
        self._response_factory: Optional[Callable[[], Action]] = None
        self._action_loader: Any = None

    # ------------------------------------------------------------------
    # Context management
    # ------------------------------------------------------------------

    def get_context(self) -> list[Context]:
        """Return a shallow copy of the current context array."""
        return list(self.context)

    def set_context(self, contexts: list[Context]) -> None:
        """Replace the entire context array."""
        self.context = list(contexts)

    def append_context(self, context: Context) -> None:
        """Append a single item to the context array."""
        self.context.append(context)

    # ------------------------------------------------------------------
    # Queue management (exposed via AgentQueueHandle)
    # ------------------------------------------------------------------

    def enqueue(self, action_set: ActionSet) -> None:
        """Append an ActionSet to the end of the queue."""
        self.queue.append(action_set)

    def insert(self, index: int, action_set: ActionSet) -> None:
        """Insert an ActionSet at a specific queue position."""
        self.queue.insert(index, action_set)

    def remove(self, index: int) -> None:
        """Remove a pending ActionSet by index."""
        if 0 <= index < len(self.queue):
            self.queue.pop(index)

    def replace_queue(self, action_sets: list[ActionSet]) -> None:
        """Replace the entire pending queue."""
        self.queue = list(action_sets)

    def remaining_sets(self) -> int:
        """Number of ActionSet slots remaining (excluding Response)."""
        return max(0, self.max_queue_count - len(self.history))

    def current_action_set(self) -> Optional[ActionSet]:
        """Return the ActionSet currently being executed (head of queue)."""
        return self.queue[0] if self.queue else None

    # ------------------------------------------------------------------
    # Pipeline execution (§3.1)
    # ------------------------------------------------------------------

    async def run(self) -> AsyncGenerator[Event, None]:
        """Execute the full AgentQueue pipeline.

        1. Create the initial ActionSet from the query.
        2. Process ActionSets from the queue until empty or limit.
        3. Execute the fixed ActionSet(Response) as conclusion.

        Yields:
            :class:`Event` instances for SSE streaming to the client.
        """
        # --- Step 1: Initial ActionSet ---
        if not self.queue:
            initial = self._make_action_set("의도분석", title="의도분석")
            self._seed_with_thinking(initial)
            self.queue.append(initial)

        consumed = 0

        # --- Step 2: Main loop ---
        while self.queue and consumed < self.max_queue_count:
            action_set = self.queue.pop(0)
            consumed += 1

            # Inject dependencies into the ActionSet
            self._inject_action_set(action_set)

            # Seed thinking for ActionSets with empty queues
            # (e.g., Planner-created ActionSets that need autonomous execution)
            if not action_set.queue:
                self._seed_with_thinking(action_set)

            async for event in action_set.run(self.context):
                yield event

            self.history.append(action_set)

        # --- Step 3: Fixed ActionSet(Response) ---
        async for event in self._run_response_set():
            yield event

        # --- Done ---
        yield Event(
            type=EventType.DONE,
            data={
                "total_action_sets": len(self.history),
                "total_actions": sum(
                    len(s.history) for s in self.history
                ),
                "context_stats": self._compute_context_stats(),
            },
            queue_id=self.id,
        )

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _make_action_set(
        self,
        subject: str,
        title: str = "",
    ) -> ActionSet:
        """Create a new ActionSet with system-level config."""
        return ActionSet(
            subject=subject,
            config=self.config,
            title=title,
            max_action_count=self.config.max_action_count,
        )

    def _seed_with_thinking(self, action_set: ActionSet) -> None:
        """Add an initial thinking Action to an ActionSet's queue."""
        if self._thinking_factory:
            thinking = self._thinking_factory()
            action_set.enqueue(thinking)

    def _inject_action_set(self, action_set: ActionSet) -> None:
        """Wire up an ActionSet with shared dependencies."""
        action_set._agent_queue = self
        action_set._llm_client = self._llm_client
        action_set._thinking_factory = self._thinking_factory

    async def _run_response_set(self) -> AsyncGenerator[Event, None]:
        """Execute the fixed concluding ActionSet(Response).

        This is always run regardless of queue state and does NOT
        count against ``max_queue_count``.
        """
        response_set = self._make_action_set("최종 응답")

        if self._response_factory:
            response_action = self._response_factory()
            response_set.enqueue(response_action)
            # Response set does NOT auto-insert thinking
            response_set._thinking_factory = None

        self._inject_action_set(response_set)
        # Override: no auto-thinking for response set
        response_set._thinking_factory = None

        async for event in response_set.run(self.context):
            yield event

        self.history.append(response_set)

    # ------------------------------------------------------------------
    # Context statistics
    # ------------------------------------------------------------------

    def _compute_context_stats(self) -> dict[str, Any]:
        """Compute detailed context statistics for UI display."""
        by_type: dict[str, dict[str, int]] = {}
        compressed_count = 0
        for c in self.context:
            t = c.type.value
            if t not in by_type:
                by_type[t] = {"count": 0, "size": 0}
            by_type[t]["count"] += 1
            by_type[t]["size"] += c.size()
            if c.is_compressed:
                compressed_count += 1
        return {
            "total_size": sum(c.size() for c in self.context),
            "total_items": len(self.context),
            "max_size": self.config.max_context_size,
            "threshold": int(
                self.config.max_context_size
                * self.config.context_compress_threshold
            ),
            "compressed_count": compressed_count,
            "by_type": by_type,
        }

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

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "query": self.query[:100],
            "max_queue_count": self.max_queue_count,
            "history_count": len(self.history),
            "queue_count": len(self.queue),
            "context_size": sum(c.size() for c in self.context),
            "created_at": self.created_at.isoformat(),
        }

    def __repr__(self) -> str:
        return (
            f"<AgentQueue id={self.id} "
            f"query='{self.query[:30]}' "
            f"history={len(self.history)} "
            f"queue={len(self.queue)}>"
        )
