from __future__ import annotations

from inspect import isclass
from typing import TYPE_CHECKING, Any, cast

from .context_vars import model_factory_context

if TYPE_CHECKING:
    from edgy.core.db.fields.types import BaseFieldType
    from edgy.core.db.models.metaclasses import MetaInfo as ModelMetaInfo

    from .base import ModelFactory
    from .types import (
        FactoryCallback,
        FactoryFieldType,
        FactoryParameters,
        FieldFactoryCallback,
        ModelFactoryContext,
    )


class FactoryField:
    """
    Represents a single field's configuration within a `ModelFactory`.

    This class encapsulates how a specific model field's value should be
    generated by a factory. It allows defining custom callbacks, parameters
    for Faker methods, and exclusion rules. It acts as an abstraction layer
    between the factory definition and the actual data generation process.

    Attributes:
        owner (type[ModelFactory]): A reference to the `ModelFactory` class
            that owns this `FactoryField`. This is set during factory metaclass
            processing.
        parameters (FactoryParameters): A dictionary of parameters to be passed
            to the field's callback or Faker method.
        no_copy (bool): If `True`, this field will not be copied during a
            shallow copy operation of the factory.
        original_name (str): The original attribute name of the field in the
            `ModelFactory` definition.
        _field_type (str): Internal storage for the determined database field type name.
        _callback (FactoryCallback | None): Internal storage for the resolved
            generation callback for this field.
        exclude (bool): If `True`, this field will be excluded from the generated
            model instance.
        name (str): The name of the field in the target Edgy model.
    """

    owner: type[ModelFactory]
    parameters: FactoryParameters
    no_copy: bool
    original_name: str
    _field_type: str = ""
    _callback: FactoryCallback | None = None
    exclude: bool
    name: str

    def __init__(
        self,
        *,
        exclude: bool = False,
        callback: FieldFactoryCallback | None = None,
        parameters: FactoryParameters | None = None,
        field_type: FactoryFieldType | None = None,
        name: str = "",
        no_copy: bool = False,
    ) -> None:
        """
        Initializes a `FactoryField` instance.

        Parameters:
            exclude (bool, optional): If `True`, the field will be excluded from
                                      the generated model. Defaults to `False`.
            callback (FieldFactoryCallback | None, optional): A callable or a
                                                              string representing
                                                              a Faker method name.
                                                              This function generates
                                                              the field's value.
                                                              Defaults to `None`.
            parameters (FactoryParameters | None, optional): A dictionary of
                                                              parameters to pass
                                                              to the `callback`.
                                                              Defaults to `None`.
            field_type (FactoryFieldType | None, optional): The type of the database
                                                            field as a string or class.
                                                            Used for mapping to
                                                            default Faker providers.
                                                            Defaults to `None`.
            name (str, optional): The name of the field in the target Edgy model.
                                  Defaults to an empty string.
            no_copy (bool, optional): If `True`, prevents this field from being
                                      copied during shallow copy operations.
                                      Defaults to `False`.
        """
        self.exclude = exclude
        self.no_copy = no_copy
        self.name = name
        # Initialize parameters, ensuring it's a mutable dict if None.
        self.parameters = parameters or {}
        # Use the setter for field_type to handle type conversion.
        self.field_type = field_type

        # If callback is a string, wrap it in a lambda to call the Faker method.
        if isinstance(callback, str):
            callback_name = callback
            callback = lambda field, context, parameters: getattr(context["faker"], callback_name)(
                **parameters
            )  # noqa
        self.callback = callback  # noqa

    def get_field_type(self, *, db_model_meta: ModelMetaInfo | None = None) -> str:
        """
        Determines and returns the string name of the database field type.

        Prioritizes the `field_type` explicitly set on `FactoryField`. If not set,
        it infers the type from the associated Edgy model's metadata.

        Parameters:
            db_model_meta (ModelMetaInfo | None, optional): The metadata of the
                Edgy model. If `None`, it's retrieved from `self.owner.meta.model.meta`.

        Returns:
            str: The string representation of the field type (e.g., "CharField", "IntegerField").
        """
        if self.field_type:
            return self.field_type
        elif db_model_meta is None:
            # If no model meta is provided, get it from the owner factory's model.
            db_model_meta = self.owner.meta.model.meta
        # Return the class name of the database field type.
        return type(db_model_meta.fields[self.name]).__name__

    def get_callback(self) -> FactoryCallback:
        """
        Resolves and returns the actual callable function used to generate the field's value.

        If a `callback` was explicitly provided during initialization, that is used.
        Otherwise, it looks up the callback from the `owner` factory's mappings
        based on the determined `field_type`. The resolved callback is cached.

        Returns:
            FactoryCallback: The callable function for generating the field's value.
        """
        if self.callback:
            return self.callback
        elif self._callback is None:
            # If no callback is set, infer it from the factory's mappings.
            self._callback = self.owner.meta.mappings[self.get_field_type()]
        return self._callback

    def get_parameters(
        self,
        *,
        context: ModelFactoryContext,
        parameters: FactoryParameters | None = None,
    ) -> dict[str, Any]:
        """
        Combines and resolves parameters for the field's generation callback.

        This method merges parameters provided directly to this call with the
        `parameters` defined during `FactoryField` initialization. It also
        resolves any callable parameters by executing them.

        Parameters:
            context (ModelFactoryContext): The current model factory context,
                                           providing access to Faker and other state.
            parameters (FactoryParameters | None, optional): Additional parameters
                                                              to merge, often coming
                                                              from the `build` call.
                                                              Defaults to `None`.

        Returns:
            dict[str, Any]: A dictionary of resolved parameters to be passed to
                            the field's generation callback.
        """
        current_parameters: FactoryParameters = {}
        # Iterate over both sets of parameters, prioritizing the 'parameters' argument.
        for parameter_dict in [parameters or {}, self.parameters]:
            for name, parameter in parameter_dict.items():
                if name not in current_parameters:  # Only add if not already set.
                    # If the parameter is a callable (and not a class), execute it.
                    if callable(parameter) and not isclass(parameter):
                        current_parameters[name] = parameter(self, context, name)
                    else:
                        current_parameters[name] = parameter
        return current_parameters

    @property
    def field_type(self) -> str:
        """
        Gets the string representation of the field's determined type.

        Returns:
            str: The field type name (e.g., "CharField").
        """
        return self._field_type

    @field_type.setter
    def field_type(self, value: FactoryFieldType | None) -> None:
        """
        Sets the field's type. Handles conversion from class types to string names.

        Parameters:
            value (FactoryFieldType | None): The value to set as the field type.
                                            Can be a string or a class type.
        """
        if value:
            # If value is not a string and is not a class, try to cast it as a type.
            if not isinstance(value, str) and not isclass(value):
                value = cast(type["BaseFieldType"], type(value))
            # If it's a class, get its name.
            if not isinstance(value, str):
                value = value.__name__
        self._field_type = cast(str, value or "")

    @field_type.deleter
    def field_type(self) -> None:
        """
        Deletes the field type, resetting it to an empty string.
        """
        self._field_type = ""

    def __copy__(self) -> FactoryField:
        """
        Creates a shallow copy of the FactoryField instance.

        This is used when a factory is copied, ensuring that FactoryField instances
        are also copied unless `no_copy` is set.

        Returns:
            FactoryField: A new `FactoryField` instance with copied attributes.
        """
        _copy = FactoryField(
            exclude=self.exclude,
            no_copy=self.no_copy,
            callback=self.callback,
            parameters=self.parameters.copy(),  # Shallow copy of parameters dict.
            name=self.name,
            field_type=self.field_type,
        )
        # Copy owner and original_name if they exist.
        if hasattr(self, "owner"):
            _copy.owner = self.owner
        if hasattr(self, "original_name"):
            _copy.original_name = self.original_name
        return _copy

    def inc_callcount(self, *, amount: int = 1, callcounts: dict[int, int] | None = None) -> int:
        """
        Increments the call count for this specific `FactoryField`.

        This is useful for tracking the number of times a field's value has
        been generated, particularly in recursive or dependent factory scenarios.
        The call count is stored in the `model_factory_context`.

        Parameters:
            amount (int, optional): The amount by which to increment the count.
                                    Defaults to 1.
            callcounts (dict[int, int] | None, optional): An explicit dictionary
                                                          to use for tracking call counts.
                                                          If `None`, it uses the
                                                          `callcounts` from the current
                                                          `model_factory_context`.

        Returns:
            int: The new call count for this field.
        """
        if callcounts is None:
            context = model_factory_context.get()
            callcounts = context["callcounts"]
        # Use the ID of the field instance as the key for its call count.
        count = callcounts.setdefault(id(self), 0)
        count += amount
        callcounts[id(self)] = count
        return count

    def get_callcount(self) -> int:
        """
        Retrieves the current call count for this `FactoryField`.

        Returns:
            int: The number of times this field's value has been generated.
        """
        context = model_factory_context.get()
        return context["callcounts"].get(id(self), 0)

    def __call__(self, *, context: ModelFactoryContext, parameters: FactoryParameters) -> Any:
        """
        Makes the `FactoryField` instance callable, generating its value.

        This method acts as a convenience shortcut to `get_callback()` and
        executes it with the provided context and parameters.

        Parameters:
            context (ModelFactoryContext): The current factory context.
            parameters (FactoryParameters): The resolved parameters for the callback.

        Returns:
            Any: The generated value for the field.
        """
        return self.get_callback()(self, context, parameters)


__all__ = ["FactoryField"]
