"""Base tool interface and abstract classes"""

import abc
import asyncio
import inspect
import json
from typing import Dict, Any, Optional, Type, List, TYPE_CHECKING, Callable, Awaitable
from dataclasses import dataclass
from enum import Enum
from uuid import uuid4

from .precheck import ToolPrecheckResult, STATUS_ALLOW, STATUS_DENY, STATUS_NEEDS_APPROVAL
from ..events import EventPayload, ev_approval_request, ev_choice_request

if TYPE_CHECKING:
    from ..model import UserContent


class ToolType(Enum):
    """Tool execution types"""
    SYNC = "sync"
    ASYNC = "async"
    STREAM = "stream"


@dataclass
class ToolResult:
    """Standardized tool execution result returned from BaseTool.run"""
    status: str
    content: Optional[Any] = None
    error: Optional[str] = None
    metadata: Optional[Dict[str, Any]] = None
    needs_user_input: bool = False

    @property
    def success(self) -> bool:
        return self.status == "success"


@dataclass
class ToolStreamEvent:
    """Tool streaming event"""
    type: str  # stdout, stderr, progress, error, done
    data: Any
    metadata: Optional[Dict[str, Any]] = None


class ToolExecutionError(Exception):
    """Custom exception that carries metadata for tool failures"""

    def __init__(
        self,
        message: str,
        *,
        metadata: Optional[Dict[str, Any]] = None,
        needs_user_input: bool = False,
        content: Optional[Any] = None,
    ) -> None:
        super().__init__(message)
        self.metadata = metadata
        self.needs_user_input = needs_user_input
        self.content = content


