"""
Object-to-atom builders for AMI SDK.

Provides ergonomic utilities for converting object/dict structures into AMI atoms.
"""
import re
from typing import Dict, Any, List, Optional, Union
from .atoms import StateAtom, state, AtomSource

# Type alias for primitive values allowed in atoms
PrimitiveValue = Union[str, int, float, bool]


class StateObject:
    """
    Builder for converting object/dict structures into state atoms.

    Provides cleaner ergonomics for defining multiple related state atoms
    from a single object, while maintaining full compatibility with the
    existing atom system.

    Attributes:
        prefix: Dotted key prefix (e.g., "user", "support.ticket")
        data: Dictionary of field names to primitive values
        source: Atom source ("system", "human", "ml")

    Examples:
        Basic usage:
            user = StateObject("user", {
                "id": "U-123",
                "name": "John Doe",
                "tier": "premium",
                "is_active": True
            })
            atoms = user.to_atoms()
            # Returns: [
            #   state("user.id", "U-123"),
            #   state("user.name", "John Doe"),
            #   state("user.tier", "premium"),
            #   state("user.is_active", True)
            # ]

        Nested prefix:
            ticket = StateObject("support.ticket", {
                "id": "T-456",
                "status": "open",
                "priority": 3
            })
            atoms = ticket.to_atoms()
            # Returns: [
            #   state("support.ticket.id", "T-456"),
            #   state("support.ticket.status", "open"),
            #   state("support.ticket.priority", 3)
            # ]

        Custom source:
            ml_data = StateObject("user", {"score": 0.95}, source="ml")
            atoms = ml_data.to_atoms()
            # Returns: [state("user.score", 0.95, source="ml")]

        Mixed usage:
            atoms = [
                *StateObject("user", {"id": "U-123"}).to_atoms(),
                state("session.active", True),
                tag("intent", "upgrade")
            ]
    """

    # Prefix pattern: 1-6 parts, lowercase, underscores allowed
    PREFIX_PATTERN = re.compile(r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){0,5}$")

    # Field name pattern: lowercase, underscores allowed
    FIELD_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$")

    def __init__(
        self,
        prefix: str,
        data: Dict[str, PrimitiveValue],
        *,
        source: AtomSource = "system"
    ) -> None:
        """
        Create a StateObject builder.

        Args:
            prefix: Dotted key prefix (1-6 parts, e.g., "user", "support.ticket")
            data: Dictionary mapping field names to primitive values (str, int, float, bool)
            source: Atom source ("system", "human", "ml")

        Raises:
            ValueError: If prefix format is invalid, data contains non-primitives,
                       or combined key depth exceeds 7 parts
            TypeError: If data is not a dictionary or contains invalid types

        Example:
            >>> user = StateObject("user", {"id": "U-123", "tier": "premium"})
            >>> atoms = user.to_atoms()
        """
        # Type validation before assignment
        if not isinstance(prefix, str):
            raise TypeError(f"prefix must be a string, got {type(prefix).__name__}")
        if not isinstance(data, dict):
            raise TypeError(f"data must be a dictionary, got {type(data).__name__}")

        self.prefix: str = prefix
        self.data: Dict[str, PrimitiveValue] = data
        self.source: AtomSource = source

        # Validate prefix
        self._validate_prefix()

        # Validate data
        self._validate_data()

    def _validate_prefix(self) -> None:
        """Validate prefix format."""
        if not self.PREFIX_PATTERN.match(self.prefix):
            parts = self.prefix.split(".")
            if len(parts) > 6:
                raise ValueError(
                    f"Prefix must have 1-6 parts (got {len(parts)}): {self.prefix}"
                )
            raise ValueError(
                f"Prefix pattern invalid (must be lowercase, start with letter, use underscores): {self.prefix}"
            )

    def _validate_data(self) -> None:
        """
        Validate data dictionary.

        Checks:
        - Field names match pattern (lowercase, underscore-separated)
        - Combined key depth stays within limits (2-7 parts)
        - Values are primitives (str, int, float, bool)
        - String values don't exceed 256 characters
        - Empty dict is allowed but generates no atoms
        """
        prefix_parts = len(self.prefix.split("."))

        for field_name, value in self.data.items():
            # Validate field name is a string
            if not isinstance(field_name, str):
                raise TypeError(
                    f"Field names must be strings, got {type(field_name).__name__}"
                )

            # Skip fields starting with underscore (internal/private fields)
            if field_name.startswith("_"):
                continue

            # Empty field names not allowed
            if not field_name:
                raise ValueError("Field names cannot be empty strings")

            # Validate field name pattern
            if not self.FIELD_PATTERN.match(field_name):
                raise ValueError(
                    f"Field name pattern invalid (must be lowercase, start with letter, use underscores): {field_name}"
                )

            # Check total depth (prefix + field = 2-7 parts)
            total_parts = prefix_parts + 1
            if total_parts > 7:
                raise ValueError(
                    f"Combined key depth exceeds maximum of 7 parts: {self.prefix}.{field_name} ({total_parts} parts)"
                )
            if total_parts < 2:
                raise ValueError(
                    f"Combined key depth must be at least 2 parts: {self.prefix}.{field_name} ({total_parts} parts)"
                )

            # Validate value type (must be primitive)
            # Note: bool must be checked before int since bool is subclass of int in Python
            if not isinstance(value, (str, int, float, bool)):
                raise TypeError(
                    f"Field '{field_name}' has non-primitive value: {type(value).__name__}. "
                    f"Only str, int, float, bool are allowed."
                )

            # Validate string length
            if isinstance(value, str) and len(value) > 256:
                raise ValueError(
                    f"Field '{field_name}' string value exceeds 256 characters (got {len(value)})"
                )

            # Validate numeric bounds to prevent overflow/precision issues
            if isinstance(value, int) and not isinstance(value, bool):
                # MongoDB supports 64-bit signed integers
                if value < -(2**63) or value >= 2**63:
                    raise ValueError(
                        f"Field '{field_name}' integer value out of range: {value} "
                        f"(must be within -2^63 to 2^63-1)"
                    )

            if isinstance(value, float):
                # Check for special float values
                if not (-1.7976931348623157e+308 <= value <= 1.7976931348623157e+308):
                    raise ValueError(
                        f"Field '{field_name}' float value out of range: {value}"
                    )
                # Warn about NaN/Inf (these cause issues in MongoDB/JSON)
                import math
                if math.isnan(value) or math.isinf(value):
                    raise ValueError(
                        f"Field '{field_name}' has invalid float value: {value} "
                        f"(NaN and Infinity are not allowed)"
                    )

    def to_atoms(self) -> List[StateAtom]:
        """
        Convert object to list of state atoms.

        Returns:
            List of StateAtom instances, one per field in data.
            Fields starting with underscore are excluded.

        Example:
            obj = StateObject("user", {"id": "U-123", "tier": "premium"})
            atoms = obj.to_atoms()
            # [StateAtom(key="user.id", value="U-123"),
            #  StateAtom(key="user.tier", value="premium")]
        """
        atoms = []

        for field_name, value in self.data.items():
            # Skip fields starting with underscore
            if field_name.startswith("_"):
                continue

            # Construct full key
            key = f"{self.prefix}.{field_name}"

            # Create state atom
            atoms.append(state(key, value, source=self.source))

        return atoms

    def __repr__(self) -> str:
        """String representation for debugging."""
        field_count = len([k for k in self.data.keys() if not k.startswith("_")])
        return f"StateObject(prefix='{self.prefix}', fields={field_count}, source='{self.source}')"

    def __eq__(self, other: object) -> bool:
        """
        Check equality with another StateObject.

        Two StateObjects are equal if they have the same prefix, data, and source.
        """
        if not isinstance(other, StateObject):
            return NotImplemented
        return (
            self.prefix == other.prefix
            and self.data == other.data
            and self.source == other.source
        )

    def __hash__(self) -> int:
        """
        Make StateObject hashable.

        Note: This assumes data dict values are hashable (which primitives are).
        """
        # Convert dict to frozenset of items for hashing
        data_items = tuple(sorted(
            (k, v) for k, v in self.data.items()
            if not k.startswith("_")  # Only hash public fields
        ))
        return hash((self.prefix, data_items, self.source))

    def copy(self) -> "StateObject":
        """
        Create a shallow copy of this StateObject.

        Returns:
            New StateObject instance with copied data.

        Example:
            >>> obj1 = StateObject("user", {"id": "U-123"})
            >>> obj2 = obj1.copy()
            >>> obj2.data["id"] = "U-456"
            >>> obj1.data["id"]  # Still "U-123"
        """
        return StateObject(
            prefix=self.prefix,
            data=self.data.copy(),
            source=self.source
        )

    def validate_numeric_fields(self, *field_names: str) -> None:
        """
        Validate that specified fields are numeric (for use with DSL numeric operators).

        This is a convenience method to catch type errors early, before DSL evaluation.

        Args:
            *field_names: Names of fields that should be numeric

        Raises:
            TypeError: If any specified field is not int or float

        Example:
            >>> user = StateObject("user", {"age": 30, "name": "John"})
            >>> user.validate_numeric_fields("age")  # OK
            >>> user.validate_numeric_fields("name")  # TypeError
        """
        for field_name in field_names:
            if field_name not in self.data:
                # Skip validation for missing fields (they might be optional)
                continue

            value = self.data[field_name]

            # Skip underscore-prefixed fields
            if field_name.startswith("_"):
                continue

            # Check if numeric (but not bool, since bool is subclass of int)
            if not isinstance(value, (int, float)) or isinstance(value, bool):
                raise TypeError(
                    f"Field '{field_name}' must be numeric (int or float) for use with "
                    f"DSL comparison operators (>, <, >=, <=). "
                    f"Got {type(value).__name__}: {value!r}"
                )
