from dataclasses import dataclass, field
from enum import StrEnum
from typing import TYPE_CHECKING, Any, List, Optional, Protocol

from django.db.models import Model, Q, QuerySet

if TYPE_CHECKING:
    from django_copyist.copyist import (
        CopyIntent,
        FieldSetToFilterMap,
        IgnoredMap,
        OutputMap,
        SetToFilterMap,
    )


class IgnoreFilterSource(StrEnum):
    UNMATCHED_SET_TO_FILTER_VALUES = "UNMATCHED_SET_TO_FILTER_VALUES"


@dataclass
class IgnoreFilter:
    """
    This configuration describes a filter for excluding certain models during the copying process.

    It is applicable when you wish to exclude a model from being copied if some
    of its fields do not match any existing data.
    This is particularly useful when using the SET_TO_FILTER action.

    :param filter_name: The name of the filter field that will be used
        to exclude all models that do not match any existing data. Usually is an `__in` filter,
        as it will be given list of unmatched ids to exclude.
        e.g. if you have hierarchy `Parent -> Child <-M2M-> Tag` and want to ignore all parents,
        where child didn't match any of its tags, you can use `child_set__tags__id__in`
    :type filter_name: str
    :param filter_source: The value of the IgnoreFilterSource enum.
        It is always set to UNMATCHED_SET_TO_FILTER_VALUES.
    :type filter_source: IgnoreFilterSource
    :param set_to_filter_origin_model: The type of the model in which
        unmatched fields for the SET_TO_FILTER action are expected.
    :type set_to_filter_origin_model: Type[Model]
    :param set_to_filter_field_name: The name of the field for which unmatched values are expected.
    :type set_to_filter_field_name: str
    """

    filter_name: str
    set_to_filter_origin_model: type[Model]
    set_to_filter_field_name: str
    filter_source: IgnoreFilterSource = (
        IgnoreFilterSource.UNMATCHED_SET_TO_FILTER_VALUES
    )


class IgnoreFunc(Protocol):
    @staticmethod
    def __call__(
        model_config: "ModelCopyConfig",
        set_to_filter_map: "SetToFilterMap",
        model_extra_filter: Q | None,
        ignored_map: "IgnoredMap",
        input_data: dict[str, Any],
    ) -> list[Model]:
        """
        A protocol that defines a function to be used for ignoring certain models
        during the copying process.

        This function is expected to take several parameters including the model configuration,
        set to filter map, model extra filter, ignored map, and input data. It should return a
        list of models that are to be excluded from copying.

        :param model_config: The configuration for the model being copied.
        :type model_config: ModelCopyConfig
        :param set_to_filter_map: The current global set to filter map.
        :type set_to_filter_map: SetToFilterMap
        :param model_extra_filter: An optional Q instance, representing current
            filters for model. It is generated by copyist during copy process
            (e.g. to filter instance list by parent id).
            You should apply it to your query to narrow down the list of models
            you are working with.
        :type model_extra_filter: Q, optional
        :param ignored_map: The current global ignored map.
        :type ignored_map: IgnoredMap
        :param input_data: The input data for the copy request.
        :type input_data: Dict[str, Any]
        :return: The list of models that are to be excluded from copying.
        :rtype: List[Model]
        """
        ...


@dataclass
class IgnoreCondition:
    """
    This configuration describes the conditions under which the copying
    of a model should be disregarded.

    Either 'filter_conditions' or 'ignore_func' must be specified.

    Attributes:
        filter_conditions: An optional list of IgnoreFilter instances.
            If provided, these will be used to filter out models that should not be copied.
        ignore_func: A function that, when provided, should return a
            list of models that are to be excluded from copying.
    """

    filter_conditions: list[IgnoreFilter] | None = None
    ignore_func: IgnoreFunc | None = None

    def __post_init__(self):
        if bool(self.ignore_func) == bool(self.filter_conditions):
            raise ValueError(
                "Only one of filter_conditions and ignore_func should be declared"
            )


class CopyActions(StrEnum):
    TAKE_FROM_ORIGIN = "TAKE_FROM_ORIGIN"
    TAKE_FROM_INPUT = "TAKE_FROM_INPUT"
    MAKE_COPY = "MAKE_COPY"
    UPDATE_TO_COPIED = "UPDATE_TO_COPIED"
    SET_TO_FILTER = "SET_TO_FILTER"


class FilterSource(StrEnum):
    FROM_INPUT = "FROM_INPUT"
    FROM_ORIGIN = "FROM_ORIGIN"


