from functools import cache
import enum
from logging import getLogger

from .config import Config
from .meta_trader import MetaTrader

logger = getLogger(__name__)


class Base:
    """A base class for all data model classes in the aiomql package.
    This class provides a set of common methods and attributes for all data model classes.
    For the data model classes attributes are annotated on the class body and are set as object attributes when the
    class is instantiated.
    """
    mt5: MetaTrader
    config: Config

    def __init__(self, **kwargs):
        """
        Initialize a new instance of the Base class
        Args:
            **kwargs: Object attributes and values as keyword arguments. Only added if they are annotated on the class body.
        """
        self.config = Config()
        self.mt5 = MetaTrader()
        self.exclude = {'mt5', "config", 'exclude', 'include', 'annotations', 'class_vars', 'dict'}
        self.include = set()
        self.set_attributes(**kwargs)

    def __repr__(self):
        kv = [(k, v) for k, v in self.__dict__.items() if not k.startswith('_') and
              (type(v) in (int, float, str) or isinstance(v, enum.Enum))]
        args = (', '.join('%s=%s' % (i, j) for i, j in kv[:3]))
        args = args if len(kv) <= 3 else args + ' ... ' + ', '.join('%s=%s' % (i, j) for i, j in kv[-1:])
        return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': args}

    def set_attributes(self, **kwargs):
        """Set keyword arguments as object attributes
        
        Keyword Args:
            **kwargs: Object attributes and values as keyword arguments

        Raises:
            AttributeError: When assigning an attribute that does not belong to the class or any parent class

        Notes:
            Only sets attributes that have been annotated on the class body.
        """
        for i, j in kwargs.items():
            try:
                setattr(self, i, self.annotations[i](j))
            except KeyError:
                logger.warning(f"Attribute {i} does not belong to class {self.__class__.__name__}")
                continue

            except ValueError:
                logger.warning(f'Cannot covert object of type {type(j)} to type {self.annotations[i]}')
                continue

            except Exception as exe:
                logger.warning(f'Did not set attribute {i} on class {self.__class__.__name__} due to {exe}')
                continue

    @property
    @cache
    def annotations(self) -> dict:
        """Class annotations from all ancestor classes and the current class.

        Returns:
            dict: A dictionary of class annotations
        """
        annots = {}
        for base in self.__class__.__mro__[-3::-1]:
            annots |= getattr(base, '__annotations__', {})
        return annots

    def get_dict(self, exclude: set = None, include: set = None) -> dict:
        """Returns class attributes as a dict, with the ability to filter

        Keyword Args:
            exclude: A set of attributes to be excluded
            include: Specific attributes to be returned

        Returns:
             dict: A dictionary of specified class attributes

        Notes:
            You can only set either of include or exclude. If you set both, include will take precedence
        """
        exclude, include = exclude or set(), include or set()
        filter_ = include or set(self.dict.keys()).difference(exclude)
        return {key: value for key, value in self.dict.items() if key in filter_}

    @property
    @cache
    def class_vars(self):
        """Annotated class attributes

        Returns:
            dict: A dictionary of available class attributes in all ancestor classes and the current class.
        """
        clss = self.__class__.__mro__[-3::-1]
        cls_dict = {}
        for cls in clss:
            cls_dict |= cls.__dict__
        return {key: value for key, value in cls_dict.items() if key in self.annotations}

    @property
    def dict(self) -> dict:
        """All instance and class attributes as a dictionary, except those excluded in the Meta class.

        Returns:
            dict: A dictionary of instance and class attributes
        """
        try:
            _filter = self.exclude.difference(self.include)
            return {key: value for key, value in (self.class_vars | self.__dict__).items() if
                    key not in _filter}
        except Exception as err:
            logger.warning(err)