"""
LangChain adapter for Boring-OS.

Provides LangChain-compatible tools that wrap Boring-OS tool calls.

Usage:
    from boring_os import BoringOS
    from boring_os.adapters.langchain import get_langchain_tools
    
    client = BoringOS(api_key="...")
    tools = get_langchain_tools(client)
    
    # Use with LangChain agent
    from langchain.agents import create_tool_calling_agent
    agent = create_tool_calling_agent(llm, tools, prompt)

Requires: pip install boring-os[langchain]
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

# Lazy import to avoid requiring langchain as a hard dependency
try:
    from langchain_core.tools import BaseTool, ToolException
    from langchain_core.callbacks import CallbackManagerForToolRun
    
    LANGCHAIN_AVAILABLE = True
except ImportError:
    LANGCHAIN_AVAILABLE = False
    BaseTool = object  # type: ignore
    ToolException = Exception  # type: ignore
    CallbackManagerForToolRun = Any  # type: ignore

if TYPE_CHECKING:
    from boring_os.client import BoringOS
    from boring_os.models import Tool, ToolInfo


def _check_langchain_available() -> None:
    """Raise ImportError if langchain is not installed."""
    if not LANGCHAIN_AVAILABLE:
        raise ImportError(
            "LangChain is required for this adapter. "
            "Install with: pip install boring-os[langchain]"
        )


class BoringOSTool(BaseTool):
    """
    LangChain tool wrapper for a Boring-OS tool.
    
    Wraps a Boring-OS tool as a LangChain BaseTool for use with
    LangChain agents and chains.
    """
    
    # Tool metadata (set by LangChain)
    name: str = ""
    description: str = ""
    
    # Boring-OS client and tool info
    client: Any = None  # BoringOS client
    tool_name: str = ""
    input_schema: dict[str, Any] = {}
    
    # Configuration
    handle_tool_error: bool = True
    
    class Config:
        """Pydantic config."""
        arbitrary_types_allowed = True
    
    @property
    def args_schema(self) -> dict[str, Any]:
        """Return JSON schema for tool arguments."""
        return self.input_schema
    
    def _run(
        self,
        run_manager: CallbackManagerForToolRun | None = None,
        **kwargs: Any,
    ) -> str:
        """
        Execute the tool synchronously.
        
        Args:
            run_manager: LangChain callback manager
            **kwargs: Tool parameters
        
        Returns:
            JSON string of the result
        """
        import json
        
        try:
            result = self.client.tools.call(self.tool_name, **kwargs)
            return json.dumps(result)
        except Exception as e:
            if self.handle_tool_error:
                raise ToolException(str(e)) from e
            raise
    
    async def _arun(
        self,
        run_manager: CallbackManagerForToolRun | None = None,
        **kwargs: Any,
    ) -> str:
        """
        Execute the tool asynchronously.
        
        Args:
            run_manager: LangChain callback manager
            **kwargs: Tool parameters
        
        Returns:
            JSON string of the result
        """
        import json
        
        try:
            result = await self.client.tools.call_async(self.tool_name, **kwargs)
            return json.dumps(result)
        except Exception as e:
            if self.handle_tool_error:
                raise ToolException(str(e)) from e
            raise


def create_langchain_tool(
    client: "BoringOS",
    tool: "Tool | ToolInfo",
) -> BoringOSTool:
    """
    Create a LangChain tool from a Boring-OS tool definition.
    
    Args:
        client: Boring-OS client instance
        tool: Tool definition or info from Boring-OS
    
    Returns:
        LangChain-compatible tool
    """
    _check_langchain_available()
    
    # Build description from tool info
    description = tool.description or f"Execute the {tool.name} tool"
    
    # Add instructions if available (full Tool object)
    if hasattr(tool, "instructions") and tool.instructions:
        if tool.instructions.summary:
            description = tool.instructions.summary
        if tool.instructions.when_to_use:
            description += f"\n\nWhen to use: {tool.instructions.when_to_use}"
        if tool.instructions.when_not_to_use:
            description += f"\n\nWhen NOT to use: {tool.instructions.when_not_to_use}"
    
    # Get input schema if available
    input_schema: dict[str, Any] = {}
    if hasattr(tool, "schema_") and tool.schema_:
        input_schema = tool.schema_.input
    
    return BoringOSTool(
        name=tool.name,
        description=description,
        client=client,
        tool_name=tool.name,
        input_schema=input_schema,
    )


def get_langchain_tools(
    client: "BoringOS",
    tool_names: list[str] | None = None,
    include_inactive: bool = False,
) -> list[BoringOSTool]:
    """
    Get LangChain tools from Boring-OS.
    
    Fetches tools from Boring-OS and wraps them as LangChain tools.
    
    Args:
        client: Boring-OS client instance
        tool_names: Optional list of specific tool names to include.
                   If None, includes all active tools.
        include_inactive: If True, include deprecated tools
    
    Returns:
        List of LangChain-compatible tools
    
    Example:
        from boring_os import BoringOS
        from boring_os.adapters.langchain import get_langchain_tools
        
        client = BoringOS(api_key="...")
        
        # Get all active tools
        tools = get_langchain_tools(client)
        
        # Get specific tools
        tools = get_langchain_tools(client, tool_names=["get_customer_profile"])
        
        # Use with agent
        from langchain.agents import create_tool_calling_agent
        agent = create_tool_calling_agent(llm, tools, prompt)
    """
    _check_langchain_available()
    
    tools: list[BoringOSTool] = []
    
    if tool_names:
        # Fetch specific tools with full details
        for name in tool_names:
            try:
                tool_def = client.tools.get(name)
                tools.append(create_langchain_tool(client, tool_def))
            except Exception:
                # Skip tools that can't be fetched
                pass
    else:
        # Fetch all active tools
        from boring_os.models import ToolStatus
        
        status = None if include_inactive else ToolStatus.ACTIVE
        response = client.tools.list(status=status, limit=1000)
        
        # For each tool, fetch full details to get schema
        for tool_info in response.tools:
            try:
                tool_def = client.tools.get(tool_info.name)
                tools.append(create_langchain_tool(client, tool_def))
            except Exception:
                # Fall back to basic info if full fetch fails
                tools.append(create_langchain_tool(client, tool_info))
    
    return tools


async def get_langchain_tools_async(
    client: "BoringOS",
    tool_names: list[str] | None = None,
) -> list[BoringOSTool]:
    """
    Async version of get_langchain_tools.
    
    Note: Currently uses sync client internally as tool listing
    is a fast operation. Full async support coming in future version.
    """
    return get_langchain_tools(client, tool_names)
