from __future__ import annotations

from collections.abc import Sequence
from functools import lru_cache, partial
from typing import Any, Literal, cast

from edgy.core.db.constants import CASCADE, RESTRICT, SET_NULL
from edgy.core.db.fields.base import Field
from edgy.core.db.fields.types import BaseFieldType
from edgy.exceptions import FieldDefinitionError

# List of default class-related keywords that should be ignored during kwargs processing.
CLASS_DEFAULTS = ["cls", "__class__", "kwargs"]

# Initialize a set of methods from BaseFieldType that can be
# overwritten by a FieldFactory.
default_methods_overwritable_by_factory: set[str] = {
    key
    for key, attr in BaseFieldType.__dict__.items()
    if callable(attr) and not key.startswith("_")
}
# Remove specific methods that should not be overwritten by factories.
default_methods_overwritable_by_factory.discard("get_column_names")
default_methods_overwritable_by_factory.discard("__init__")

# Add useful helper methods that can be overwritten by factories.
default_methods_overwritable_by_factory.add("get_default_value")
default_methods_overwritable_by_factory.add("get_columns_nullable")
default_methods_overwritable_by_factory.add("get_server_default")
default_methods_overwritable_by_factory.add("customize_default_for_server_default")

# Add extra methods that can be overwritten by factories,
# including descriptor methods and lifecycle hooks.
default_methods_overwritable_by_factory.add("__set__")
default_methods_overwritable_by_factory.add("__get__")
default_methods_overwritable_by_factory.add("modify_input")
default_methods_overwritable_by_factory.add("post_save_callback")
default_methods_overwritable_by_factory.add("post_delete_callback")

# Add methods specific to BaseCompositeField that can be overwritten.
default_methods_overwritable_by_factory.add("translate_name")
default_methods_overwritable_by_factory.add("get_composite_fields")

# Add methods specific to Field that can be overwritten.
default_methods_overwritable_by_factory.add("check")
default_methods_overwritable_by_factory.add("get_column")

# Add methods specific to RelationshipField that can be overwritten.
default_methods_overwritable_by_factory.add("traverse_field")
default_methods_overwritable_by_factory.add("is_cross_db")

# Add methods specific to ForeignKey that can be overwritten.
default_methods_overwritable_by_factory.add("expand_relationship")


class FieldFactoryMeta(type):
    """
    Metaclass for `FieldFactory` that enhances `isinstance` and `issubclass` checks.

    This metaclass allows `FieldFactory` and its subclasses to correctly identify
    instances and subclasses of the `BaseFieldType` they are designed to build,
    even though they are factories and not direct field types.
    """

    def __instancecheck__(self, instance: Any) -> bool:
        """
        Checks if an instance is an instance of `FieldFactory` or the field class
        it produces.
        """
        # Checks if the instance is a direct instance of the FieldFactory or
        # an instance of the field class generated by this factory.
        return super().__instancecheck__(instance) or isinstance(
            instance, self._get_field_cls(self)
        )

    def __subclasscheck__(self, subclass: Any) -> bool:
        """
        Checks if a subclass is a subclass of `FieldFactory` or the field class
        it produces.
        """
        # Checks if the subclass is a direct subclass of FieldFactory or
        # a subclass of the field class generated by this factory.
        return super().__subclasscheck__(subclass) or issubclass(
            subclass, self._get_field_cls(self)
        )


