import pandas as pd
from app.rulemall.validators import *
import json

class RuleSet:

    def __init__(self, data, rule_groups=[]):
        self.data = data
        self.rule_groups = rule_groups

    def to_dict(self):
        output = []
        for rule_group in self.rule_groups:
            output.append(rule_group.to_dict())
        return output

    def from_dict(self, rule_groups):
        self.rule_groups = []
        for rule_group in rule_groups:
            rg = RulesGroup()
            rg.from_dict(rule_group)
            rg.reset_data(self.data)
            self.rule_groups.append(rg)


class RulesGroup:

    def __init__(self, name='', data=None, rules=None, filters=None, by=None):
        self.name = name
        self.data = data
        self.by = by
        self.rules = rules
        self.filters = filters

    def __add__(self, rule):
        self.rules = AndOperator([self.rules, rule])

    def reset_data(self, data=None):
        self.data = data

    def validate(self):
        if self.data is not None:
            data = self.data
            if self.filters:
                filters, _ = self.filters.validate(data)
                data = data.loc[filters]
            if self.rules:
                return self.rules.validate(data)
        return None

    def get_target(self):
        if self.rules:
            return self.rules.get_target()

    def to_dict(self):
        d = {'name': self.name, 'rules': [], 'filters': []}
        for rule in self.rules:
            d['rules'].append(rule.to_dict())
        for f in self.filters:
            d['filters'].append(f.to_dict())
        return d

    def reset_rules(self, rules):
        self.rules = None
        if 'AND' in rules:
            a = AndOperator(operands=[])
        elif 'OR' in rules:
            a = OrOperator(operands=[])
        else:
            try:
                a = Rule()
            except Exception as e:
                print(e)
                raise TypeError(str(e))
        try:
            a.from_dict(rules.copy())
        except Exception as e:
            print(e)
            raise TypeError(str(e))
        self.rules = a

    def reset_filters(self, filters):
        self.filters = None
        if 'AND' in filters:
            a = AndOperator()
        elif 'OR' in filters:
            a = OrOperator()
        else:
            a = Rule()
        try:
            a.from_dict(filters.copy())
            self.filters = a
        except Exception as e:
            print(e)
            raise TypeError(str(e))

    def from_dict(self, dct):
        self.name = dct['name']
        rules = dct['rules']
        filters = dct.get('filters')
        self.reset_rules(rules)
        try:
            self.reset_filters(filters)
        except Exception:
            self.filters = None
    # TODO: complete from config method
    # TODO: Add a method to return the names of all rules withing the object
    # TODO: Get rule applicator from factory of adaptor


class Rule:

    def __init__(self, name=None, target=None, validator=None, invert=False, orient='column'):

        self.name = name
        self.target = target
        self.validator = validator
        self.orient = orient
        self.invert = invert
        self.vdf = pd.DataFrame()

    def validate(self, s):
        if self.orient == 'column':
            if self.invert:
                self.vdf[self.name] = ~self.validator.validate(s[self.target])
                self.vdf['responsible'] = self.vdf[self.name].apply(lambda x: [] if x else [self.name])
            else:
                self.vdf[self.name] = self.validator.validate(s[self.target])
                self.vdf['responsible'] = self.vdf[self.name].apply(lambda x: [] if x else [self.name])

        elif self.orient == 'record':
            if self.invert:
                self.vdf[self.name] = ~self.validator.validate(s)
                self.vdf['responsible'] = self.vdf[self.name].apply(lambda x: [] if x else [self.name])
            else:
                self.vdf[self.name] = self.validator.validate(s)
                self.vdf['responsible'] = self.vdf[self.name].apply(lambda x: [] if x else [self.name])

        return self.vdf[self.name], self.vdf['responsible']

    def get_target(self):
        return self.target

    def to_dict(self):
        d = self.validator.to_dict()
        d['rule_attributes'] = {"name": self.name, "target": self.target, "orient": self.orient, "invert": self.invert}
        return d

    def from_dict(self, dct):
        rule_type = dct['rule_type']
        if rule_type == 'Range':
            validator = Range(**dct['validator_parameters'])
        elif rule_type == 'Regex':
            validator = Regex(**dct['validator_parameters'])
        elif rule_type == 'Length':
            validator = Length(**dct['validator_parameters'])
        elif rule_type == 'Unique':
            validator = Unique(**dct['validator_parameters'])
        elif rule_type == 'Category':
            validator = Category(**dct['validator_parameters'])
        elif rule_type == 'Compare':
            validator = Compare(**dct['validator_parameters'])
        elif rule_type == 'Empty':
            validator = Empty()
        elif rule_type == 'Logic':
            validator = Logic(**dct['validator_parameters'])
        else:
            raise NotImplementedError

        self.validator = validator
        self.name = dct['rule_attributes']['name']
        self.target = dct['rule_attributes']['target']
        self.invert = dct['rule_attributes'].get('invert', False)
        self.orient = dct['rule_attributes'].get('orient', 'column')
        if isinstance(self.invert, str):
            self.invert = json.loads(self.invert)


class OperatorBaseClass(metaclass=ABCMeta):

    def __init__(self, operands=[]):
        self.operands = operands

    @staticmethod
    def operation(data, other):
        raise NotImplementedError

    def get_target(self):
        columns = []
        for op in self.operands:
            a = op.get_target()
            if isinstance(a, list):
                columns.extend(a)
            else:
                columns.append(a)
        return columns

    def validate(self, data):
        if self.operands:
            t, r = self.operands[0].validate(data)
            for op in self.operands[1:]:
                v1, v2 = op.validate(data)
                r = pd.concat([r, v2.rename('r')], axis=1)
                t = self.operation(t, v1)
                r.loc[t, 'responsible'] = r.loc[t, 'responsible'].apply(lambda x: [])
                r.loc[~t].apply(lambda x: x['responsible'].extend(x['r']), axis=1)
                r.loc[~t, 'responsible'] = r.loc[~t, 'responsible'].apply(lambda x: list(set(x)))
                r = r['responsible']
            return t, r
        else:
            return None, None

    def _to_dict(self):
        res = []
        for op in self.operands:
            res.append(op.to_dict())
        return res

    def _from_dict(self, dct):
        for op in dct:
            if 'AND' in op:
                a = AndOperator([])
            elif 'OR' in op:
                a = OrOperator([])
            else:
                try:
                    a = Rule()
                except Exception as e:
                    print(e)
                    raise TypeError('Not a Rule!')
            a.from_dict(op.copy())
            self.operands.append(a)


class OrOperator(OperatorBaseClass):

    @staticmethod
    def operation(data, other):
        return data | other

    def to_dict(self):
        return {'OR': self._to_dict()}

    def from_dict(self, dct):
        dct = dct['OR'].copy()
        self._from_dict(dct)


class AndOperator(OperatorBaseClass):

    @staticmethod
    def operation(data, other):
        return data & other

    def to_dict(self):
        return {'AND': self._to_dict()}

    def from_dict(self, dct):
        dct = dct['AND'].copy()
        self._from_dict(dct)
