import warnings
import multiprocessing

import cobra
from cobra.flux_analysis.gapfilling import GapFiller

from ..commons.fluxbal import fba_no_warnings
from ..commons.fluxbal import get_optthr



def add_demand(model, mid):
    
    
    rid = f"demand_{mid}"
    newr = cobra.Reaction(rid)
    model.add_reactions([newr])
    model.reactions.get_by_id(rid).reaction = f"{mid} -->"
    model.reactions.get_by_id(rid).bounds = (0, 1000)
    
    return rid



def can_synth(model, mid):
    
    
    # changes are temporary: demand is not added, objective is not changed.
    with model: 

        rid = add_demand(model, mid)

        # set the objective to this demand reaction:
        model.objective = rid

        # perform FBA: 
        res, value, status = fba_no_warnings(model)
        response = True if (value > get_optthr() and status == 'optimal') else False
        
        return response, round(value, 2), status



def perform_gapfilling(model, universe, mid=None, slim=None, minflux=1, exr=False, nsol=3, penalties=None, verbose=True, timeout=None, logger=None, boost=False): 

    
    def temporized_task(model, universe, minflux, exr, nsol, penalties):

        # launch cobrapy gapfilling: 
        gapfiller = GapFiller(model, universe, 
            lower_bound = minflux,
            demand_reactions = False, 
            exchange_reactions = exr, 
            integer_threshold = 1e-10,  # default: 1e-6.
            penalties = penalties,
        )
        try: solutions = gapfiller.fill(iterations=nsol)
        except Exception as e: 
            return e
        
        return solutions
            
       
    # temporary changes (objective, solver, demands edits are temporary)
    # remove genes to avoid errors due to the context restoration (eg "ValueError: id {gene_id} is already present in list")
    model_nogenes = model.copy()
    cobra.manipulation.remove_genes(model_nogenes, [g for g in model_nogenes.genes], remove_reactions=False)
    universe_nogenes = universe.copy()
    cobra.manipulation.remove_genes(universe_nogenes, [g for g in universe_nogenes.genes], remove_reactions=False)
    

    # if focusing on a particular biosynthesis
    if mid != None:
        model_nogenes.objective = add_demand(model_nogenes, mid)
        universe_nogenes.objective = add_demand(universe_nogenes, mid)


    # if requested, boost the actual sources:
    if boost:
        for r in model_nogenes.reactions: 
            if len(r.metabolites)==1 and list(r.metabolites)[0].id.endswith('_e'): # if exchange reaction
                if r.lower_bound < 0: 
                    r.lower_bound = -1000


    # "AssertionError: daemonic processes are not allowed to have children" could be raised since
    # child processes created by multiprocessing.Pool are demoniac by deafault,
    # and demoniac processes cannot create their own child processes like the temporized gap-filler. 
    if timeout != None: 


        # run the gapfilling algorithm inside a timer: 
        # first define the worker for the separate process
        def worker(results_channel):
            result = temporized_task(model_nogenes, universe_nogenes, minflux, exr, nsol, penalties)
            results_channel.put(result)


        # then start the separate process
        results_channel = multiprocessing.Queue()
        process = multiprocessing.Process(target=worker, args=(results_channel,))
        process.start()  # start the process
        process.join(timeout) # wait for the process to finish or timeout


        # if still running after the timeout:
        if process.is_alive():  
            process.terminate()
            e = "Gap-filling timed out, consider to increase the 'timeout' attribute."
            if verbose:  # avoid the error stack trace
                print("WARNING: cobrapy:", e) if logger==None else logger.warning(f'cobrapy: {e}')  
            return None


        else: # the worker finished: retrieve the result or exception from the queue:
            result = results_channel.get()
            if isinstance(result, Exception):  
                if verbose: 
                    print("WARNING: cobrapy:", result) if logger==None else logger.warning(f'cobrapy: {result}')  
                return None  
            else:  # yielded a true result, not an Exception
                solutions = result


    else:  # do not temporize the gap-filling:
        result = temporized_task(model_nogenes, universe_nogenes, minflux, exr, nsol, penalties)
        if isinstance(result, Exception):  
            if verbose: 
                print("WARNING: cobrapy:", result) if logger==None else logger.warning(f'cobrapy: {result}')
            return None
        else:  # yielded a true result, not an Exception
            solutions = result


    # iterate the solutions:
    first_sol_rids = []  # rids proposed during the 1st solution
    for i, solution in enumerate(solutions):
        if verbose and logger==None: print(f'Solution {i+1}. Reactions to add: {len(solution)}.')


        # iterate the reactions: 
        counter = 0
        for r in solution: 
            counter += 1
            # note: this 'r' is not linked to any model. 
            # indeed, calling r.model, None will be returned. 
            if verbose and logger==None: print(f'{counter} {r.id} {r.name}')

            
            # populate results with IDs from first solution:
            if i == 0: first_sol_rids.append(r.id)

            
        # separate solutions with a new line:
        if i+1 != len(solutions): 
            if verbose and logger==None: print()


    return first_sol_rids



def import_from_universe(model, universe, rid, bounds=None, gpr=None):

    
    # get the universal reaction
    ru = universe.reactions.get_by_id(rid)
    
    # create a new empty reaction
    r = cobra.Reaction(rid)
    model.add_reactions([r])
    r = model.reactions.get_by_id(rid)
    
    # copy the name
    r.name = ru.name
    
    # build string all universal metabolites are still there
    # (remove__disconnected is called later):
    r.build_reaction_from_string(ru.reaction)
    
    # set bounds
    if bounds != None: r.bounds = bounds
    else: r.bounds = ru.bounds
        
    # set GPR
    if gpr != None:
        r.gene_reaction_rule = gpr
    else:
        r.gene_reaction_rule = ''
    r.update_genes_from_gpr()
    
    # set annotations
    r.annotation = ru.annotation



def include_forced(logger, model, universe, force_inclusion):
    
    
    # get rids provided by user:
    if force_inclusion != '-':
        introduced_rids = []
        forced_rids = force_inclusion.split(',')
        
        
        # get rids already modeled
        modeled_rids = [r.id for r in model.reactions]
        universal_rids = [r.id for r in universe.reactions]
        
        
        # skip reactions not modeled in whole universe
        for rid in forced_rids: 
            if rid not in universal_rids:
                logger.debug(f"Ignoring reaction '{rid}' since it's not included in the universe.")
                continue
            
            
            # otherwise add the universal reaction
            if rid not in modeled_rids:
                import_from_universe(model, universe, rid, gpr='')
                introduced_rids.append(rid)
                logger.warning(f"Reaction '{rid}' forcibly included as orphan.")
            else:
                logger.debug(f"Requested reaction '{rid}' was already included.")
        logger.info(f"Forcibly included {len(introduced_rids)} reactions as orphans.")
        
        

def get_repository_nogenes(logger, universe):
    # provide a gene-free repository of reactions to be used during gap-filling


    # make an editable copy:
    repository_nogenes = universe.copy()
    
        
    # remove all genes to avoid the "ValueError: id purP is already present in list"
    cobra.manipulation.delete.remove_genes(repository_nogenes, [g.id for g in repository_nogenes.genes], remove_reactions=False)

    
    return repository_nogenes




    
    