class FieldFactory(metaclass=FieldFactoryMeta):
    """
    The base class for all model field factories used within Edgy.

    `FieldFactory` provides a standardized way to define and create database fields.
    It encapsulates the logic for constructing `BaseFieldType` instances,
    applying constraints, determining column and Pydantic types, and
    overwriting specific methods on the generated field instances.

    Attributes:
        field_bases (Sequence[Any]): A sequence of base classes that the generated
                                     field type will inherit from. By default, it's `(Field,)`.
        field_type (Any): The default Pydantic type for the field. Defaults to `None`.
        methods_overwritable_by_factory (frozenset[str]): A frozen set of method names
                                                          that can be overwritten by
                                                          methods defined in the factory.
    """

    field_bases: Sequence[Any] = (Field,)
    field_type: Any = None
    methods_overwritable_by_factory: frozenset[str] = frozenset(
        default_methods_overwritable_by_factory
    )

    def __new__(cls, **kwargs: Any) -> BaseFieldType:
        """
        Creates a new instance of a field type generated by the factory.

        This method is the entry point for creating field instances. It first
        validates the provided keyword arguments using `cls.validate` and then
        delegates the field construction to `cls.build_field`.

        Args:
            **kwargs (Any): Arbitrary keyword arguments to be used in field construction.

        Returns:
            BaseFieldType: An instance of the field type built by this factory.
        """
        cls.validate(kwargs)
        return cls.build_field(**kwargs)

    @classmethod
    def build_field(cls, **kwargs: Any) -> BaseFieldType:
        """
        Constructs and returns a new field instance based on the factory's configuration.

        This method orchestrates the creation of a `BaseFieldType` instance.
        It determines the column type, Pydantic type, and constraints from the
        provided `kwargs` and the factory's properties. It then instantiates
        the field and calls `overwrite_methods` to apply any factory-defined
        method overrides.

        Args:
            **kwargs (Any): Keyword arguments for configuring the field.

        Returns:
            BaseFieldType: The newly constructed and configured field instance.
        """
        column_type = cls.get_column_type(kwargs)
        pydantic_type = cls.get_pydantic_type(kwargs)
        constraints = cls.get_constraints(kwargs)

        # Get the dynamically generated field class.
        new_field = cls._get_field_cls(cls)

        # Instantiate the new field with all relevant properties.
        new_field_obj: BaseFieldType = new_field(  # type: ignore
            field_type=pydantic_type,
            annotation=pydantic_type,
            column_type=column_type,
            constraints=constraints,
            factory=cls,  # Store a reference to the factory itself.
            **kwargs,
        )
        # Apply any methods from the factory that should override the field's methods.
        cls.overwrite_methods(new_field_obj)
        return new_field_obj

    @classmethod
    def overwrite_methods(cls, field_obj: BaseFieldType) -> None:
        """
        Overwrites methods on the given `field_obj` with methods defined in the factory.

        This method iterates through the factory's own methods. If a method's name
        is present in `methods_overwritable_by_factory`, it replaces the corresponding
        method on the `field_obj`. It handles class methods and ensures proper
        `staticmethod` wrapping for consistent behavior across Python versions.

        Args:
            field_obj (BaseFieldType): The field instance whose methods are to be
                                       overwritten.
        """
        for key in dir(cls):
            # Check if the method is intended to be overwritten and exists in the factory.
            if key in cls.methods_overwritable_by_factory and hasattr(cls, key):
                fn = getattr(cls, key)
                original_fn = getattr(field_obj, key, None)
                # Unwrap the function if it's a method wrapper.
                if hasattr(fn, "__func__"):
                    fn = fn.__func__

                # Set the method on the field_obj, binding it with partial to preserve
                # `cls` and `field_obj` context, and making it a static method.
                setattr(
                    field_obj,
                    key,
                    # .__func__ is a workaround for python < 3.10, python >=3.10 works without
                    staticmethod(partial(fn, cls, field_obj, original_fn=original_fn)).__func__,
                )

    @classmethod
    def repack(cls, field_obj: BaseFieldType) -> None:
        """
        Repacks methods on the given `field_obj` that were previously overwritten
        by the factory.

        This method is used to re-apply the `partial` and `staticmethod` wrappers
        to methods that were already overwritten. This can be necessary in scenarios
        where field objects are serialized/deserialized or otherwise lose their
        original method bindings. It ensures that the context (`cls`, `field_obj`)
        remains correctly bound.

        Args:
            field_obj (BaseFieldType): The field instance whose methods are to be repacked.
        """
        for key in dir(cls):
            # Check if the method is intended to be overwritten and exists in the factory.
            if key in cls.methods_overwritable_by_factory and hasattr(cls, key):
                packed_fn = getattr(field_obj, key, None)
                # If the method exists and has a `func` attribute (indicating a partial function).
                if packed_fn is not None and hasattr(packed_fn, "func"):
                    # Reapply the staticmethod and partial binding.
                    setattr(
                        field_obj,
                        key,
                        # .__func__ is a workaround for python < 3.10, python >=3.10 works without
                        staticmethod(
                            partial(packed_fn.func, cls, field_obj, **packed_fn.keywords)
                        ).__func__,
                    )

    @classmethod
    def validate(cls, kwargs: dict[str, Any]) -> None:  # pragma no cover
        """
        Performs validation on the field parameters and can modify them if necessary.

        This method is intended to be overridden by subclasses to implement
        specific validation rules for their respective field types. It receives
        all parameters passed during the field's construction.

        Args:
            kwargs (dict[str, Any]): A dictionary containing all parameters
                                     passed during the field's construction.
        """

    @classmethod
    def get_constraints(cls, kwargs: dict[str, Any]) -> Sequence[Any]:
        """
        Returns a sequence of constraints applicable to the column.

        This method can be overridden by subclasses to provide specific database
        constraints for the column associated with this field type.

        Args:
            kwargs (dict[str, Any]): Keyword arguments provided during field creation.

        Returns:
            Sequence[Any]: A sequence of constraint objects.
        """
        return []

    @classmethod
    def get_column_type(cls, kwargs: dict[str, Any]) -> Any:
        """
        Returns the SQL column type for the field.

        For regular fields, this will return the appropriate SQLAlchemy column type.
        For meta fields (fields that don't directly map to a database column),
        it should return `None`.

        Args:
            kwargs (dict[str, Any]): Keyword arguments provided during field creation.

        Returns:
            Any: The SQLAlchemy column type or `None` if it's a meta field.
        """
        return None

    @classmethod
    def get_pydantic_type(cls, kwargs: dict[str, Any]) -> Any:
        """
        Returns the Pydantic type for the field.

        This type is used by Pydantic for validation and serialization. By default,
        it returns the `field_type` attribute of the factory.

        Args:
            kwargs (dict[str, Any]): Keyword arguments provided during field creation.

        Returns:
            Any: The Pydantic type associated with the field.
        """
        return cls.field_type

    @staticmethod
    @lru_cache(None)
    def _get_field_cls(cls: FieldFactory) -> BaseFieldType:
        """
        Internal static method to dynamically create and cache the actual field class.

        This method uses `lru_cache` to ensure that the field class is created
        only once for each `FieldFactory` type. It constructs a new type based
        on the `FieldFactory`'s `__name__` and `field_bases`.

        Args:
            cls (FieldFactory): The `FieldFactory` instance itself.

        Returns:
            BaseFieldType: The dynamically created and cached field class.
        """
        # Dynamically create a new type based on the factory's name and specified field bases.
        return cast(BaseFieldType, type(cls.__name__, cast(Any, cls.field_bases), {}))


