import warnings
from decimal import Decimal

import numpy as np
import pandas as pnd
import cobra

from .coeffs import get_uni_biom_coeff
from .coeffs import check_exp_biomass_data
from .coeffs import compute_exp_DNA_coeffs
from .coeffs import compute_exp_RNA_coeffs
from .coeffs import compute_exp_PROTS_coeffs
from .coeffs import compute_exp_LIPIDS_coeffs
from .coeffs import compute_exp_WTA_coeffs
from .coeffs import compute_exp_LTA_coeffs
from .coeffs import compute_exp_PEPTIDO_coeffs
from .coeffs import compute_exp_EPS_coeffs



def get_biomass_dict(logger, universe, dbuni):
    
    
    # iterate table rows:
    universal_mids = [m.id for m in universe.metabolites]
    B = dbuni['B'].set_index('pure_mid', drop=True, verify_integrity=True)
    class_to_precursors = dict()
    for puremid, row in B.iterrows():
        classs = row['class']  # class is reserved 
        
        
        # check if precursors are modeled in the universe:
        if f"{puremid}_c" not in universal_mids:
            logger.error(f"Metabolite '{puremid}', indicated in '{classs}', is not present in universe.")
            return 1
        
        
        # populate dic
        if classs not in class_to_precursors.keys():
            class_to_precursors[classs] = set()
        class_to_precursors[classs].add(f"{puremid}_c")
    
                                        
    # Cofactors:
    # Note: universal ('cofs_uni_Xavier2017') and conditional ('cofs_cond_Xavier2017') cofactors have been defined in Xavier2017 .
    # Here presented in the same order of Xavier2017.
    # Cofactors not mentioned in Xavier2017 ('cofactors_add') are reported below.
    # Xavier2017: 10.1016/j.ymben.2016.12.002.
    
    # 'f4200_c' is coenzyme f420, electron transfer in methanogens, actinobacteria , and others-
    # 'f4200_c', # B2: included by Xavier2017 in 'universal' but the description seems conditional.
    # 'ACP_c', acyl-carrier protein (removed as it contains the 'X' atom, causing error in Memote)                                    
                                        
    return class_to_precursors



def get_precursors_to_pathway(logger, dbuni):
    
    
    B = dbuni['B'].set_index('pure_mid', drop=True, verify_integrity=True)
    precursors_to_pathway = dict()
    for puremid, row in B.iterrows():
        
        
        # check evaluation aprameter
        evaluation = row['evaluation']  # class is reserved 
        allowded = ['universal', 'experimental', 'completeness']
        if evaluation not in allowded:
            logger.error(f"Unrecognized evaluation field for '{puremid}'. Alloweded values are {allowded}.")
            return 1
        
        
        # parse only if 'completeness'
        if evaluation == 'universal': continue
        if evaluation == 'experimental': continue
        
        
        # populate dict
        groups = [i.strip() for i in row['bs_groups'].strip().split(';')]
        precursors_to_pathway[f'{puremid}_c'] = groups
    
    
    return precursors_to_pathway



def introduce_universal_biomass(logger, universe, dbuni): 
    
    
    logger.debug("Introducing universal biomass reaction...")
    
    
    # biold universal biomass reaction string with default coefficients:
    biomass_dict = get_biomass_dict(logger, universe, dbuni)
    rstring = ''
    for classs, precursors in biomass_dict.items():
        rstring = rstring + f'{get_uni_biom_coeff()} {(" + " + str(get_uni_biom_coeff()) + " ").join(precursors)} + '
    rstring = rstring + f'{get_uni_biom_coeff()} atp_c + {get_uni_biom_coeff()} h2o_c --> {get_uni_biom_coeff()} adp_c + {get_uni_biom_coeff()} h_c + {get_uni_biom_coeff()} pi_c'

    
    # convert to a proper cobra.Reaction object
    r = cobra.Reaction('Biomass')
    universe.add_reactions([r])
    r = universe.reactions.get_by_id('Biomass')
    r.name = 'Biomass assembly reaction'
    r.build_reaction_from_string(rstring)
    
    
    # add SBO annotation
    r.annotation['sbo'] = ['SBO:0000629']  # biomass reaction 
    
    
    # set as objective:
    universe.objective = 'Biomass'
    
    
    return universe



