"""
Base tool class for iq-sdk.

Tools are single-purpose processing units that can be registered
with workers to handle specific task types.

Versioning:
    Tools use semantic versioning without patch (only: MAJOR.MINOR).
    - Major versions indicate breaking interface changes
    - Minor versions add features without breaking the interface

    The version is derived from the class name:
    - MyTool_1_0 → version 1.0
    - MyTool_2_3 → version 2.3

Execution Model:
    Each tool call creates a NEW INSTANCE of the tool class.
    This instance contains all category data as instance attributes:
    - self.general: General/config data for this call
    - self.input: Task-specific input data
    - self.output: Pre-filled output template (modify in-place)
    - self.api_input, self.api_output, etc.

    Because each call gets its own instance, there are no concurrency
    issues - all data is isolated per call.

Example:
    class ImageResize_1_0(BaseTool):
        async def process(self) -> None:
            # Access via self - it's YOUR instance for this call
            width = self.input.width
            quality = self.general.quality

            # Modify output directly
            self.output.url = await self._resize(...)
            self.output.status = "completed"
"""

from __future__ import annotations

import os
import re
import traceback
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any, ClassVar, Generic, TypeVar

from pydantic import BaseModel

from iq_sdk.properties import ExposureCategory, Property, PropertyRegistry
from iq_sdk.tool_registry import (
    clear_tool_registry,
    get_tool_class,
    get_tool_registry,
    register_tool,
)

# Re-export for backwards compatibility
__all__ = [
    "BaseTool",
    "ToolGeneral",
    "ToolInput",
    "ToolOutput",
    "ToolApiInput",
    "ToolApiOutput",
    "ToolEventReceiver",
    "ToolResult",
    "ToolModelFactory",
    "get_tool_registry",
    "get_tool_class",
    "clear_tool_registry",
]


# ============================================================================
# BASE MODELS
# ============================================================================


class ToolGeneral(BaseModel):
    """Base model for tool general/configuration data."""

    pass


class ToolInput(BaseModel):
    """Base model for tool input data."""

    pass


class ToolOutput(BaseModel):
    """Base model for tool output data."""

    pass


class ToolApiInput(BaseModel):
    """Base model for API input data."""

    pass


class ToolApiOutput(BaseModel):
    """Base model for API output data."""

    pass


class ToolEventReceiver(BaseModel):
    """Base model for event receiver data."""

    pass


TOutput = TypeVar("TOutput", bound=ToolOutput)


# ============================================================================
# TOOL RESULT
# ============================================================================


@dataclass
class ToolResult(Generic[TOutput]):
    """Result wrapper for tool execution."""

    success: bool
    output: TOutput | None = None
    error: str | None = None
    error_traceback: str | None = None
    duration_ms: float = 0.0
    metadata: dict[str, Any] = field(default_factory=dict)
    timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))

    @classmethod
    def success_result(
        cls,
        output: TOutput,
        duration_ms: float = 0.0,
        metadata: dict[str, Any] | None = None,
    ) -> ToolResult[TOutput]:
        """Create a successful result."""
        return cls(
            success=True,
            output=output,
            duration_ms=duration_ms,
            metadata=metadata or {},
        )

    @classmethod
    def failure_result(
        cls,
        error: str,
        error_traceback: str | None = None,
        duration_ms: float = 0.0,
        metadata: dict[str, Any] | None = None,
    ) -> ToolResult[TOutput]:
        """Create a failed result."""
        return cls(
            success=False,
            error=error,
            error_traceback=error_traceback,
            duration_ms=duration_ms,
            metadata=metadata or {},
        )


# ============================================================================
# MODEL FACTORY
# ============================================================================