@dataclass
class FieldFilterConfig:
    """
    This configuration specifies the source from which the filter value is obtained.

    If the source is FROM_ORIGIN, the filter value is derived from the original model.
    If the source is FROM_INPUT, the filter value is extracted from the input data.

    :param source: A FilterSource enumeration value. It can be either FROM_INPUT or FROM_ORIGIN.
    :type source: FilterSource
    :param key: An optional key in the input data that identifies the filter value.
        This should be specified if the source is FROM_INPUT, defaults to None.
    :type key: str, optional
    """

    source: FilterSource
    key: str | None = None

    def __post_init__(self):
        if self.source == FilterSource.FROM_INPUT and not self.key:
            raise ValueError("FROM_INPUT filter source should define key")

        if self.source != FilterSource.FROM_INPUT and self.key:
            raise ValueError("key should not be defined if not FROM_INPUT type")


class SetToFilterFunc(Protocol):
    @staticmethod
    def __call__(
        model_config: "ModelCopyConfig",
        input_data: dict[str, Any],
        field_name: str,
        field_copy_config: "FieldCopyConfig",
        set_to_filter_map: "SetToFilterMap",
        instance_list: list[Model],
        referenced_instance_list: list[Model],
    ) -> "FieldSetToFilterMap":
        """
        A protocol that defines a function to be used for setting the filter map for a field.

        This function is expected to take several parameters including the model configuration,
        input data, field name, field copy configuration, set to filter map, instance list,
        and referenced instance list. It should return a FieldSetToFilterMap, which stores mapping
        of original object id string to substitute id or None.

        :param model_config: The configuration for the model being copied.
        :type model_config: ModelCopyConfig
        :param input_data: The input data for the copy request.
        :type input_data: Dict[str, Any]
        :param field_name: The name of the field for which the filter map is being set.
        :type field_name: str
        :param field_copy_config: The copy configuration for the SET_TO_FILTER field.
        :type field_copy_config: FieldCopyConfig
        :param set_to_filter_map: The current global set to filter map.
        :type set_to_filter_map: SetToFilterMap
        :param instance_list: The list of instances of the model being copied.
        :type instance_list: List[Model]
        :param referenced_instance_list: The list of instances that are referenced by the field.
        :type referenced_instance_list: List[Model]
        :return: The updated set to filter map for the field.
        :rtype: FieldSetToFilterMap
        """
        ...


@dataclass
class FilterConfig:
    """
    This class is a configuration that describes how a field's value
    should be queried from existing data.

    :param filters: A dictionary storing information on how a field should be queried.
        The key is the field name, and the value is a FieldFilterConfig instance.
        If a field is not present in filters,
        it will be ignored and the default for the model will be used, defaults to None.
    :type filters: Dict[str, FieldFilterConfig], optional
    :param filter_func: An optional instance of SetToFilterFunc.
        If present, it will be used for querying the field value from existing data,
        defaults to None.
    :type filter_func: SetToFilterFunc, optional
    """

    filters: dict[str, FieldFilterConfig] | None = None
    filter_func: SetToFilterFunc | None = None

    def __post_init__(self):
        if bool(self.filters) == bool(self.filter_func):
            raise ValueError(
                "Filter config should define one of: 'filters', 'filter_func'"
            )


@dataclass
class FieldCopyConfig:
    """
    This class is a configuration that describes how a specific field should be copied.

    :param action: The action to be executed.
    :type action: CopyAction
    :param copy_with_config: A nested instance of ModelCopyConfig.
        This should be defined if the action is MAKE_COPY.
        It describes how a nested model should be copied, defaults to None.
    :type copy_with_config: ModelCopyConfig, optional
    :param reference_to: The model type to which the field should be updated.
        This should be defined if the action is UPDATE_TO_COPIED or SET_TO_FILTER,
        defaults to None.
    :type reference_to: Model, optional
    :param filter_config: An instance of FilterConfig.
        This should be defined if the action is SET_TO_FILTER. It describes how the value for the
        field should be queried from existing data, defaults to None.
    :type filter_config: FilterConfig, optional
    :param input_key: The key in the input data from which the value for the field should be taken.
        This should be defined if the action is TAKE_FROM_INPUT, defaults to None.
    :type input_key: str, optional
    :raises ValueError: If the action is MAKE_COPY and copy_with_config is not defined,
        or if the action is UPDATE_TO_COPIED and reference_to is not defined,
        or if the action is SET_TO_FILTER and either filter_config
        or reference_to is not defined, or if the action is TAKE_FROM_INPUT
        and input_key is not defined.
    """

    action: CopyActions
    copy_with_config: Optional["ModelCopyConfig"] = None
    reference_to: type[Model] | None = None
    filter_config: FilterConfig | None = None
    input_key: str | None = None

    def __post_init__(self):
        if self.action == CopyActions.MAKE_COPY and not self.copy_with_config:
            raise ValueError("MAKE_COPY action should define copy_with_config")
        elif self.action == CopyActions.UPDATE_TO_COPIED and not self.reference_to:
            raise ValueError("UPDATE_TO_COPIED should define reference_to")
        elif self.action == CopyActions.SET_TO_FILTER:
            if not self.filter_config:
                raise ValueError("SET_TO_FILTER should define filter_config")
            if not self.reference_to:
                raise ValueError("SET_TO_FILTER should define reference_to")
        elif self.action == CopyActions.TAKE_FROM_INPUT and not self.input_key:
            raise ValueError("TAKE_FROM_INPUT should define input_key")


