"""
Property system for iq-sdk tools.

Properties define the interface of tools - their inputs, outputs, and metadata.
Each property can be exposed in multiple categories (input, output, API, events).
"""

from __future__ import annotations

import re
from collections.abc import Iterator
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any

# ============================================================================
# DATA TYPES
# ============================================================================


class DataType(StrEnum):
    """Supported data types for properties."""

    STRING = "str"
    INTEGER = "int"
    FLOAT = "float"
    BOOLEAN = "bool"
    LIST = "list"
    DICT = "dict"
    DATE = "date"
    DATETIME = "datetime"
    FILE = "file"
    IMAGE = "image"
    JSON = "json"
    ANY = "any"

    @classmethod
    def from_str(cls, value: str) -> DataType:
        """Parse a string to DataType."""
        mapping = {
            "str": cls.STRING,
            "string": cls.STRING,
            "int": cls.INTEGER,
            "integer": cls.INTEGER,
            "float": cls.FLOAT,
            "number": cls.FLOAT,
            "bool": cls.BOOLEAN,
            "boolean": cls.BOOLEAN,
            "list": cls.LIST,
            "array": cls.LIST,
            "dict": cls.DICT,
            "object": cls.DICT,
            "date": cls.DATE,
            "datetime": cls.DATETIME,
            "file": cls.FILE,
            "image": cls.IMAGE,
            "json": cls.JSON,
            "any": cls.ANY,
        }
        return mapping.get(value.lower(), cls.STRING)


# ============================================================================
# PRIVACY CLASSES
# ============================================================================


@dataclass
class PrivacyClassInfo:
    """Metadata for a privacy class."""

    name: str
    description: str
    icon: str
    display_value: str  # What to show when masked
    roles: list[str] = field(default_factory=list)  # Roles that can view (for Protected)


class PrivacyClass(StrEnum):
    """
    Privacy classification for properties.

    Determines how property values are displayed and who can access them:
    - PUBLIC: Fully visible to everyone
    - PROTECTED: Masked by default, viewable with specific roles
    - SECRET: Never visible, cannot be retrieved

    Privacy can be set at tool level (default for all properties) or
    property level (overrides tool default).

    Names containing 'password', 'secret', 'api_token', 'api_key', 'credential'
    are automatically classified as SECRET.
    """

    PUBLIC = "public"
    PROTECTED = "protected"
    SECRET = "secret"

    @property
    def info(self) -> PrivacyClassInfo:
        """Get metadata for this privacy class."""
        return _PRIVACY_INFO[self]

    @property
    def description(self) -> str:
        """Human-readable description."""
        return self.info.description

    @property
    def icon(self) -> str:
        """Icon for UI display."""
        return self.info.icon

    @property
    def display_value(self) -> str:
        """What to display when value is masked."""
        return self.info.display_value

    @classmethod
    def from_property_name(cls, name: str) -> PrivacyClass | None:
        """
        Auto-detect privacy class from property name.

        Returns SECRET for names containing sensitive keywords,
        None otherwise (use explicit setting or default).
        """
        sensitive_patterns = [
            r"password",
            r"secret",
            r"api_token",
            r"api_key",
            r"api_secret",
            r"credential",
            r"private_key",
            r"access_token",
            r"refresh_token",
            r"auth_token",
        ]

        name_lower = name.lower()
        for pattern in sensitive_patterns:
            if re.search(pattern, name_lower):
                return cls.SECRET

        return None


_PRIVACY_INFO: dict[PrivacyClass, PrivacyClassInfo] = {
    PrivacyClass.PUBLIC: PrivacyClassInfo(
        name="Public",
        description="Fully visible, no restrictions",
        icon="🌐",
        display_value="",  # Show actual value
    ),
    PrivacyClass.PROTECTED: PrivacyClassInfo(
        name="Protected",
        description="Masked by default, viewable with specific roles",
        icon="🔒",
        display_value="***",
    ),
    PrivacyClass.SECRET: PrivacyClassInfo(
        name="Secret",
        description="Never visible, cannot be retrieved",
        icon="🔐",
        display_value="[SECRET]",
    ),
}


@dataclass
class ProtectedPrivacy:
    """
    Privacy configuration for PROTECTED class with role-based access.

    Example:
        >>> privacy = ProtectedPrivacy(roles=["admin", "auditor"])
        >>> # User with 'admin' OR 'auditor' role can view the value
    """

    privacy_class: PrivacyClass = PrivacyClass.PROTECTED
    roles: list[str] = field(default_factory=lambda: ["admin"])

    def can_view(self, user_roles: list[str]) -> bool:
        """Check if user with given roles can view this value."""
        return any(role in self.roles for role in user_roles)


