from __future__ import annotations

from collections import UserDict
from copy import copy, deepcopy
from dataclasses import dataclass, is_dataclass
from enum import Enum

from pandas import DataFrame
from sqlalchemy import MetaData
from sqlalchemy.orm import DeclarativeBase as SQLTable, registry
from typing_extensions import Any, Optional, Dict, Type, Set, Hashable, Union, List, TYPE_CHECKING

from ..utils import make_set, row_to_dict, table_rows_as_str, get_value_type_from_type_hint, SubclassJSONSerializer, \
    get_full_class_name, get_type_from_string, make_list, is_iterable, serialize_dataclass, dataclass_to_dict, copy_case

if TYPE_CHECKING:
    from ripple_down_rules.rules import Rule
    from .callable_expression import CallableExpression


class Case(UserDict, SubclassJSONSerializer):
    """
    A collection of attributes that represents a set of constraints on a case. This is a dictionary where the keys are
    the names of the attributes and the values are the attributes. All are stored in lower case.
    """

    def __init__(self, _obj_type: Type, _id: Optional[Hashable] = None,
                 _name: Optional[str] = None, original_object: Optional[Any] = None, **kwargs):
        """
        Create a new row.

        :param _obj_type: The type of the object that the row represents.
        :param _id: The id of the row.
        :param _name: The semantic name that describes the row.
        :param kwargs: The attributes of the row.
        """
        super().__init__(kwargs)
        self._original_object = original_object
        self._obj_type: Type = _obj_type
        self._id: Hashable = _id if _id is not None else id(self)
        self._name: str = _name if _name is not None else self._obj_type.__name__

    @classmethod
    def from_obj(cls, obj: Any, obj_name: Optional[str] = None, max_recursion_idx: int = 3) -> Case:
        """
        Create a row from an object.

        :param obj: The object to create a row from.
        :param max_recursion_idx: The maximum recursion index to prevent infinite recursion.
        :param obj_name: The name of the object.
        :return: The row of the object.
        """
        return create_case(obj, max_recursion_idx=max_recursion_idx, obj_name=obj_name)

    def __getitem__(self, item: str) -> Any:
        return super().__getitem__(item.lower())

    def __setitem__(self, name: str, value: Any):
        name = name.lower()
        if name in self:
            if isinstance(self[name], list):
                self[name].extend(make_list(value))
            elif isinstance(value, list):
                new_list = make_list(self[name])
                new_list.extend(make_list(value))
                super().__setitem__(name, new_list)
            else:
                super().__setitem__(name, value)
        else:
            super().__setitem__(name, value)
        setattr(self, name, self[name])

    def __contains__(self, item):
        if isinstance(item, (type, Enum)):
            item = item.__name__
        return super().__contains__(item.lower())

    def __delitem__(self, key):
        super().__delitem__(key.lower())

    def __hash__(self):
        return self._id

    def _to_json(self) -> Dict[str, Any]:
        serializable = {k: v for k, v in self.items() if not k.startswith("_")}
        serializable["_id"] = self._id
        serializable["_obj_type"] = get_full_class_name(self._obj_type) if self._obj_type is not None else None
        serializable["_name"] = self._name
        for k, v in serializable.items():
            if isinstance(v, set):
                serializable[k] = {'_type': get_full_class_name(set), 'value': serialize_dataclass(list(v))}
            else:
                serializable[k] = serialize_dataclass(v)
        return {k: v.to_json() if isinstance(v, SubclassJSONSerializer) else v for k, v in serializable.items()}

    @classmethod
    def _from_json(cls, data: Dict[str, Any]) -> Case:
        id_ = data.pop("_id")
        obj_type = get_type_from_string(data.pop("_obj_type")) if data["_obj_type"] is not None else None
        name = data.pop("_name")
        for k, v in data.items():
            data[k] = SubclassJSONSerializer.from_json(v)
        return cls(_obj_type=obj_type, _id=id_, _name=name, **data)

    def __deepcopy__(self, memo: Dict[Hashable, Any]) -> Case:
        """
        Create a deep copy of the case.

        :param memo: A dictionary to keep track of objects that have already been copied.
        :return: A deep copy of the case.
        """
        new_case = Case(self._obj_type, _id=self._id, _name=self._name, original_object=self._original_object)
        for k, v in self.items():
            new_case[k] = deepcopy(v)
        return new_case

    def __copy__(self) -> Case:
        """
        Create a shallow copy of the case.

        :return: A shallow copy of the case.
        """
        new_case = Case(self._obj_type, _id=self._id, _name=self._name, original_object=self._original_object)
        for k, v in self.items():
            new_case[k] = copy(v)
        return new_case