class ForeignKeyFieldFactory(FieldFactory):
    """
    A factory specifically designed for creating Foreign Key fields.

    This factory extends `FieldFactory` to handle the specific requirements of
    foreign key relationships, including target model definition, cascade
    behaviors (`on_update`, `on_delete`), and related name configuration.
    It enforces validation rules for foreign key properties.

    Attributes:
        field_type (Any): The Pydantic type for the foreign key field, typically `Any`
                          as the actual type depends on the related model's primary key.
    """

    field_type: Any = Any

    def __new__(
        cls,
        *,
        to: Any = None,
        on_update: str = CASCADE,
        on_delete: str = RESTRICT,
        related_name: str | Literal[False] = "",
        **kwargs: Any,
    ) -> BaseFieldType:
        """
        Creates a new `ForeignKeyFieldFactory` instance, processing foreign key specific arguments.

        This method merges the foreign key specific arguments (`to`, `on_update`,
        `on_delete`, `related_name`) with any generic `kwargs` before performing
        validation and building the field.

        Args:
            to (Any): The target model or model name to which the foreign key points.
                      Defaults to `None`.
            on_update (str): The action to perform on update of the referenced key.
                             Defaults to `CASCADE`.
            on_delete (str): The action to perform on deletion of the referenced row.
                             Defaults to `RESTRICT`.
            related_name (str | Literal[False]): The name for the reverse relationship
                                                 from the target model. Can be `False`
                                                 to disable the reverse relation.
                                                 Defaults to `""`.
            **kwargs (Any): Additional keyword arguments for the field.

        Returns:
            BaseFieldType: An instance of the foreign key field.
        """
        # Combine local variables (foreign key specific args) with general kwargs.
        kwargs = {
            **kwargs,
            **{key: value for key, value in locals().items() if key not in CLASS_DEFAULTS},
        }
        # Validate the combined arguments.
        cls.validate(kwargs)
        # Build and return the field.
        return cls.build_field(**kwargs)

    @classmethod
    def validate(cls, kwargs: dict[str, Any]) -> None:
        """
        Validates the parameters for a foreign key field.

        This method enforces rules specific to foreign keys, such as:
        - `on_delete` must not be null.
        - If `SET_NULL` is used for `on_delete` or `on_update`, the field must be nullable.
        - `related_name` must be a string if provided and not `False`.
        It also sets a default `null` value if not provided.

        Args:
            kwargs (dict[str, Any]): A dictionary of parameters for the foreign key field.

        Raises:
            FieldDefinitionError: If any foreign key validation rule is violated.
        """
        on_update = kwargs.get("on_update", CASCADE)
        on_delete = kwargs.get("on_delete", RESTRICT)
        # Set default null to False if not explicitly provided.
        kwargs.setdefault("null", False)
        null = kwargs["null"]

        if on_delete is None:
            raise FieldDefinitionError("on_delete must not be null.")

        # If SET_NULL is enabled for on_delete, the field must be nullable.
        if on_delete == SET_NULL and not null:
            raise FieldDefinitionError("When SET_NULL is enabled, null must be True.")

        # If SET_NULL is enabled for on_update, the field must be nullable.
        if on_update and (on_update == SET_NULL and not null):
            raise FieldDefinitionError("When SET_NULL is enabled, null must be True.")
        related_name = kwargs.get("related_name", "")

        # related_name can be None or False, which is tolerated.
        if related_name and not isinstance(related_name, str):
            raise FieldDefinitionError("related_name must be a string.")

        # Convert related_name to lowercase if it's a non-empty string.
        if related_name:
            kwargs["related_name"] = related_name.lower()