# ============================================================================
# EXPOSURE CATEGORIES
# ============================================================================


@dataclass
class ExposureCategoryInfo:
    """Metadata for an exposure category."""

    name: str
    description: str
    icon: str


class ExposureCategory(StrEnum):
    """
    Categories that define where/how a property is exposed.

    Properties can be assigned to multiple categories, allowing
    the same property definition to be used in different contexts.
    """

    GENERAL = "general"
    INPUT = "input"
    OUTPUT = "output"
    API_INPUT = "api_input"
    API_OUTPUT = "api_output"
    EVENT_RECEIVER = "event_receiver"

    @property
    def info(self) -> ExposureCategoryInfo:
        """Get metadata for this category."""
        return _CATEGORY_INFO[self]

    @property
    def description(self) -> str:
        """Human-readable description."""
        return self.info.description

    @property
    def icon(self) -> str:
        """Icon for UI display."""
        return self.info.icon


_CATEGORY_INFO: dict[ExposureCategory, ExposureCategoryInfo] = {
    ExposureCategory.GENERAL: ExposureCategoryInfo(
        name="General",
        description="General tool metadata and configuration",
        icon="⚙️",
    ),
    ExposureCategory.INPUT: ExposureCategoryInfo(
        name="Input",
        description="Properties received as task input",
        icon="📥",
    ),
    ExposureCategory.OUTPUT: ExposureCategoryInfo(
        name="Output",
        description="Properties returned as task result",
        icon="📤",
    ),
    ExposureCategory.API_INPUT: ExposureCategoryInfo(
        name="API Input",
        description="Properties for external API requests",
        icon="🔌",
    ),
    ExposureCategory.API_OUTPUT: ExposureCategoryInfo(
        name="API Output",
        description="Properties from external API responses",
        icon="📡",
    ),
    ExposureCategory.EVENT_RECEIVER: ExposureCategoryInfo(
        name="Event Receiver",
        description="Properties for receiving external events",
        icon="📨",
    ),
}


# ============================================================================
# CUSTOM PROPERTIES CONFIGURATION
# ============================================================================


@dataclass
class CustomPropertiesConfig:
    """
    Configuration for accepting custom/dynamic properties.

    Allows a property to accept additional key-value pairs at runtime,
    with optional constraints on count and types.

    Examples:
        >>> # Accept any number of custom props with any type
        >>> config = CustomPropertiesConfig(enabled=True)

        >>> # Accept 1-5 custom props, strings only
        >>> config = CustomPropertiesConfig(
        ...     enabled=True,
        ...     min_count=1,
        ...     max_count=5,
        ...     allowed_types=[DataType.STRING],
        ... )
    """

    enabled: bool = False
    min_count: int | None = None
    max_count: int | None = None
    allowed_types: list[DataType] = field(default_factory=list)  # Empty = Any

    @classmethod
    def disabled(cls) -> CustomPropertiesConfig:
        """Create disabled config."""
        return cls(enabled=False)

    @classmethod
    def any(cls) -> CustomPropertiesConfig:
        """Create config accepting any custom properties."""
        return cls(enabled=True)

    @classmethod
    def limited(
        cls,
        min_count: int | None = None,
        max_count: int | None = None,
        allowed_types: list[DataType] | None = None,
    ) -> CustomPropertiesConfig:
        """Create config with limits."""
        return cls(
            enabled=True,
            min_count=min_count,
            max_count=max_count,
            allowed_types=allowed_types or [],
        )


# ============================================================================
# CONSTRAINTS
# ============================================================================


@dataclass
class NumRange:
    """Numeric range constraint."""

    min: float | None = None
    max: float | None = None
    step: float | None = None


# ============================================================================
# PROPERTY DEFINITION
# ============================================================================