TAKE_FROM_ORIGIN = FieldCopyConfig(action=CopyActions.TAKE_FROM_ORIGIN)
"""
Shortcut for creating FieldCopyConfig with TAKE_FROM_ORIGIN action
"""


def UpdateToCopied(reference: type[Model]) -> FieldCopyConfig:
    """
    This function is a shortcut for creating a FieldCopyConfig instance with the
    UPDATE_TO_COPIED action.

    :param reference: The type of the model to which the field should be updated.
    :type reference: Type[Model]
    :return: A FieldCopyConfig instance with the action
        set to UPDATE_TO_COPIED and the reference set to the provided model type.
    :rtype: FieldCopyConfig
    """
    return FieldCopyConfig(action=CopyActions.UPDATE_TO_COPIED, reference_to=reference)


def MakeCopy(config: "ModelCopyConfig") -> FieldCopyConfig:
    """
    This function is a shortcut for creating a FieldCopyConfig instance with the MAKE_COPY action.

    :param config: A nested instance of ModelCopyConfig.
        This describes how a nested model should be copied.
    :type config: ModelCopyConfig
    :return: A FieldCopyConfig instance with the action set to
        MAKE_COPY and the copy_with_config set to the provided config.
    :rtype: FieldCopyConfig
    """
    return FieldCopyConfig(action=CopyActions.MAKE_COPY, copy_with_config=config)


class DataModificationActions(StrEnum):
    DELETE_BY_FILTER = "DELETE_BY_FILTER"
    EXECUTE_FUNC = "EXECUTE_FUNC"


class DataPreparationFunc(Protocol):
    @staticmethod
    def __call__(
        model_config: "ModelCopyConfig",
        input_data: dict[str, Any],
        set_to_filter_map: "SetToFilterMap",
        output_map: "OutputMap",
    ) -> None:
        """
        A protocol that defines a function to be used for preparing data before the copying process.

        This function is expected to take several parameters including the model configuration,
        input data, set to filter map, and output map. It does not return any value.

        :param model_config: The configuration for the model being copied.
        :type model_config: ModelCopyConfig
        :param input_data: The input data for the copy request.
        :type input_data: Dict[str, Any]
        :param set_to_filter_map: The current global set to filter map.
        :type set_to_filter_map: SetToFilterMap
        :param output_map: The current global output map.
        :type output_map: OutputMap
        """
        ...


@dataclass
class DataModificationStep:
    action: DataModificationActions
    filter_field_to_input_key: dict[str, str] | None = None
    func = None

    def __post_init__(self):
        if bool(self.func) == bool(self.filter_field_to_input_key):
            raise ValueError(
                "Only one of func and filter_field_to_input_key should be declared"
            )


@dataclass
class DataPreparationStep(DataModificationStep):
    func: DataPreparationFunc | None = None


class PostcopyFunc(Protocol):
    @staticmethod
    def __call__(
        model_config: "ModelCopyConfig",
        input_data: dict[str, Any],
        set_to_filter_map: "SetToFilterMap",
        output_map: "OutputMap",
        copy_intent_list: "List[CopyIntent]",
    ) -> None:
        """
        A protocol that defines a function to be used for post-copy operations.

        This function is expected to take several parameters including the model configuration,
        input data, set to filter map, output map, and a list of copy intents.
        It does not return any value.

        :param model_config: The configuration for the model being copied.
        :type model_config: ModelCopyConfig
        :param input_data: The input data for the copy request.
        :type input_data: Dict[str, Any]
        :param set_to_filter_map: The current global set to filter map.
        :type set_to_filter_map: SetToFilterMap
        :param output_map: The current global output map.
        :type output_map: OutputMap
        :param copy_intent_list: The list of copy intents.
            Copy intent stores information about original and copied model
        :type copy_intent_list: List[CopyIntent]
        """
        ...