@dataclass
class CaseAttributeValue(SubclassJSONSerializer):
    """
    A column value is a value in a column.
    """
    id: Hashable
    """
    The row id of the column value.
    """
    value: Any
    """
    The value of the column.
    """

    def __eq__(self, other):
        if not isinstance(other, CaseAttributeValue):
            return False
        return self.value == other.value

    def __hash__(self):
        return self.id

    def _to_json(self) -> Dict[str, Any]:
        return {"id": self.id, "value": self.value}

    @classmethod
    def _from_json(cls, data: Dict[str, Any]) -> CaseAttributeValue:
        return cls(id=data["id"], value=data["value"])


class CaseAttribute(list, SubclassJSONSerializer):
    nullable: bool = True
    """
    A boolean indicating whether the case attribute can be None or not.
    """
    mutually_exclusive: bool = False
    """
    A boolean indicating whether the case attribute is mutually exclusive or not. (i.e. can only have one value)
    """

    @classmethod
    def from_obj(cls, values: List[Any]) -> CaseAttribute:
        return cls(make_list(values))

    @property
    def as_dict(self) -> Dict[str, Any]:
        """
        Get the case attribute as a dictionary.

        :return: The case attribute as a dictionary.
        """
        return {self.__class__.__name__: self}

    def filter_by(self, condition: CallableExpression) -> CaseAttribute:
        """
        Filter the column by a condition.

        :param condition: The condition to filter by.
        :return: The filtered column.
        """
        return self.__class__([v for v in self if condition(v)])

    def __eq__(self, other):
        if not isinstance(other, list):
            return super().__eq__(make_list(other))
        return super().__eq__(other)

    def __hash__(self):
        return hash(id(self))

    def __str__(self):
        if len(self) == 0:
            return "None"
        return str([v for v in self]) if len(self) > 1 else str(next(iter(self)))

    def _to_json(self) -> Dict[str, Any]:
        return {str(i): v.to_json() if isinstance(v, SubclassJSONSerializer) else v
                for i, v in enumerate(self)}

    @classmethod
    def _from_json(cls, data: Dict[str, Any]) -> CaseAttribute:
        return cls([SubclassJSONSerializer.from_json(v) for _, v in data.items()])


def create_cases_from_dataframe(df: DataFrame, name: Optional[str] = None) -> List[Case]:
    """
    Create cases from a pandas DataFrame.

    :param df: The DataFrame to create cases from.
    :param name: The semantic name of the DataFrame that describes the DataFrame.
    :return: The cases of the DataFrame.
    """
    cases = []
    attribute_names = list(df.columns)
    for row_id, case in df.iterrows():
        case = {col_name: case[col_name].item() for col_name in attribute_names}
        cases.append(Case(DataFrame, _id=row_id, _name=name, **case))
    return cases


def create_case(obj: Any, recursion_idx: int = 0, max_recursion_idx: int = 0,
                obj_name: Optional[str] = None, parent_is_iterable: bool = False) -> Case:
    """
    Create a case from an object.

    :param obj: The object to create a case from.
    :param recursion_idx: The current recursion index.
    :param max_recursion_idx: The maximum recursion index to prevent infinite recursion.
    :param obj_name: The name of the object.
    :param parent_is_iterable: Boolean indicating whether the parent object is iterable or not.
    :return: The case that represents the object.
    """
    obj_name = obj_name or obj.__class__.__name__
    if isinstance(obj, DataFrame):
        return create_cases_from_dataframe(obj, obj_name)
    if isinstance(obj, Case) or (is_dataclass(obj) and not isinstance(obj, SQLTable)):
        return obj
    if ((recursion_idx > max_recursion_idx)
            or (obj.__class__.__module__ == "builtins" and not isinstance(obj, (list, set, dict)))
            or (obj.__class__ in [MetaData, registry])):
        return Case(type(obj), _id=id(obj), _name=obj_name, original_object=obj,
                    **{obj_name or obj.__class__.__name__: make_list(obj) if parent_is_iterable else obj})
    case = Case(type(obj), _id=id(obj), _name=obj_name, original_object=obj)
    if isinstance(obj, dict):
        for k, v in obj.items():
            case = create_or_update_case_from_attribute(v, k, obj, obj_name, recursion_idx,
                                                        max_recursion_idx, parent_is_iterable, case)
    for attr in dir(obj):
        if attr.startswith("_") or callable(getattr(obj, attr)):
            continue
        attr_value = getattr(obj, attr)
        case = create_or_update_case_from_attribute(attr_value, attr, obj, obj_name, recursion_idx,
                                                    max_recursion_idx, parent_is_iterable, case)
    return case