class BaseTool(abc.ABC):
    """Abstract base class for all tools"""
    
    def __init_subclass__(cls, **kwargs):
        """Subclass hook reserved for future extension."""
        super().__init_subclass__(**kwargs)
    
    @property
    @abc.abstractmethod
    def name(self) -> str:
        """Tool name (must be unique)"""
        pass
    
    @property
    @abc.abstractmethod
    def description(self) -> str:
        """Tool description for LLM"""
        pass
    
    @property
    @abc.abstractmethod
    def schema(self) -> Dict[str, Any]:
        """OpenAI function calling schema"""
        pass
    
    @property
    def tool_type(self) -> ToolType:
        """Tool execution type"""
        return ToolType.SYNC
    
    @property
    def requires_permission(self) -> bool:
        """Whether this tool requires user permission to execute"""
        return False
    
    @property
    def is_destructive(self) -> bool:
        """Whether this tool can modify system state"""
        return False
    
    # Standard interface methods that all tools should implement
    @classmethod
    @abc.abstractmethod
    def get_schema(cls) -> Dict[str, Any]:
        """Get OpenAI function calling schema"""
        pass
    
    @abc.abstractstaticmethod
    def execute(**kwargs) -> Any:
        """Execute tool"""
        pass
    
    @classmethod
    @abc.abstractmethod
    def validate_arguments(cls, args: Dict[str, Any]) -> Optional[str]:
        """Validate tool arguments before execution"""
        pass
    
    @classmethod
    @abc.abstractmethod
    def get_preview(cls, args: Dict[str, Any]) -> Optional[str]:
        """Get preview text for the tool"""
        pass
    
    # Optional methods with default implementations
    @classmethod
    def get_precheck(cls, args: Dict[str, Any]):
        """Get precheck result (optional method)"""
        return None
    
    @classmethod
    def get_request_template(cls, prefix: str) -> Optional[str]:
        """Get request template (optional method)"""
        return None
    
    @classmethod
    def get_response_template(cls, item) -> Optional[str]:
        """Get response template (optional method)"""
        return None

    @classmethod
    def user_input_hook(cls) -> Optional["UserContent"]:
        """Optional hook to inject tool status into user input."""
        return None
    
    @classmethod
    def on_denial(cls, args: Dict[str, Any]) -> Dict[str, Any]:
        """Get denial information (optional method)
        
        Returns a dictionary that will be placed in the content's "info" field
        when the tool execution is denied.
        """
        return {}

    # ------------------------------------------------------------------
    # Unified runner that handles validation / precheck / approval / execute
    # ------------------------------------------------------------------

    @classmethod
    async def run(
        cls,
        args: Dict[str, Any],
        *,
        cancel_event: Optional[asyncio.Event] = None,
        approval_publish: Optional[Callable[[EventPayload], Awaitable[None]]] = None,
        event_publish: Optional[Callable[[EventPayload], Awaitable[None]]] = None,
    ) -> ToolResult:
        """Run the tool through validation, optional approval, and execution."""
        validation_error = cls.validate_arguments(args)
        if validation_error:
            return ToolResult(status="failure", error=validation_error)

        precheck = cls.get_precheck(args) or ToolPrecheckResult(status=STATUS_ALLOW)
        if precheck.status == STATUS_DENY:
            return ToolResult(
                status="denied",
                error=precheck.message or "Tool precheck denied execution.",
                metadata={"reasons": precheck.reasons},
            )

        if precheck.status == STATUS_NEEDS_APPROVAL:
            if approval_publish is None:
                return ToolResult(
                    status="denied",
                    error="Approval required but publisher is unavailable.",
                    metadata={"reasons": precheck.reasons},
                    needs_user_input=True,
                )
            approved = await cls._request_tool_approval_via_publish(approval_publish, args, precheck)
            if not approved:
                return ToolResult(
                    status="denied",
                    error=precheck.message or "User denied tool approval.",
                    metadata={"reasons": precheck.reasons},
                    needs_user_input=True,
                )

        loop: Optional[asyncio.AbstractEventLoop] = None
        try:
            loop = asyncio.get_running_loop()
        except Exception:
            loop = None
        kwargs = cls._build_execute_kwargs(
            args,
            cancel_event=cancel_event,
            event_publish=event_publish,
            loop=loop,
        )

        try:
            out = await cls._call_execute(kwargs)
        except ToolExecutionError as exc:
            return ToolResult(
                status="failure",
                error=str(exc),
                metadata=exc.metadata,
                needs_user_input=exc.needs_user_input,
                content=exc.content,
            )
        except Exception as exc:
            return ToolResult(status="failure", error=str(exc))

        if isinstance(out, ToolResult):
            return out
        return ToolResult(status="success", content=out)

    @classmethod
    async def _call_execute(cls, kwargs: Dict[str, Any]) -> Any:
        execute_fn = cls.execute
        if inspect.iscoroutinefunction(execute_fn):
            return await execute_fn(**kwargs)
        # Run sync tools in a worker thread so long-running commands don't block UI/event loop.
        return await asyncio.to_thread(execute_fn, **kwargs)

    @classmethod
    def _build_execute_kwargs(
        cls,
        args: Dict[str, Any],
        *,
        cancel_event: Optional[asyncio.Event],
        event_publish: Optional[Callable[[EventPayload], Awaitable[None]]] = None,
        loop: Optional[asyncio.AbstractEventLoop] = None,
    ) -> Dict[str, Any]:
        execute_fn = cls.execute
        sig = inspect.signature(execute_fn)
        kwargs = dict(args)
        if "cancel_event" in sig.parameters and cancel_event is not None:
            kwargs.setdefault("cancel_event", cancel_event)
        if "event_publish" in sig.parameters and event_publish is not None:
            kwargs.setdefault("event_publish", event_publish)
        if "emit_event" in sig.parameters and event_publish is not None and loop is not None:
            def _emit_event(payload: EventPayload) -> None:
                try:
                    loop.call_soon_threadsafe(asyncio.create_task, event_publish(payload))
                except Exception:
                    pass
            kwargs.setdefault("emit_event", _emit_event)
        if "event_loop" in sig.parameters and loop is not None:
            kwargs.setdefault("event_loop", loop)
        return kwargs

    @classmethod
    async def _request_tool_approval_via_publish(
        cls,
        publish: Callable[[EventPayload], Awaitable[None]],
        args: Dict[str, Any],
        precheck: ToolPrecheckResult,
    ) -> bool:
        loop = asyncio.get_running_loop()
        approval_fut: asyncio.Future = loop.create_future()
        await publish(
            ev_approval_request(
                id=f"appr_{uuid4().hex}",
                tool=cls.name,
                args=args,
                future=approval_fut,
                precheck={
                    "status": precheck.status,
                    "message": precheck.message,
                    "reasons": precheck.reasons,
                    "metadata": precheck.metadata,
                },
            )
        )
        try:
            fut_res = await approval_fut
        except Exception:
            fut_res = None
        if isinstance(fut_res, dict):
            return bool(fut_res.get("approved"))
        return bool(fut_res)

    @classmethod
    async def _request_choice(
        cls,
        publish: Callable[[EventPayload], Awaitable[None]],
        prompt: str,
        choices: list[str],
        *,
        hint: Optional[str] = None,
    ) -> tuple[Optional[str], bool]:
        loop = asyncio.get_running_loop()
        choice_fut: asyncio.Future = loop.create_future()
        await publish(
            ev_choice_request(
                id=f"choice_{uuid4().hex}",
                tool=cls.name,
                prompt=prompt,
                choices=choices,
                hint=hint,
                future=choice_fut,
            )
        )
        try:
            fut_res = await choice_fut
        except Exception:
            fut_res = None
        if isinstance(fut_res, dict):
            return fut_res.get("selected"), bool(fut_res.get("confirmed"))
        return None, False
    
    def format_result(self, result: ToolResult) -> str:
        """Format tool result for LLM consumption"""
        if result.status == "success":
            if isinstance(result.content, (dict, list)):
                return json.dumps(result.content, ensure_ascii=False, indent=2)
            return str(result.content)
        error_info = {
            "error": result.error,
            "metadata": result.metadata,
            "status": result.status,
        }
        return json.dumps(error_info, ensure_ascii=False, indent=2)


# =============================================================================
# Integrated Tool Registry
# =============================================================================

class BaseToolRegistry:
    """Central registry for all available tools"""
    
    def __init__(self):
        self._tools: Dict[str, Type[BaseTool]] = {}
    
    def auto_register_tool(self, tool_class: Type[BaseTool]):
        """Auto-register a tool class."""
        try:
            # Instantiate the tool class to access metadata.
            tool_instance = tool_class()
            
            # Read core metadata.
            name = tool_instance.name
            if not name:
                raise ValueError(f"Tool class {tool_class.__name__} has no valid name")
            
            # Skip if already registered.
            if name in self._tools:
                return
            
            # Register tool class.
            self._tools[name] = tool_class
            
        except Exception as e:
            raise ValueError(f"Failed to auto-register tool {tool_class.__name__}: {e}")
    
    def get_tool_class(self, name: str) -> Optional[Type[BaseTool]]:
        """Get tool class by name"""
        return self._tools.get(name)
    
    def list_tools(self) -> List[str]:
        """List all available tool names"""
        return list(self._tools.keys())
    
    def get_schemas(self) -> List[dict]:
        """Get all tool schemas for OpenAI function calling"""
        schemas = []
        for tool_class in self._tools.values():
            try:
                schema = tool_class.get_schema()
                schemas.append(schema)
            except Exception:
                continue
        return schemas