@dataclass
class PostcopyStep(DataModificationStep):
    """
    This class represents a step to be executed after the copying of model data.

    :param action: The action to be executed.
    :type action: Action
    :param filter_field_to_input_key: An optional mapping of model
        field names to input data keys. This is used for the `DELETE_BY_FILTER` action.
        If present, it will be used as an additional filter for querying the given model,
        defaults to None.
    :type filter_field_to_input_key: dict, optional
    :param func: An optional instance of PostcopyFunc.
        If present, it will be executed after the copying of model data, defaults to None.
    :type func: PostcopyFunc, optional
    """

    func: PostcopyFunc | None = None


@dataclass
class ModelCopyConfig:
    """
    This class is a configuration that describes how a specific model should be copied.

    :param model: The root model class that should be copied.
    :type model: Type[Model]
    :param field_copy_actions: A dictionary storing information on copying each field.
        The key is the field name, and the value is a FieldCopyConfig instance.
        If a field is not present in field_copy_actions,
        it will be ignored and the default for the model will be used.
    :type field_copy_actions: Dict[str, FieldCopyConfig]
    :param ignore_condition: An optional instance of IgnoreCondition.
        This describes the condition on which copying of the model should be ignored,
        defaults to None.
    :type ignore_condition: IgnoreCondition, optional
    :param compound_copy_actions: A list of ModelCopyConfig instances.
        The copying for these instances should be executed after copying the current model.
        This should be used if describing other models in a nested style in
        field_copy_actions is not appropriate
        (e.g., when copying another model depends on multiple fields), defaults to None.
    :type compound_copy_actions: List[ModelCopyConfig], optional
    :param filter_field_to_input_key: A dictionary mapping of model field names,
        which are used for the initial querying of the model,
        to input data key (e.g., {"id": "player_id"} if you want to query
        by Player.id and you have the field "player_id" in input_data), defaults to None.
    :type filter_field_to_input_key: Dict[str, str], optional
    :param data_preparation_steps: A list of DataPreparationStep instances.
        These can be used if special actions are needed to prepare the database state
        before copying, for example, if you need to delete some data in the
        target "location" before copying data from the origin, defaults to None.
    :type data_preparation_steps: List[DataPreparationStep], optional
    :param postcopy_steps: A list of PostcopyStep instances. These can be used if
        special actions are needed after the model data is copied, defaults to None.
    :type postcopy_steps: List[PostcopyStep], optional
    :param static_filters: An optional Q instance. If present, this will be used
        as an additional filter for querying the given model, defaults to None.
    :type static_filters: Q, optional
    """

    model: type[Model]
    field_copy_actions: dict[str, FieldCopyConfig]
    ignore_condition: IgnoreCondition | None = None
    compound_copy_actions: list["ModelCopyConfig"] = field(default_factory=list)
    filter_field_to_input_key: dict[str, str] = field(default_factory=dict)
    data_preparation_steps: list[DataPreparationStep] = field(default_factory=list)
    postcopy_steps: list[PostcopyStep] = field(default_factory=list)
    static_filters: Q | None = None


def get_queryset_for_model_config(
    model_config: ModelCopyConfig,
    extra_filters: Q | None,
    input_data: dict[str, Any],
) -> QuerySet:
    model_class = model_config.model
    if not (model_config.filter_field_to_input_key or extra_filters):
        raise ValueError(
            f"get_instances_for_model_config was called without filters for {model_class.__name__}"
        )

    query = model_class.objects.all()
    if extra_filters:
        query = query.filter(extra_filters)

    if model_config.filter_field_to_input_key:
        for filter_field, input_key in model_config.filter_field_to_input_key.items():
            input_value = input_data.get(input_key)
            if not input_value:
                raise ValueError(
                    f"Filter {filter_field} value with key {input_key} was not found in input_data"
                )
            query = query.filter(**{filter_field: input_value})

    if model_config.static_filters:
        query = query.filter(model_config.static_filters)

    query = query.distinct()
    return query
