#!/usr/bin/env python3

"""Classifier.py: Abstract class for domain name classifier"""
__author__      = "Radek Hranicky, Jan Polisensky"

from abc import abstractmethod
import re
import datetime

class Classifier():

    def __init__(self):
        """
        ! Constructor of the Classifier abstract class
        """
        self.name = ""
        self.external_requires = []
        self.external_wants = [] 
        self.classifier_wants = []
        self.classifier_requires = []
        self.external_wants_parsed = []
        self.external_requires_parsed = []
        self.final = False
        self.models_path = './classifiers/models'
        self.external_model_data = './classifiers/external_model_data'

    def getName(self):
        """
        ! Returns the name of the classifier
        @return classifier name
        """
        return self.name

    def getExternalRequires(self):
        """
        ! Returns list of data types of required external data (each from a single resolver)
        @return List of external input types, e.g. ['geo', 'dns', 'ssl_data']
        """
        return self.external_requires

    def getExternalWants(self):
        """
        ! Returns list of data types of wanted external data (each from a single resolver)
        @return List of external input types, e.g. ['geo', 'dns', 'ssl_data']
        """
        return self.external_wants

    def getClassifierWants(self):
        """
        ! List of classifier names whose outputs are necessary for this one
        @return List of classifier names, e.g. ['lexical', 'geocls']
        """
        return self.classifier_wants
    
    def getClassifierRequires(self):
        """
        ! List of classifier names whose outputs are necessary for this one
        @return List of classifier names, e.g. ['lexical', 'geocls']
        """
        return self.classifier_requires

    def isFinal(self):
        """
        ! Returns whether the classifier provides the final result (only one should)
        @return True if this is the final classifier, False otherwise
        """
        return self.final
    
    def get_input_data(self, data_block):
        
        # No input data
        if (len(self.external_requires_parsed) == 0) and(len(self.external_wants_parsed) == 0):
            return False
        
        for block in self.external_requires_parsed:
            try:
                if block[data_block] != None:
                    return block[data_block]
            except:
                pass
            
        for block in self.external_wants_parsed:
            try:
                if block[data_block] != None:
                    return block[data_block]
            except:
                pass
        
        # Data not found
        return False
    

    def regex_cnt(self, string, pattern):
        return len(re.findall(pattern+"$", string))
    
    def check_data_requirements(self, data_in):
        """
        ! Checks data requirements for the classifier
        @return False if the requirements are not met, True otherwise
        """
        requires = self.external_requires
        
        try:
            for data_block in requires:
                if data_in[data_block] is None:
                    return False
        except:
            return False
        
        return True
    
    def parse_input_data(self, external_data, return_data=False):
        """
        ! Parse and check data requirements for the classifier
        """
        self.check_data_requirements(external_data)
        
        requires = self.external_requires
        wants = self.external_wants
        
        for data_block in requires:
            
            if data_block == 'geo':
                if not isinstance(external_data[data_block], list):
                    return self.__err_handler("Geo data in Resolver are not list", 3)
            else:
                if not isinstance(external_data[data_block], dict):
                    return self.__err_handler(str(data_block) + " in Classifier are not dict", 3)            
            
            
            ### resolver specific parsing ###
            ### put here parsing of data from resolvers with specific format ###
            if data_block == "dns_whois":
                self.external_requires_parsed.append({'dns': external_data[data_block]['dns_data']})
                self.external_requires_parsed.append({'whois': external_data[data_block]['whois_data']})
            else:
                self.external_requires_parsed.append({data_block: external_data[data_block]})
                
                
        for data_block in wants:             
                
            try:
                self.external_wants_parsed.append({data_block: external_data[data_block]})      
            except Exception as e:
                pass
                # Optional data broken, no problem, they are only optional   
        
        if return_data:
            return {'requires': self.external_requires_parsed, 'wants':self.external_wants_parsed}  
        else:
            return True        
        
        
    def err_handler(self, message, code=0):

        error_message = "Clasifier " + str(self.name) + " returned error: " + str(message)

        status = {
            "success": False,
            "error_description": str(error_message),
            "badness": None,
            "accuracy": None,
            "explanation": [],
            "final" : False,
            "created" : datetime.datetime.now()
        }

        return status

    @abstractmethod
    def classify(self, domain_name, internal_data, external_data):
        """
        ! Perform the classifion of the given domain_name
        @param domain_name Domain name to classify, e.g. 'fit.vut.cz'
        @param internal_data Dictionary of dependency classifiers' outputs
        @param external_data Dictionary of external inputs, e.g.
        {
            "geo": {
                "country": "US",
                "region": "Nevada",
                "city": "Las Vegas",
                "loc": "36.1750,-115.1372",
                "org": "AS22773 Cox Communications Inc."
            },
            "dns": {
                ...
            }
            "ai_results" : {                  # (Optional) Result of previous classifiers
                "lexical-model" : 0.98
            }
        }

        external_data['dns'] = ..JSON..
        @return Returns the classification output

        Example output:
        {
            "classifier_name": "lexical"                    # Value of classifier.name
            "success": True,                                # True or False
            "error_description": '',                        # e.g. 'missing model' or 'wrong data vector'
            "badness": 0.83,                                # float (0 to 1)
            "accuracy": 0.9                                 # float (0 to 1)
            "explanation": ['suspicious cert', 'authority'] # list of strings
            "final:" False                                  # True/False, is final result
            "created": datetime.datetime                    # Timestamp of the result creation
        }
        """
        pass