def adjust_biomass_precursors(logger, model, universe, conditional_threshold, dbuni):
    
    
    # 'cond_col_dict' is a dictionary of variable biomass precursors
    cond_col_dict = {}
    modeled_rids = [r.id for r in model.reactions]
    cnt_removed = 0

    
    # get and iterate 'precursors_to_pathway'
    precursor_to_pathway = get_precursors_to_pathway(logger, dbuni)
    if type(precursor_to_pathway) == int: return 1
    for precursor, pathways in precursor_to_pathway.items(): 
        
        
        # iterate alternative pathways for this biomass precursor:
        pathway_to_absence = {}
        pathway_to_compstring = {}   # completeness string
        for pathway in pathways:   
            cnt_members_tot = 0
            cnt_members_present = 0

            
            # continue if the pathway is still lmissing form the universe: 
            if pathway not in [gr.id for gr in universe.groups]:
                continue   
                
                
            # count group members in this strain
            for member in universe.groups.get_by_id(pathway).members:
                cnt_members_tot += 1    
                if member.id in modeled_rids:
                    cnt_members_present += 1
                    
                    
            # populate dicts:
            pathway_to_absence[pathway] = (cnt_members_present / cnt_members_tot) < conditional_threshold
            pathway_to_compstring[pathway] = f'{pathway}: {cnt_members_present}/{cnt_members_tot}'
            

        cond_col_dict[precursor] = '; '.join(list(pathway_to_compstring.values()))
        if all(list(pathway_to_absence.values())):
            cnt_removed += 1
            # add metabolites to the right side (they will disappear if the balance is 0)
            model.reactions.Biomass.add_metabolites({precursor: get_uni_biom_coeff()})
            logger.warning(f"Removed biomass precursor '{precursor}' as it seemed not required ({cond_col_dict[precursor]}).")

       
    if model.id != universe.id:   # avoid logging when testing blocked biomass precusors of the universe.
        logger.info(f"Removed {cnt_removed} conditional biomass precursors.")
        
        
    return cond_col_dict



def compute_biomass_norm(logger, model, biomass): 
    
    
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        # avoids warnings like "UserWarning: 9.823 is not an integer (in formula C9.823H5.5789999999999935N3.6769999999999996O3.628P1.0)"
    
    
        norm = Decimal(0.0)
        for m, coeff in model.reactions.get_by_id('Biomass').metabolites.items():
            if coeff < 0.0:   # reactant
                if m.id not in ["atp_c", "h2o_c"]:
                    # '/1000' as they are milli-model coefficients:
                    norm = norm + Decimal(abs(coeff)) / 1000 * Decimal(m.formula_weight) 
        logger.info(f"Biomass adjusted using '{biomass}' data: {str(round(norm, 16))} gDW.")



def adjust_biomass_coefficients(logger, model, universe, dbuni, dbexp, biomass):
    # Strategy: the default coefficients are equal to 'get_uni_biom_coeff()', which is  a really samm number, negligible.
    # Coefficients are adjusted only for precursors comprised by MWF, all the others are left with default coefficients.
    # Therefore, if the final biomass is not sufficiently close to 1.0, just lower the 'get_uni_biom_coeff()', 
    # or lower the number of precursors left with default coefficient. 
    
    
    if biomass == '-':
        logger.info(f"No biomass data provided: using minimal coefficients ({get_uni_biom_coeff()}).")
        return 0
    
    
    # check biomass data
    response = check_exp_biomass_data(logger, dbexp, biomass)
    if type(response)==int: return 1
    else: MWF, DNA, RNA, PROTS, LIPIDS_PL, LIPIDS_FA = response
    
    
    # compute coefficient for 'Biomass' reaction:
    biomass_dict = get_biomass_dict(logger, universe, dbuni)
    reacs_prods_coeffs = dict()
    
    
    # the reaction 'MEANFA' will replace 'sn_meanfa_c' to produce 'meanfa_c'.
    model.remove_reactions([model.reactions.get_by_id("sn_meanfa_c")])
    logger.debug(f"Removed 'sn_meanfa_c': will be replaced by 'MEANFA'.")
    
    
    # DNA
    response = compute_exp_DNA_coeffs(logger, model, MWF, DNA)
    if type(response)==int: return 1
    else: reacs_prods_coeffs['DNA'] = response
    # RNA
    response = compute_exp_RNA_coeffs(logger, model, MWF, RNA)
    if type(response)==int: return 1
    else: reacs_prods_coeffs['RNA'] = response
    # PROTS
    response = compute_exp_PROTS_coeffs(logger, model, MWF, PROTS)
    if type(response)==int: return 1
    else: reacs_prods_coeffs['PROTS'] = response
    # LIPIDS
    response = compute_exp_LIPIDS_coeffs(logger, model, MWF, LIPIDS_PL, LIPIDS_FA)
    if type(response)==int: return 1
    else: reacs_prods_coeffs['LIPIDS'] = response 
    # WTA
    response = compute_exp_WTA_coeffs(logger, model, biomass_dict, MWF, dbexp)    
    if type(response)==int: return 1
    else: reacs_prods_coeffs['WTA'] = response
    # LTA
    response = compute_exp_LTA_coeffs(logger, model, biomass_dict, MWF, dbexp)
    if type(response)==int: return 1
    else: reacs_prods_coeffs['LTA'] = response
    # PEPTIDO
    response = compute_exp_PEPTIDO_coeffs(logger, model, biomass_dict, MWF, dbexp)
    if type(response)==int: return 1
    else: reacs_prods_coeffs['PEPTIDO'] = response
    # EPS
    response = compute_exp_EPS_coeffs(logger, model, biomass_dict, MWF, dbexp)
    if type(response)==int: return 1
    else: reacs_prods_coeffs['EPS'] = response  
    
    
    # merge dicts of reactants and products: 
    reactant_to_coeffs = dict()
    product_to_coeffs = dict()
    for i in reacs_prods_coeffs.values():
        reacs, prods = i[0], i[1]
        for key, coeff in reacs.items():
            if key not in reactant_to_coeffs.keys():
                reactant_to_coeffs[key] = Decimal('0.0')
            reactant_to_coeffs[key] = reactant_to_coeffs[key] + coeff
        for key, coeff in prods.items():
            if key in ['DNA_c', 'RNA_c', 'PROTS_c', 'LIPIDS_c']:
                continue
            if key not in product_to_coeffs.keys():
                product_to_coeffs[key] = Decimal('0.0')
            product_to_coeffs[key] = product_to_coeffs[key] + coeff
    
    
    # update biomass coefficients
    for mid in [m.id for m in model.reactions.get_by_id('Biomass').reactants]:
        if mid in reactant_to_coeffs.keys(): 
            model.reactions.Biomass.add_metabolites({mid: get_uni_biom_coeff()})
            model.reactions.Biomass.add_metabolites({mid: -float(reactant_to_coeffs[mid])})
    for mid in [m.id for m in model.reactions.get_by_id('Biomass').products]:
        if mid in product_to_coeffs.keys(): 
            model.reactions.Biomass.add_metabolites({mid: -get_uni_biom_coeff()})
            model.reactions.Biomass.add_metabolites({mid: float(product_to_coeffs[mid])})
            
            
    # remove unsed biomass precursors:
    biomass_dict = get_biomass_dict(logger, universe, dbuni)
    # remove all fatty acids:
    for mid in biomass_dict["LIPIDS_FA"]:
        model.reactions.Biomass.add_metabolites({mid: get_uni_biom_coeff()})
    # remove unused phospholipids:
    for mid in biomass_dict["LIPIDS_PL"]:
        if mid not in reacs_prods_coeffs['LIPIDS'][0].keys():
            model.reactions.Biomass.add_metabolites({mid: get_uni_biom_coeff()})
            
            
    # check normalization to 1.0 gDW
    compute_biomass_norm(logger, model, biomass)
            
    
    return 0