class ToolModelFactory:
    """
    Factory for creating Pydantic models from a PropertyRegistry.

    Cached per tool class to avoid rebuilding models on every call.
    """

    _cache: ClassVar[dict[int, ToolModelFactory]] = {}

    def __init__(self, properties: PropertyRegistry) -> None:
        self.properties = properties
        self._models: dict[ExposureCategory, type[BaseModel]] = {}
        self._build_models()

    @classmethod
    def for_registry(cls, properties: PropertyRegistry) -> ToolModelFactory:
        """Get or create factory for a PropertyRegistry."""
        key = id(properties)
        if key not in cls._cache:
            cls._cache[key] = cls(properties)
        return cls._cache[key]

    def _build_models(self) -> None:
        """Build Pydantic models for each category with properties."""
        from iq_sdk.properties import create_pydantic_model

        base_classes = {
            ExposureCategory.GENERAL: ToolGeneral,
            ExposureCategory.INPUT: ToolInput,
            ExposureCategory.OUTPUT: ToolOutput,
            ExposureCategory.API_INPUT: ToolApiInput,
            ExposureCategory.API_OUTPUT: ToolApiOutput,
            ExposureCategory.EVENT_RECEIVER: ToolEventReceiver,
        }

        for category in ExposureCategory:
            props = self.properties.by_category(category)
            if props:
                base_class = base_classes.get(category, BaseModel)
                model = create_pydantic_model(
                    f"{category.name.title()}Model",
                    props,
                    base_class=base_class,
                )
                self._models[category] = model

    def get_model(self, category: ExposureCategory) -> type[BaseModel] | None:
        """Get the Pydantic model for a category."""
        return self._models.get(category)

    def has_category(self, category: ExposureCategory) -> bool:
        """Check if a category has any properties defined."""
        return category in self._models

    def create_instance(
        self,
        category: ExposureCategory,
        data: dict[str, Any] | None = None,
        *,
        skip_validation: bool = False,
    ) -> BaseModel | None:
        """
        Create a model instance for a category.

        Args:
            category: The exposure category
            data: Data to populate the model with
            skip_validation: If True, use model_construct() to skip validation.
                           Useful for OUTPUT models that are populated later.
        """
        model = self._models.get(category)
        if model is None:
            return None

        if skip_validation:
            # Use model_construct to skip validation - for output models
            # that will be populated during process()
            return model.model_construct(**(data or {}))

        return model.model_validate(data or {})

    def get_categories(self) -> list[ExposureCategory]:
        """Get list of categories that have properties."""
        return list(self._models.keys())


# ============================================================================
# BASE TOOL
# ============================================================================


