from abc import ABC, abstractmethod
from enum import Enum
from logging import getLogger

from openhands.sdk.context.view import View
from openhands.sdk.event.condenser import Condensation
from openhands.sdk.llm import LLM
from openhands.sdk.utils.models import (
    DiscriminatedUnionMixin,
)


logger = getLogger(__name__)


class CondenserBase(DiscriminatedUnionMixin, ABC):
    """Abstract condenser interface.

    Condensers take a list of `Event` objects and reduce them into a potentially smaller
    list.

    Agents can use condensers to reduce the amount of events they need to consider when
    deciding which action to take. To use a condenser, agents can call the
    `condensed_history` method on the current `State` being considered and use the
    results instead of the full history.

    If the condenser returns a `Condensation` instead of a `View`, the agent should
    return `Condensation.action` instead of producing its own action. On the next agent
    step the condenser will use that condensation event to produce a new `View`.
    """

    @abstractmethod
    def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation:
        """Condense a sequence of events into a potentially smaller list.

        New condenser strategies should override this method to implement their own
        condensation logic. Call `self.add_metadata` in the implementation to record any
        relevant per-condensation diagnostic information.

        Args:
            view: A view of the history containing all events that should be condensed.
            agent_llm: LLM instance used by the agent. Condensers use this for token
                counting purposes. Defaults to None.

        Returns:
            View | Condensation: A condensed view of the events or an event indicating
            the history has been condensed.
        """

    def handles_condensation_requests(self) -> bool:
        """Whether this condenser handles explicit condensation requests.

        If this returns True, the agent will trigger the condenser whenever a
        CondensationRequest event is added to the history. If False, the condenser will
        only be triggered when the agent's own logic decides to do so (e.g. context
        window exceeded).

        Returns:
            bool: True if the condenser handles explicit condensation requests, False
            otherwise.
        """
        return False


class PipelinableCondenserBase(CondenserBase):
    """Abstract condenser interface which may be pipelined. (Since a pipeline
    condenser should not nest another pipeline condenser)"""


class NoCondensationAvailableException(Exception):
    """Raised when a condenser is asked to provide a condensation but none is available.

    This can happen if the condenser's `should_condense` method returns True, but due to
    API constraints no condensation can be generated.

    When this exception is raised from a rolling condenser's `get_condensation` method,
    the agent will fall back to using the uncondensed view for the next agent step.
    """


class CondensationRequirement(Enum):
    """The type of condensation required by a rolling condenser."""

    HARD = "hard"
    """Indicates that a condensation is required right now, and the agent cannot proceed
    without it.
    """

    SOFT = "soft"
    """Indicates that a condensation is desired but not strictly required."""


class RollingCondenser(PipelinableCondenserBase, ABC):
    """Base class for a specialized condenser strategy that applies condensation to a
    rolling history.

    The rolling history is generated by `View.from_events`, which analyzes all events in
    the history and produces a `View` object representing what will be sent to the LLM.

    If `condensation_requirement` says so, the condenser is then responsible for
    generating a `Condensation` object from the `View` object. This will be added to the
    event history which should -- when given to `get_view` -- produce the condensed
    `View` to be passed to the LLM.
    """

    @abstractmethod
    def condensation_requirement(
        self, view: View, agent_llm: LLM | None = None
    ) -> CondensationRequirement | None:
        """Determine how a view should be condensed.

        Args:
            view: The current view of the conversation history.
            agent_llm: LLM instance used by the agent. Condensers use this for token
                counting purposes. Defaults to None.

        Returns:
            CondensationRequirement | None: The type of condensation required, or None
            if no condensation is needed.
        """

    @abstractmethod
    def get_condensation(
        self, view: View, agent_llm: LLM | None = None
    ) -> Condensation:
        """Get the condensation from a view."""

    def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation:
        # If we trigger the condenser-specific condensation threshold, compute and
        # return the condensation.
        request = self.condensation_requirement(view, agent_llm=agent_llm)
        if request is not None:
            try:
                return self.get_condensation(view, agent_llm=agent_llm)

            except NoCondensationAvailableException as e:
                logger.debug(f"No condensation available: {e}")

                if request == CondensationRequirement.SOFT:
                    # For soft requests, we can just return the uncondensed view. This
                    # request will _eventually_ be handled, but it's not critical that
                    # we do so immediately.
                    return view

                # Otherwise re-raise the exception.
                else:
                    raise e

        # Otherwise we're safe to just return the view.
        else:
            return view