@dataclass
class Property:
    """
    A tool property definition.

    Properties define the structure of tool interfaces. Each property
    can be exposed in multiple categories (input, output, API, etc.).

    Example:
        >>> user_id = Property(
        ...     name="user_id",
        ...     data_type=DataType.INTEGER,
        ...     label="User ID",
        ...     description="The unique identifier of the user",
        ...     categories=[ExposureCategory.INPUT, ExposureCategory.OUTPUT],
        ...     required=True,
        ... )
    """

    # Core attributes
    name: str
    data_type: DataType = DataType.STRING
    label: str = ""
    description: str = ""

    # Exposure
    categories: list[ExposureCategory] = field(default_factory=list)

    # Privacy (None = use tool default, auto-detect from name)
    privacy: PrivacyClass | ProtectedPrivacy | None = None

    # Validation
    required: bool = False
    default: Any = None

    # Constraints
    filters: dict[str, Any] = field(default_factory=dict)
    num_range: NumRange | None = None
    enum_values: list[Any] | None = None
    regex: str | None = None

    # UI/Frontend hints
    js_depends_on: str | None = None
    pluck: str | None = None
    samples: str | None = None

    # Custom properties support
    custom_properties: CustomPropertiesConfig | bool = False

    def __post_init__(self) -> None:
        """Initialize computed fields."""
        if not self.label:
            # Capitalize and replace underscores with spaces
            self.label = self.name.replace("_", " ").title()

        if not self.categories:
            self.categories = [ExposureCategory.GENERAL]

        # Convert bool to CustomPropertiesConfig
        if isinstance(self.custom_properties, bool):
            self.custom_properties = CustomPropertiesConfig(enabled=self.custom_properties)

    def get_effective_privacy(
        self, tool_default: PrivacyClass = PrivacyClass.PUBLIC
    ) -> PrivacyClass:
        """
        Get effective privacy class, considering auto-detection and defaults.

        Priority:
        1. Auto-detect SECRET from property name
        2. Explicit property privacy setting
        3. Tool default
        """
        # Auto-detect secret names first (highest priority)
        auto_detected = PrivacyClass.from_property_name(self.name)
        if auto_detected == PrivacyClass.SECRET:
            return PrivacyClass.SECRET

        # Explicit privacy setting
        if self.privacy is not None:
            if isinstance(self.privacy, ProtectedPrivacy):
                return PrivacyClass.PROTECTED
            return self.privacy

        # Tool default
        return tool_default

    def in_category(self, category: ExposureCategory) -> bool:
        """Check if this property is exposed in a category."""
        return category in self.categories

    def add_category(self, category: ExposureCategory) -> None:
        """Add a category to this property."""
        if category not in self.categories:
            self.categories.append(category)

    def to_pydantic_field(self) -> tuple[Any, Any]:
        """
        Convert to Pydantic field definition.

        Returns:
            Tuple of (type, Field) for use in model creation
        """
        from pydantic import Field

        # Map DataType to Python types
        type_mapping = {
            DataType.STRING: str,
            DataType.INTEGER: int,
            DataType.FLOAT: float,
            DataType.BOOLEAN: bool,
            DataType.LIST: list,
            DataType.DICT: dict,
            DataType.DATE: "date",
            DataType.DATETIME: "datetime",
            DataType.FILE: str,  # File path as string
            DataType.IMAGE: str,  # Image path as string
            DataType.JSON: dict,
            DataType.ANY: Any,
        }

        python_type = type_mapping.get(self.data_type, str)

        # Build Field kwargs
        field_kwargs: dict[str, Any] = {
            "description": self.description or self.label,
        }

        if self.required:
            field_kwargs["default"] = ...
        elif self.default is not None:
            field_kwargs["default"] = self.default
        else:
            field_kwargs["default"] = None
            python_type = python_type | None  # type: ignore

        # Add constraints
        if self.num_range:
            if self.num_range.min is not None:
                field_kwargs["ge"] = self.num_range.min
            if self.num_range.max is not None:
                field_kwargs["le"] = self.num_range.max

        if self.regex:
            field_kwargs["pattern"] = self.regex

        return (python_type, Field(**field_kwargs))

    def to_dict(self) -> dict[str, Any]:
        """
        Convert property to dictionary for serialization.

        Returns:
            Dictionary representation suitable for JSON serialization.
        """
        result: dict[str, Any] = {
            "name": self.name,
            "data_type": self.data_type.value,
            "label": self.label,
            "categories": [c.value for c in self.categories],
            "required": self.required,
        }

        # Add optional fields only if set
        if self.description:
            result["description"] = self.description
        if self.default is not None:
            result["default"] = self.default
        if self.filters:
            result["filters"] = self.filters
        if self.num_range:
            result["num_range"] = {"min": self.num_range.min, "max": self.num_range.max}
        if self.enum_values:
            result["enum_values"] = self.enum_values
        if self.regex:
            result["regex"] = self.regex
        if self.js_depends_on:
            result["js_depends_on"] = self.js_depends_on
        if self.pluck:
            result["pluck"] = self.pluck
        if self.samples:
            result["samples"] = self.samples
        if self.privacy:
            if isinstance(self.privacy, ProtectedPrivacy):
                result["privacy"] = {"class": "protected", "roles": self.privacy.roles}
            else:
                result["privacy"] = self.privacy.value
        if (
            isinstance(self.custom_properties, CustomPropertiesConfig)
            and self.custom_properties.enabled
        ):
            result["custom_properties"] = {
                "enabled": True,
                "min_count": self.custom_properties.min_count,
                "max_count": self.custom_properties.max_count,
                "allowed_types": [t.value for t in self.custom_properties.allowed_types],
            }

        return result


