from functools import reduce
from pydantic import BaseModel,ValidationError
from termcolor import colored
from source.excel import Excel
from model.common.errordict import error_dict

class CommonModel(BaseModel):
    # every succesor class has its own properties
    class Config:
        arbitrary_types_allowed = True
    
    # initialize the model with a list of excels, which includes all nececcery information the model required. 
    # output_excel_file is finename for generating new excel. global_dict is the dict generated by globals() from successor's class
    def __init__(self,excels,output_excel_file,global_dict,generate_excel=True):
        # add up all input excels and return the combined one
        *original_excel_objs,new_excel_obj=self._addupExcels(excels) # the last one is new_excel_obj, before this are all original_objs 
        if output_excel_file:
            data=self._generateExcel(global_dict,new_excel_obj,output_excel_file)
            if generate_excel: self.generateExcel(**data)
        else:
            #get the dict as input kwargs. Here globals() is very important. Without it, the KwargsMix cannot find required classes
            kwargs=self._getInput(global_dict,original_excel_objs,new_excel_obj)
            # use pre-assigned referrence value (kwargs) to initialize the model
            try:
                super().__init__(**kwargs)
            except ValidationError as e:
                exit(0)    # since in _getinput has capatured exceptions, here we do nothing.

    # get combined excel throgh add up all input excels. 
    def _addupExcels(self,excels):
        excel_objs=list(map(lambda x:Excel(x), excels))
        return [*excel_objs,reduce(lambda a,b:a+b, excel_objs)]
    
    """
    Prerequisite of below methods
    1. excel_obj is the one that all input excels combined, no sheet has same name
    2. Successor's model is constructed with all sheets/tables, no specific single varialbe
    3. Only InfoSheet and TableList two data structure in the successor's model properties
    """
    def _getInput(self,global_dict,original_excel_objs,new_excel_obj):

        input_kwargs={}
        # According to the prerequisite described above, all the referrenced scheme are defined in 'definitions'. 
        # key is Model name, and after lower(), it is also excel sheet name and the property defined in this class. They are same. This is the requirement.
        for k in self.schema()['definitions'].keys(): 
            # globals() returns a dict inlcuding all global classes, methods, ....
            klass=global_dict.get(k)  
            # use k.lower (it's a sheet name here) to get sheet data
            sheet_data=new_excel_obj.dict.get(k.lower(),None)   
            # only two types, one is list of model objects,another is model object
            if not isinstance(sheet_data,list):
                # assign the input kwargs. Here k.lower() is class's property. klass is the model, on which the app create an obj
                try:
                    input_kwargs[k.lower()]=klass(**sheet_data)
                except ValidationError as e:
                    sheet_name='info-'+k.lower() # sheet name
                    excel_obj=[ excel_obj for excel_obj in original_excel_objs if sheet_name in excel_obj.sheet_names][0]
                    self.interpretError(excel_obj,sheet_name,e)
            else:
                try:
                    input_kwargs[k.lower()]=[klass(**d) for d in sheet_data]
                except ValidationError as e:
                    table_name='table-'+k.lower() # sheet name
                    excel_obj=[ excel_obj for excel_obj in original_excel_objs if table_name in excel_obj.table_names][0]
                    self.interpretError(excel_obj,table_name,e)
        return input_kwargs

    # generate a new excel according to the program class's properties    
    def _generateExcel(self,global_dict,new_excel_obj,excel_file):    
        sheets=[]
        tables=[]
        # successo's referrenced class is retored in schema's definition fields with dict format.
        # 获得每一sheet对应class，并获得它的properties list
        classes=self.schema()['definitions'].keys()
        for k in classes: 
            # global_dict is a dict inlcuding all global classes, methods, ....
            klass=global_dict.get(k) # k here is actually a sheet/table name, and klass is the class related to sheet. 
            variables=list(klass.__fields__.keys()) # get all variables in the sheet/table
            sheet_table_name=k.lower() # k.lower is the sheet / table name according to convention
            if 'info-'+sheet_table_name in new_excel_obj.sheet_names:
                sheets.append(new_excel_obj.getSheet(sheet_table_name,variables)) # get sheet object 
            if 'table-'+sheet_table_name in new_excel_obj.table_names:
                tables.append(new_excel_obj.getTable(sheet_table_name,variables)) # get table object
        return {'new_excel_obj':new_excel_obj,'excel_file':excel_file,'sheets':sheets,'tables':tables}
    
    def generateExcel(self,**data):        
        # 根据最终更新了variables的sheets和talbes，生成新的excel文件
        data['new_excel_obj'].makeExcel(data['excel_file'],sheets=data['sheets'],tables=data['tables'])

    # def interpretError(self,excel_obj,sheet_name,e:ValidationError):
    #     excel_name=excel_obj.excel_name.split('/')[-1] # only get the excel file name and ignore the path
    #     print(colored(f"{excel_name}{error_dict.get('file')}{sheet_name}{error_dict.get('sheet')}{error_dict.get('has error')}，{error_dict.get('detail')}: ",'red'))
    #     for err in e.errors():
    #         for variable in err['loc']:
    #             if sheet_name.startswith('info-'):
    #                 sheet=excel_obj.getSheet(sheet_name[5:]) # remove info-
    #                 info_node=sheet.data.get(variable)
    #                 if info_node !=None: 
    #                     print(colored(f"{info_node.description}",'red'),error_dict.get(err['msg']),error_dict.get('has error'))
    #             if sheet_name.startswith('table-'):
    #                 table=excel_obj.getTable(sheet_name[6:])
    #                 title=table.column_titles.get(variable)
    #                 print(colored(f"{title}",'red'),error_dict.get('the_column'),error_dict.get('has error'),error_dict.get(err['msg'],error_dict.get('error')))        
    #     print(e.json)        
    
    def interpretError(self,excel_obj,sheet_name,e:ValidationError):
        excel_name=excel_obj.excel_name.split('/')[-1] # only get the excel file name and ignore the path
        print(colored(f"{excel_name}{error_dict.get('file')}{sheet_name}{error_dict.get('sheet')}{error_dict.get('has error')}，{error_dict.get('detail')}: ",'red'))
        for err in e.errors():
            for variable in err['loc']:
                if sheet_name.startswith('info-'):
                    sheet=excel_obj.getSheet(sheet_name[5:]) # remove info-
                    info_node=sheet.data.get(variable)
                    if info_node !=None: 
                        print(colored(f"{info_node.description}",'red'),err['msg'])
                if sheet_name.startswith('table-'):
                    table=excel_obj.getTable(sheet_name[6:])
                    title=table.column_titles.get(variable)
                    print(colored(f"{title}",'red'),'the_column has error',err['msg'])
        print(e)        
