"""
Fluent assertion API for voice agent responses.

Usage:
    from rehearse import expect, LLMJudge

    judge = LLMJudge(model="gpt-4o-mini")

    expect(response).to_contain("hello")
    await expect(response).to_satisfy("a friendly greeting", llm=judge)
    expect(response.latency).to_be_less_than(2.0)
"""

import re
from typing import Any, List, TYPE_CHECKING

if TYPE_CHECKING:
    from ..conversation import Response
    from .llm import LLMJudge


class AssertionError(Exception):
    """Assertion failed."""

    pass


class Expectation:
    """Fluent assertion wrapper for responses."""

    def __init__(self, value: Any):
        self.value = value

    def to_contain(self, text: str) -> "Expectation":
        """Assert that response contains the given text (case-insensitive)."""
        if hasattr(self.value, "text"):
            actual = self.value.text
        else:
            actual = str(self.value)

        if text.lower() not in actual.lower():
            raise AssertionError(
                f"Expected '{text}' in response, got: '{actual}'"
            )
        return self

    def to_contain_any(self, options: List[str]) -> "Expectation":
        """Assert that response contains any of the given options."""
        if hasattr(self.value, "text"):
            actual = self.value.text.lower()
        else:
            actual = str(self.value).lower()

        for option in options:
            if option.lower() in actual:
                return self

        raise AssertionError(
            f"Expected any of {options} in response, got: '{actual}'"
        )

    def to_match(self, pattern: str) -> "Expectation":
        """Assert that response matches the given regex pattern."""
        if hasattr(self.value, "text"):
            actual = self.value.text
        else:
            actual = str(self.value)

        if not re.search(pattern, actual, re.IGNORECASE):
            raise AssertionError(
                f"Expected pattern '{pattern}' to match, got: '{actual}'"
            )
        return self

    async def to_satisfy(
        self,
        *intents: str,
        llm: "LLMJudge",
    ) -> "Expectation":
        """
        Assert that response matches the given intent(s) using LLM judgment.

        This is the primary method for semantic evaluation of responses.
        Uses LiteLLM to support multiple providers (OpenAI, Azure, Anthropic, etc.).

        Args:
            *intents: One or more intent descriptions to check.
                      All intents must pass for the assertion to succeed.
            llm: LLMJudge instance for evaluation.

        Returns:
            self for chaining

        Raises:
            AssertionError: If the response doesn't match the intent

        Example:
            judge = LLMJudge(model="gpt-4o-mini")
            await expect(response).to_satisfy("offers a friendly greeting", llm=judge)
            await expect(response).to_satisfy(
                "acknowledges the request",
                "provides clear next steps",
                llm=judge
            )
        """
        if hasattr(self.value, "text"):
            actual = self.value.text
        else:
            actual = str(self.value)

        # Check each intent
        for intent in intents:
            result = await llm.judge(actual, intent)
            if not result.passed:
                raise AssertionError(
                    f"LLM judgment failed.\n"
                    f"  Intent: '{intent}'\n"
                    f"  Actual: '{actual}'\n"
                    f"  Reason: {result.reasoning}"
                )

        return self

    def to_satisfy_sync(
        self,
        *intents: str,
        llm: "LLMJudge",
    ) -> "Expectation":
        """
        Synchronous version of to_satisfy().

        Args:
            *intents: One or more intent descriptions to check.
            llm: LLMJudge instance for evaluation.

        Returns:
            self for chaining

        Example:
            judge = LLMJudge(model="gpt-4o-mini")
            expect(response).to_satisfy_sync("friendly greeting", llm=judge)
        """
        if hasattr(self.value, "text"):
            actual = self.value.text
        else:
            actual = str(self.value)

        # Check each intent
        for intent in intents:
            result = llm.judge_sync(actual, intent)
            if not result.passed:
                raise AssertionError(
                    f"LLM judgment failed.\n"
                    f"  Intent: '{intent}'\n"
                    f"  Actual: '{actual}'\n"
                    f"  Reason: {result.reasoning}"
                )

        return self

    def to_be_less_than(self, limit: float) -> "Expectation":
        """Assert that the value is less than the limit."""
        if hasattr(self.value, "latency"):
            actual = self.value.latency
        else:
            actual = float(self.value)

        if actual >= limit:
            raise AssertionError(
                f"Expected value < {limit}, got: {actual}"
            )
        return self

    def to_be_greater_than(self, limit: float) -> "Expectation":
        """Assert that the value is greater than the limit."""
        if hasattr(self.value, "latency"):
            actual = self.value.latency
        else:
            actual = float(self.value)

        if actual <= limit:
            raise AssertionError(
                f"Expected value > {limit}, got: {actual}"
            )
        return self

    def to_equal(self, expected: Any) -> "Expectation":
        """Assert exact equality."""
        if self.value != expected:
            raise AssertionError(
                f"Expected '{expected}', got: '{self.value}'"
            )
        return self

    def to_be_empty(self) -> "Expectation":
        """Assert that the value is empty."""
        if hasattr(self.value, "text"):
            actual = self.value.text
        elif hasattr(self.value, "__len__"):
            actual = self.value
        else:
            actual = str(self.value)

        if actual:
            raise AssertionError(
                f"Expected empty value, got: '{actual}'"
            )
        return self

    def to_not_be_empty(self) -> "Expectation":
        """Assert that the value is not empty."""
        if hasattr(self.value, "text"):
            actual = self.value.text
        elif hasattr(self.value, "__len__"):
            actual = self.value
        else:
            actual = str(self.value)

        if not actual:
            raise AssertionError("Expected non-empty value")
        return self


class ToolCallExpectation:
    """Assertion wrapper for tool calls."""

    def __init__(self, tool_calls: list):
        self.tool_calls = tool_calls

    def to_contain(self, action: str = None, **kwargs) -> "ToolCallExpectation":
        """
        Assert that tool calls contain the specified action.

        Args:
            action: The action/function name to look for
            **kwargs: Additional parameters to match
        """
        for call in self.tool_calls:
            call_name = call.get("name") or call.get("action")
            if action and call_name != action:
                continue

            # Check additional parameters
            params = call.get("parameters") or call.get("arguments") or {}
            match = True
            for key, value in kwargs.items():
                if params.get(key) != value:
                    match = False
                    break

            if match:
                return self

        raise AssertionError(
            f"Expected tool call '{action}' with {kwargs}, got: {self.tool_calls}"
        )

    def to_be_empty(self) -> "ToolCallExpectation":
        """Assert no tool calls were made."""
        if self.tool_calls:
            raise AssertionError(
                f"Expected no tool calls, got: {self.tool_calls}"
            )
        return self


def expect(value: Any) -> Expectation:
    """
    Create an expectation for fluent assertions.

    Usage:
        expect(response).to_contain("hello")
        expect(response.latency).to_be_less_than(2.0)
        expect(c.tool_calls).to_contain("transfer")
    """
    # Handle tool_calls specially
    if isinstance(value, list) and (
        not value or isinstance(value[0], dict)
    ):
        return ToolCallExpectation(value)

    return Expectation(value)