# ============================================================================
# PROPERTY REGISTRY
# ============================================================================


class PropertyRegistry:
    """
    Central registry for tool properties.

    Manages all properties for a tool and provides lookup by name
    or by exposure category.

    Example:
        >>> props = PropertyRegistry()
        >>> props.add(Property(name="id", data_type=DataType.INTEGER, required=True))
        >>> props.add(Property(name="name", data_type=DataType.STRING))
        >>>
        >>> # Find by name
        >>> id_prop = props.find("id")
        >>>
        >>> # Get all input properties
        >>> inputs = props.by_category(ExposureCategory.INPUT)
    """

    def __init__(self, default_privacy: PrivacyClass = PrivacyClass.PUBLIC) -> None:
        self._properties: dict[str, Property] = {}
        self.default_privacy = default_privacy

    def add(self, prop: Property) -> PropertyRegistry:
        """Add a property to the registry."""
        self._properties[prop.name] = prop
        return self

    def add_or_extend(self, prop: Property) -> PropertyRegistry:
        """
        Add a property or extend its categories if it already exists.

        If the property already exists, merge the new categories with existing ones.
        """
        existing = self._properties.get(prop.name)
        if existing:
            # Merge categories
            for cat in prop.categories:
                existing.add_category(cat)
        else:
            self._properties[prop.name] = prop
        return self

    def find(self, name: str) -> Property | None:
        """Find a property by name."""
        return self._properties.get(name)

    def get(self, name: str) -> Property:
        """Get a property by name, raising if not found."""
        prop = self.find(name)
        if prop is None:
            raise KeyError(f"Property not found: {name}")
        return prop

    def by_category(self, category: ExposureCategory) -> list[Property]:
        """Get all properties in a category."""
        return [p for p in self._properties.values() if p.in_category(category)]

    def all(self) -> list[Property]:
        """Get all registered properties."""
        return list(self._properties.values())

    def names(self) -> list[str]:
        """Get all property names."""
        return list(self._properties.keys())

    def __iter__(self) -> Iterator[Property]:
        """Iterate over all properties."""
        return iter(self._properties.values())

    def __len__(self) -> int:
        """Number of registered properties."""
        return len(self._properties)

    def __contains__(self, name: str) -> bool:
        """Check if a property exists."""
        return name in self._properties

    def to_dict(self) -> dict[str, Any]:
        """
        Convert registry to dictionary for serialization.

        Returns:
            Dictionary with all properties serialized.
        """
        return {
            "properties": [p.to_dict() for p in self._properties.values()],
            "default_privacy": self.default_privacy.value,
        }

    def to_json(self) -> str:
        """
        Serialize registry to JSON string.

        Returns:
            JSON string representation of all properties.
        """
        import json

        return json.dumps(self.to_dict(), indent=2)


# ============================================================================
# STANDARD PROPERTIES
# ============================================================================
# Pre-defined properties that can be imported and reused across tools.