def create_or_update_case_from_attribute(attr_value: Any, name: str, obj: Any, obj_name: Optional[str] = None,
                                         recursion_idx: int = 0, max_recursion_idx: int = 1,
                                         parent_is_iterable: bool = False,
                                         case: Optional[Case] = None) -> Case:
    """
    Create or update a case from an attribute of the object that the case represents.

    :param attr_value: The attribute value.
    :param name: The name of the attribute.
    :param obj: The parent object of the attribute.
    :param obj_name: The parent object name.
    :param recursion_idx: The recursion index to prevent infinite recursion.
    :param max_recursion_idx: The maximum recursion index.
    :param parent_is_iterable: Boolean indicating whether the parent object is iterable or not.
    :param case: The case to update.
    :return: The updated/created case.
    """
    if case is None:
        case = Case(type(obj), _id=id(obj), _name=obj_name, original_object=obj)
    if isinstance(attr_value, (dict, UserDict)):
        case.update({f"{obj_name}.{k}": v for k, v in attr_value.items()})
    if hasattr(attr_value, "__iter__") and not isinstance(attr_value, str):
        column = create_case_attribute_from_iterable_attribute(attr_value, name, obj, obj_name,
                                                               recursion_idx=recursion_idx + 1,
                                                               max_recursion_idx=max_recursion_idx)
        case[name] = column
    else:
        case[name] = make_list(attr_value) if parent_is_iterable else attr_value
    return case


def create_case_attribute_from_iterable_attribute(attr_value: Any, name: str, obj: Any, obj_name: Optional[str] = None,
                                                  recursion_idx: int = 0,
                                                  max_recursion_idx: int = 1) -> CaseAttribute:
    """
    Get a case attribute from an iterable attribute.

    :param attr_value: The iterable attribute to get the case from.
    :param name: The name of the case.
    :param obj: The parent object of the iterable.
    :param obj_name: The parent object name.
    :param recursion_idx: The recursion index to prevent infinite recursion.
    :param max_recursion_idx: The maximum recursion index.
    :return: A case attribute that represents the original iterable attribute.
    """
    values = list(attr_value.values()) if isinstance(attr_value, (dict, UserDict)) else attr_value
    try:
        _type = type(list(values)[0]) if len(values) > 0 else get_value_type_from_type_hint(name, obj)
    except ValueError:
        _type = None
    attr_case = Case(_type, _id=id(attr_value), _name=name, original_object=attr_value)
    case_attr = CaseAttribute(values)
    for idx, val in enumerate(values):
        sub_attr_case = create_case(val, recursion_idx=recursion_idx,
                                    max_recursion_idx=max_recursion_idx,
                                    obj_name=name, parent_is_iterable=True)
        attr_case.update(sub_attr_case)
    for sub_attr, val in attr_case.items():
        try:
            setattr(case_attr, sub_attr, val)
        except AttributeError:
            pass
    return case_attr


def show_current_and_corner_cases(case: Any, targets: Optional[Dict[str, Any]] = None,
                                  current_conclusions: Optional[Dict[str, Any]] = None,
                                  last_evaluated_rule: Optional[Rule] = None) -> str:
    """
    Get the the data to show of the new case and if last evaluated rule exists also show that of the corner case.

    :param case: The new case.
    :param targets: The target attribute of the case.
    :param current_conclusions: The current conclusions of the case.
    :param last_evaluated_rule: The last evaluated rule in the RDR.
    """
    corner_case = None
    targets = {f"target_{name}": value for name, value in targets.items()} if targets else {}
    current_conclusions = {name: value for name, value in current_conclusions.items()} if current_conclusions else {}
    information = ""
    if last_evaluated_rule:
        action = "Refinement" if last_evaluated_rule.fired else "Alternative"
        information += f"{action} needed for rule: {last_evaluated_rule}\n"
        corner_case = last_evaluated_rule.corner_case

    corner_row_dict = None
    if isinstance(case, SQLTable):
        case_dict = row_to_dict(case)
        if last_evaluated_rule and last_evaluated_rule.fired:
            corner_row_dict = row_to_dict(last_evaluated_rule.corner_case)
    elif is_dataclass(case):
        case_dict = dataclass_to_dict(case)
        if last_evaluated_rule and last_evaluated_rule.fired:
            corner_row_dict = dataclass_to_dict(last_evaluated_rule.corner_case)
    else:
        case_dict = copy_case(case)
        if last_evaluated_rule and last_evaluated_rule.fired:
            corner_row_dict = copy_case(corner_case)

    case_dict.update(targets)
    case_dict.update(current_conclusions)
    all_table_rows = [case_dict]
    if corner_row_dict:
        corner_conclusion = last_evaluated_rule.conclusion(case)
        corner_row_dict.update({corner_conclusion.__class__.__name__: corner_conclusion})
        all_table_rows.append(corner_row_dict)
    information += "\n" + "=" * 50 + "\n"
    information += "\n" + table_rows_as_str(all_table_rows) + "\n"
    return information