class BaseTool(ABC):
    """
    Abstract base class for all tools.

    IMPORTANT: Each execute() call creates a NEW INSTANCE of the tool.
    This means self.input, self.general, self.output etc. are isolated
    per call - no shared state, no concurrency issues.

    Instance Attributes (set per call):
        general: General/config data for this call (GENERAL properties)
        input: Task-specific input data (INPUT properties)
        output: Pre-filled output template - modify directly (OUTPUT properties)
        api_input: External API request data (API_INPUT properties)
        api_output: External API response data (API_OUTPUT properties)
        event_receiver: Event data (EVENT_RECEIVER properties)
        metadata: Additional call metadata

    Class Attributes:
        properties: PropertyRegistry defining the tool interface

    Example:
        class ImageResize_1_0(BaseTool):
            '''Resize images to specified dimensions.'''

            properties = properties  # From properties.py

            async def process(self) -> None:
                # Access via self - this instance is YOURS for this call
                width = self.input.width
                height = self.input.height
                quality = self.general.quality

                # Modify output directly (pre-filled with defaults)
                self.output.resized_url = await self._resize(...)
                self.output.status = "completed"
    """

    # -------------------------------------------------------------------------
    # Class-level attributes (shared across all instances)
    # -------------------------------------------------------------------------

    properties: ClassVar[PropertyRegistry] = PropertyRegistry()

    # -------------------------------------------------------------------------
    # Instance attributes (unique per call)
    # -------------------------------------------------------------------------

    general: BaseModel | None
    input: BaseModel | None
    output: BaseModel | None
    api_input: BaseModel | None
    api_output: BaseModel | None
    event_receiver: BaseModel | None
    metadata: dict[str, Any]

    # Internal
    _factory: ToolModelFactory | None

    def __init__(self) -> None:
        """Initialize a new tool instance (called per execute)."""
        self.general = None
        self.input = None
        self.output = None
        self.api_input = None
        self.api_output = None
        self.event_receiver = None
        self.metadata = {}
        self._factory = None

    def __init_subclass__(cls, **kwargs: Any) -> None:
        """Auto-register tool classes in the global registry."""
        super().__init_subclass__(**kwargs)

        # Skip registration for abstract classes or the base class itself
        if cls.__module__ == "iq_sdk.tool":
            return

        # Register by module path (e.g. "tools.my_tool_1.my_tool_1_0")
        module_path = f"{cls.__module__}.{cls.__name__}"
        register_tool(cls, module_path)

    # -------------------------------------------------------------------------
    # Tool metadata (class-level, derived from class name)
    # -------------------------------------------------------------------------

    @property
    def name(self) -> str:
        """
        Unique identifier for this tool.
        Auto-generated from: {project}:{base_name}:{version}
        """
        project = self._get_project_name()
        base_name = self._get_base_name()
        version = self.version
        return f"{project}:{base_name}:{version}"

    @property
    def description(self) -> str:
        """Human-readable description of what this tool does."""
        return self.__class__.__doc__ or ""

    @property
    def version(self) -> str:
        """Tool version string (MAJOR.MINOR), extracted from class name."""
        return self._extract_version_from_classname()

    @property
    def major_version(self) -> int:
        """Major version number."""
        parts = self.version.split(".")
        return int(parts[0]) if parts else 1

    @property
    def minor_version(self) -> int:
        """Minor version number."""
        parts = self.version.split(".")
        return int(parts[1]) if len(parts) > 1 else 0

    @property
    def factory(self) -> ToolModelFactory:
        """Get the model factory for this tool's properties."""
        if self._factory is None:
            self._factory = ToolModelFactory.for_registry(self.properties)
        return self._factory

    def _get_project_name(self) -> str:
        """Get project name from environment or config."""
        project = os.environ.get("IQ_PROJECT_NAME")
        if project:
            return project
        try:
            from iq_sdk.config import load_settings

            settings = load_settings()
            return settings.project_name
        except Exception:
            return "unknown"

    def _get_base_name(self) -> str:
        """Extract base tool name from class name (snake_case)."""
        class_name = self.__class__.__name__
        base_name = re.sub(r"_\d+_\d+$", "", class_name)
        base_name = re.sub(r"(?<!^)(?=[A-Z])", "_", base_name).lower()
        return base_name

    def _extract_version_from_classname(self) -> str:
        """Extract version from class name suffix."""
        class_name = self.__class__.__name__
        match = re.search(r"_(\d+)_(\d+)$", class_name)
        if match:
            major, minor = match.groups()
            return f"{major}.{minor}"
        return "1.0"

    # -------------------------------------------------------------------------
    # Execution
    # -------------------------------------------------------------------------

    @classmethod
    async def execute(
        cls,
        data: dict[str, dict[str, Any]] | None = None,
        *,
        metadata: dict[str, Any] | None = None,
    ) -> ToolResult:
        """
        Execute the tool with the given data.

        Creates a NEW INSTANCE of the tool for this call, populates it
        with validated data, and calls process(). This ensures complete
        isolation between concurrent calls.

        Args:
            data: Dict with category names as keys and their data as values.
                  Category names match ExposureCategory values:
                  - "general": General/config data
                  - "input": Task input data
                  - "api_input": External API request data
                  - "event_receiver": Event data

                  Output categories are auto-created with defaults.

            metadata: Additional call metadata (request_id, etc.)

        Returns:
            ToolResult containing output or error information

        Example:
            result = await MyTool_1_0.execute({
                "general": {"queue": "fast", "priority": 1},
                "input": {"data": "hello", "count": 5},
            })
        """
        import time

        start_time = time.perf_counter()
        data = data or {}

        try:
            # Create NEW INSTANCE for this call
            instance = cls()
            instance.metadata = metadata or {}

            # Get factory (cached at class level)
            factory = instance.factory

            # Dynamically populate all categories from data dict
            category_attr_map = {
                ExposureCategory.GENERAL: "general",
                ExposureCategory.INPUT: "input",
                ExposureCategory.API_INPUT: "api_input",
                ExposureCategory.EVENT_RECEIVER: "event_receiver",
            }

            # Input categories: validate from provided data
            for category, attr_name in category_attr_map.items():
                if factory.has_category(category):
                    category_data = data.get(category.value, {})
                    model_instance = factory.create_instance(category, category_data)
                    setattr(instance, attr_name, model_instance)

            # Output categories: pre-fill with defaults (will be modified by process)
            # Skip validation since required fields will be set during process()
            output_categories = {
                ExposureCategory.OUTPUT: "output",
                ExposureCategory.API_OUTPUT: "api_output",
            }

            for category, attr_name in output_categories.items():
                if factory.has_category(category):
                    model_instance = factory.create_instance(category, {}, skip_validation=True)
                    setattr(instance, attr_name, model_instance)

            # Execute the tool's process method
            await instance.process()

            duration_ms = (time.perf_counter() - start_time) * 1000

            return ToolResult.success_result(
                output=instance.output,
                duration_ms=duration_ms,
                metadata=instance.metadata,
            )

        except Exception as e:
            duration_ms = (time.perf_counter() - start_time) * 1000

            return ToolResult.failure_result(
                error=str(e),
                error_traceback=traceback.format_exc(),
                duration_ms=duration_ms,
            )

    @abstractmethod
    async def process(self) -> None:
        """
        Process the tool call.

        This is the main method to implement. Access all data via self:
        - self.general: General/config data (GENERAL properties)
        - self.input: Task input data (INPUT properties)
        - self.output: Pre-filled output - modify directly (OUTPUT properties)
        - self.api_input: External API request data
        - self.api_output: External API response data
        - self.event_receiver: Event data
        - self.metadata: Additional metadata

        The output is pre-filled with defaults. Just modify the values
        that differ from defaults.

        Example:
            async def process(self) -> None:
                result = await self._do_work(self.input.data)
                self.output.result = result
                self.output.status = "completed"

        Note:
            - Each call gets its own instance - no shared state
            - Exceptions are caught by execute() and returned as errors
        """
        pass

    # -------------------------------------------------------------------------
    # Lifecycle hooks (called on worker, not per-call)
    # -------------------------------------------------------------------------

    @classmethod  # noqa: B027
    async def setup(cls) -> None:
        """
        Called once when the tool is registered with a worker.

        Use for class-level initialization that should be shared:
        - Database connection pools
        - API client instances
        - Model loading
        - Cache warming

        Note: These resources are shared across all calls. For per-call
        data, use self.general in process().

        Override in subclass if needed. Default implementation does nothing.
        """

    @classmethod  # noqa: B027
    async def teardown(cls) -> None:
        """
        Called when the worker shuts down.

        Use for cleanup of class-level resources created in setup().

        Override in subclass if needed. Default implementation does nothing.
        """

    # -------------------------------------------------------------------------
    # Property accessors
    # -------------------------------------------------------------------------

    def get_input_properties(self) -> list[Property]:
        """Get properties for input category."""
        return self.properties.by_category(ExposureCategory.INPUT)

    def get_output_properties(self) -> list[Property]:
        """Get properties for output category."""
        return self.properties.by_category(ExposureCategory.OUTPUT)

    def get_general_properties(self) -> list[Property]:
        """Get properties for general category."""
        return self.properties.by_category(ExposureCategory.GENERAL)

    def get_api_input_properties(self) -> list[Property]:
        """Get properties for api input category."""
        return self.properties.by_category(ExposureCategory.API_INPUT)

    def get_api_output_properties(self) -> list[Property]:
        """Get properties for api output category."""
        return self.properties.by_category(ExposureCategory.API_OUTPUT)

    def get_event_receiver_properties(self) -> list[Property]:
        """Get properties for event receiver category."""
        return self.properties.by_category(ExposureCategory.EVENT_RECEIVER)

    def to_info(self) -> dict[str, Any]:
        """Return tool metadata as dictionary."""
        return {
            "name": self.name,
            "base_name": self._get_base_name(),
            "description": self.description,
            "version": self.version,
            "major_version": self.major_version,
            "minor_version": self.minor_version,
            "properties": {
                "general": [p.name for p in self.get_general_properties()],
                "input": [p.name for p in self.get_input_properties()],
                "output": [p.name for p in self.get_output_properties()],
                "api_input": [p.name for p in self.get_api_input_properties()],
                "api_output": [p.name for p in self.get_api_output_properties()],
                "event_receiver": [p.name for p in self.get_event_receiver_properties()],
            },
            "categories": [cat.value for cat in self.factory.get_categories()],
        }

    # -------------------------------------------------------------------------
    # Convenience accessors
    # -------------------------------------------------------------------------

    def get(self, category: ExposureCategory) -> BaseModel | None:
        """Get model for a specific category."""
        mapping = {
            ExposureCategory.GENERAL: self.general,
            ExposureCategory.INPUT: self.input,
            ExposureCategory.OUTPUT: self.output,
            ExposureCategory.API_INPUT: self.api_input,
            ExposureCategory.API_OUTPUT: self.api_output,
            ExposureCategory.EVENT_RECEIVER: self.event_receiver,
        }
        return mapping.get(category)

    def to_kwargs(self) -> dict[str, Any]:
        """
        Convert all category data to kwargs dict.

        Useful for passing to helper methods or logging.
        Only includes non-None categories.
        """
        result: dict[str, Any] = {}
        if self.general is not None:
            result["general"] = self.general
        if self.input is not None:
            result["input"] = self.input
        if self.output is not None:
            result["output"] = self.output
        if self.api_input is not None:
            result["api_input"] = self.api_input
        if self.api_output is not None:
            result["api_output"] = self.api_output
        if self.event_receiver is not None:
            result["event_receiver"] = self.event_receiver
        if self.metadata:
            result["metadata"] = self.metadata
        return result