class StandardProperties:
    """
    Collection of commonly used properties.

    These are pre-defined properties that can be imported and added to
    any tool's property registry. They follow best practices and include
    proper privacy classifications.

    Example:
        >>> from iq_sdk.properties import StandardProperties
        >>>
        >>> props = PropertyRegistry()
        >>> props.add(StandardProperties.delay_queue())
        >>> props.add(StandardProperties.environment())
    """

    @staticmethod
    def delay_queue(
        categories: list[ExposureCategory] | None = None,
    ) -> Property:
        """
        Delay queue property for controlling task processing timing.

        Values: "short" (< 1 min), "default" (normal), "long" (> 5 min)
        """
        return Property(
            name="delay_queue",
            data_type=DataType.STRING,
            label="Delay Queue",
            description="Task processing priority queue",
            categories=categories or [ExposureCategory.INPUT],
            required=False,
            default="default",
            enum_values=["short", "default", "long"],
        )

    @staticmethod
    def environment(
        categories: list[ExposureCategory] | None = None,
        enum_values: list[str] | None = None,
    ) -> Property:
        """
        Environment property for multi-environment deployments.

        Default values: "development", "staging", "production"
        """
        return Property(
            name="environment",
            data_type=DataType.STRING,
            label="Environment",
            description="Target deployment environment",
            categories=categories or [ExposureCategory.GENERAL],
            required=False,
            default="development",
            enum_values=enum_values or ["development", "staging", "production"],
        )

    @staticmethod
    def privacy_level(
        categories: list[ExposureCategory] | None = None,
    ) -> Property:
        """
        Privacy level property for data classification.
        """
        return Property(
            name="privacy_level",
            data_type=DataType.STRING,
            label="Privacy Level",
            description="Data privacy classification",
            categories=categories or [ExposureCategory.GENERAL],
            required=False,
            default="public",
            enum_values=["public", "protected", "secret"],
        )

    @staticmethod
    def api_key(
        categories: list[ExposureCategory] | None = None,
    ) -> Property:
        """
        API key property with automatic SECRET privacy.
        """
        return Property(
            name="api_key",
            data_type=DataType.STRING,
            label="API Key",
            description="API authentication key",
            categories=categories or [ExposureCategory.INPUT],
            required=False,
            privacy=PrivacyClass.SECRET,
        )

    @staticmethod
    def request_id(
        categories: list[ExposureCategory] | None = None,
    ) -> Property:
        """
        Request ID property for tracking and correlation.
        """
        return Property(
            name="request_id",
            data_type=DataType.STRING,
            label="Request ID",
            description="Unique request identifier for tracking",
            categories=categories or [ExposureCategory.INPUT, ExposureCategory.OUTPUT],
            required=False,
        )

    @staticmethod
    def correlation_id(
        categories: list[ExposureCategory] | None = None,
    ) -> Property:
        """
        Correlation ID property for distributed tracing.
        """
        return Property(
            name="correlation_id",
            data_type=DataType.STRING,
            label="Correlation ID",
            description="ID for correlating related requests",
            categories=categories or [ExposureCategory.INPUT, ExposureCategory.OUTPUT],
            required=False,
        )

    @staticmethod
    def user_id(
        categories: list[ExposureCategory] | None = None,
        privacy: PrivacyClass = PrivacyClass.PROTECTED,
    ) -> Property:
        """
        User ID property with configurable privacy.
        """
        return Property(
            name="user_id",
            data_type=DataType.STRING,
            label="User ID",
            description="User identifier",
            categories=categories or [ExposureCategory.INPUT],
            required=False,
            privacy=privacy,
        )

    @staticmethod
    def timestamp(
        name: str = "timestamp",
        categories: list[ExposureCategory] | None = None,
    ) -> Property:
        """
        Timestamp property for event timing.
        """
        return Property(
            name=name,
            data_type=DataType.DATETIME,
            label="Timestamp",
            description="Event timestamp",
            categories=categories or [ExposureCategory.OUTPUT],
            required=False,
        )

    @staticmethod
    def metadata(
        categories: list[ExposureCategory] | None = None,
        custom_properties: bool | CustomPropertiesConfig = True,
    ) -> Property:
        """
        Metadata property for arbitrary key-value pairs.
        """
        return Property(
            name="metadata",
            data_type=DataType.DICT,
            label="Metadata",
            description="Additional metadata as key-value pairs",
            categories=categories or [ExposureCategory.INPUT, ExposureCategory.OUTPUT],
            required=False,
            default={},
            custom_properties=custom_properties,
        )

    @staticmethod
    def tags(
        categories: list[ExposureCategory] | None = None,
    ) -> Property:
        """
        Tags property for categorization.
        """
        return Property(
            name="tags",
            data_type=DataType.LIST,
            label="Tags",
            description="List of tags for categorization",
            categories=categories or [ExposureCategory.INPUT, ExposureCategory.OUTPUT],
            required=False,
            default=[],
        )


def create_pydantic_model(
    name: str,
    properties: list[Property],
    base_class: type | None = None,
) -> type:
    """
    Dynamically create a Pydantic model from properties.

    Args:
        name: Model class name
        properties: List of properties to include
        base_class: Optional base class to inherit from

    Returns:
        A new Pydantic model class
    """
    from pydantic import create_model

    field_definitions = {prop.name: prop.to_pydantic_field() for prop in properties}

    if base_class:
        return create_model(name, __base__=base_class, **field_definitions)  # type: ignore[call-overload]
    return create_model(name, **field_definitions)  # type: ignore[call-overload]