def get_biomass_df_structure(logger, df, model, universe, dbuni, cond_col_dict):
    
    
    # get strain-specific biomass precursors / coeffs to gap-fill: 
    biomass_mids_to_coeff = dict()
    for m, coeff in model.reactions.Biomass.metabolites.items():
        if coeff < 0:   # reactants
            biomass_mids_to_coeff[m.id] = abs(coeff)
            
            
    # get all universal biomass precursors: 
    biomass_dict = get_biomass_dict(logger, universe, dbuni)
    biomass_mids = []
    for key, values in biomass_dict.items():
        for i in values:
            biomass_mids.append(i)
            
    
    # preassign class to each precursor  (besically invert 'biomass_dict')
    precursor_to_class = dict()
    for classs, mids in biomass_dict.items():  # class is protected keyword
        for mid in mids: 
            precursor_to_class[mid] = classs
    precursor_to_class['atp_c'] = '/'
    
    
    # define universal biomass precursors (atp in 1st position)
    universal_precs = [m.id for m in universe.reactions.Biomass.reactants if m.id not in ['atp_c', 'h2o_c']]
    universal_precs = ['atp_c'] + universal_precs
    
    
    # prepare sheet for excel output
    df['name'] = None
    df['class'] = None
    df['cond'] = None
    df['coeff'] = None
    for mid in universal_precs:
        
        
        # skip if this strain does not produce particular PL/FA 
        if mid in biomass_mids and not mid in biomass_mids_to_coeff.keys():
            if mid in biomass_dict['LIPIDS_PL'] or mid in biomass_dict['LIPIDS_FA']:
                continue    
        
        
        # set base info 
        df.loc[mid, 'name'] = universe.metabolites.get_by_id(mid).name
        df.loc[mid, 'class'] = precursor_to_class[mid]
        
        
        # set 'cond' column:
        if mid in cond_col_dict.keys():
            df.loc[mid, 'cond'] = f'{cond_col_dict[mid]}'
        else:
            df.loc[mid, 'cond'] = '/'
            
            
        # set 'coeff' column:
        if mid not in [m.id for m in model.reactions.Biomass.reactants]:  
                df.loc[mid, 'coeff'] = 'removed'
        else:
            df.loc[mid, 'coeff'] = biomass_mids_to_coeff[mid]
            
            
    return df


