"""
Pydantic Schema Discovery.

Auto-discovers Pydantic UI schemas from lightwave.schema.pydantic.ui
by introspecting model fields, types, and serialization aliases.
"""

from __future__ import annotations

import importlib
import inspect
import pkgutil
from dataclasses import dataclass, field
from typing import Any, get_args, get_origin

from pydantic import BaseModel


@dataclass
class PydanticProperty:
    """A property within a Pydantic schema."""

    name: str
    python_type: str
    optional: bool
    serialization_alias: str | None = None
    description: str | None = None

    @property
    def camel_case_name(self) -> str:
        """Get the camelCase name (alias or converted from snake_case)."""
        if self.serialization_alias:
            return self.serialization_alias
        # Convert snake_case to camelCase
        parts = self.name.split("_")
        return parts[0] + "".join(p.capitalize() for p in parts[1:])

    def __repr__(self) -> str:
        opt = " (optional)" if self.optional else ""
        alias = f" -> {self.serialization_alias}" if self.serialization_alias else ""
        return f"{self.name}: {self.python_type}{opt}{alias}"


@dataclass
class PydanticSchema:
    """A Pydantic model schema definition."""

    name: str
    module: str
    source_file: str
    properties: dict[str, PydanticProperty] = field(default_factory=dict)
    base_classes: list[str] = field(default_factory=list)

    def __repr__(self) -> str:
        bases = f" extends {', '.join(self.base_classes)}" if self.base_classes else ""
        return f"{self.name}{bases} {{ {len(self.properties)} props }}"


class PydanticSchemaDiscovery:
    """
    Auto-discover Pydantic UI schemas from lightwave.schema.pydantic.ui.

    Introspects model fields to extract type information and serialization aliases,
    enabling comparison with TypeScript interfaces.

    Usage:
        discovery = PydanticSchemaDiscovery()
        schemas = discovery.discover_all()

        for name, schema in schemas.items():
            print(f"{name}: {len(schema.properties)} properties")
    """

    UI_MODULE = "lightwave.schema.pydantic.ui"

    def __init__(self, module_path: str | None = None):
        """
        Initialize discovery with optional custom module path.

        Args:
            module_path: Module path to discover schemas from.
                        Defaults to lightwave.schema.pydantic.ui
        """
        self.module_path = module_path or self.UI_MODULE
        self._cache: dict[str, PydanticSchema] = {}

    def discover_all(self) -> dict[str, PydanticSchema]:
        """
        Discover all Pydantic UI schemas.

        Returns:
            Dictionary mapping schema names to PydanticSchema objects
        """
        self._cache.clear()
        self._discover_module(self.module_path)
        return self._cache

    def _discover_module(self, module_name: str) -> None:
        """Recursively discover schemas in a module."""
        try:
            module = importlib.import_module(module_name)
        except ImportError as e:
            # Module doesn't exist or has import errors
            import warnings

            warnings.warn(f"Could not import {module_name}: {e}", stacklevel=2)
            return

        # Discover classes in this module
        for name in dir(module):
            if name.startswith("_"):
                continue

            obj = getattr(module, name)

            # Check if it's a Pydantic model ending with Props
            if (
                isinstance(obj, type)
                and issubclass(obj, BaseModel)
                and obj is not BaseModel
                and name.endswith("Props")
                and obj.__module__ == module.__name__  # Defined here, not imported
            ):
                self._extract_schema(name, obj, module_name)

        # Recursively discover submodules
        if hasattr(module, "__path__"):
            for _, submodule_name, _ in pkgutil.iter_modules(module.__path__):
                full_name = f"{module_name}.{submodule_name}"
                self._discover_module(full_name)

    def _extract_schema(
        self,
        name: str,
        model: type[BaseModel],
        module_name: str,
    ) -> None:
        """Extract schema information from a Pydantic model."""
        # Skip if already discovered
        if name in self._cache:
            return

        try:
            source_file = inspect.getfile(model)
        except TypeError:
            source_file = "unknown"

        schema = PydanticSchema(
            name=name,
            module=module_name,
            source_file=source_file,
            base_classes=[b.__name__ for b in model.__bases__ if issubclass(b, BaseModel) and b is not BaseModel],
        )

        # Extract field information
        for field_name, field_info in model.model_fields.items():
            annotation = field_info.annotation

            # Get type string
            type_str = self._type_to_string(annotation)

            # Check if optional (not required)
            is_optional = not field_info.is_required()

            # Get serialization alias
            alias = field_info.serialization_alias

            schema.properties[field_name] = PydanticProperty(
                name=field_name,
                python_type=type_str,
                optional=is_optional,
                serialization_alias=alias,
                description=field_info.description,
            )

        self._cache[name] = schema

    def _type_to_string(self, annotation: Any) -> str:
        """Convert a type annotation to a string representation."""
        if annotation is None:
            return "None"

        # Handle None type
        if annotation is type(None):
            return "None"

        # Handle generic types (list, dict, Optional, Union)
        origin = get_origin(annotation)
        args = get_args(annotation)

        if origin is not None:
            origin_name = getattr(origin, "__name__", str(origin))

            # Handle Union types (including Optional and Python 3.10+ | syntax)
            # Python 3.10+ creates types.UnionType for X | Y syntax
            is_union = origin_name == "Union" or origin_name == "UnionType" or "Union" in str(origin)
            if is_union:
                type_strs = [self._type_to_string(a) for a in args]
                # Filter out None for cleaner representation
                non_none = [t for t in type_strs if t != "None"]
                has_none = len(non_none) < len(type_strs)
                if len(non_none) == 1 and has_none:
                    return f"{non_none[0]} | None"
                return " | ".join(type_strs)

            # Handle list
            if origin is list:
                inner = self._type_to_string(args[0]) if args else "Any"
                return f"list[{inner}]"

            # Handle dict
            if origin is dict:
                if args:
                    key = self._type_to_string(args[0])
                    val = self._type_to_string(args[1]) if len(args) > 1 else "Any"
                    return f"dict[{key}, {val}]"
                return "dict"

            # Handle Literal
            if origin_name == "Literal" or str(origin) == "typing.Literal":
                return f"Literal{list(args)}"

            # Generic fallback
            if args:
                args_str = ", ".join(self._type_to_string(a) for a in args)
                return f"{origin_name}[{args_str}]"
            return origin_name

        # Handle simple types
        if hasattr(annotation, "__name__"):
            return annotation.__name__

        # Fallback to string representation
        return str(annotation)

    def get_schema(self, name: str) -> PydanticSchema | None:
        """Get a specific schema by name."""
        return self._cache.get(name)

    def get_schemas_by_module(self, module_pattern: str) -> dict[str, PydanticSchema]:
        """Get schemas from modules matching a pattern."""
        return {name: schema for name, schema in self._cache.items() if module_pattern in schema.module}

    def get_property_aliases(self, schema_name: str) -> dict[str, str]:
        """
        Get mapping of camelCase aliases to snake_case names for a schema.

        Returns:
            Dict mapping camelCase names to snake_case property names
        """
        schema = self._cache.get(schema_name)
        if not schema:
            return {}

        return {prop.camel_case_name: prop.name for prop in schema.properties.values()}
