import argparse
import sys
import multiprocessing 
import logging 
from logging.handlers import QueueHandler
import traceback
import importlib.metadata
from datetime import datetime


from .getmaps import getmaps_command
from .parsedb import parsedb_command
from .mkmodel import mkmodel_command
from .runsims import runsims_command


import cobra


cobra_config = cobra.Configuration()
solver_name = str(cobra_config.solver.log).split(' ')[1]
solver_name = solver_name.replace("optlang.", '')
solver_name = solver_name.replace("_interface", '')


def main():
    
    
    # define the header of main- and sub-commands. 
    header = f'gsrap v{importlib.metadata.metadata("gsrap")["Version"]},\ndeveloped by Gioele Lazzari (gioele.lazzari@univr.it).'
    
    
    # create the command line arguments:
    parser = argparse.ArgumentParser(description=header, add_help=False)
    parser.add_argument("-h", "--help", action="help", help="Show this help message and exit.")
    parser.add_argument("-v", "--version", action="version", version=f"v{importlib.metadata.metadata('gsrap')['Version']}", help="Show version number and exit.")
    subparsers = parser.add_subparsers(title='gsrap subcommands', dest='subcommand', help='', required=True)
    
    
    # create the 3 subparsers:
    getmaps_parser = subparsers.add_parser('getmaps', description=header, help="Create 'gsrap.maps' by retrieving metabolic information from metabolic databases.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False)
    parsedb_parser = subparsers.add_parser('parsedb', description=header, help="Parse the online database to create the universe GSMM. Requires 'gsrap.maps'.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False)
    mkmodel_parser = subparsers.add_parser('mkmodel', description=header, help='Reconstruct a GSMM from the universe and a functional annotation.', formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False)
    runsims_parser = subparsers.add_parser('runsims', description=header, help='Analyze a GSMM previously built.', formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False)

    
    # add arguments for the 'getmaps' command
    getmaps_parser.add_argument("-h", "--help", action="help", help="Show this help message and exit.")
    getmaps_parser.add_argument("-v", "--version", action="version", version=f"v{importlib.metadata.metadata('gsrap')['Version']}", help="Show version number and exit.")
    getmaps_parser.add_argument("--verbose", action='store_true', help="Make stdout messages more verbose, including debug messages.")
    getmaps_parser.add_argument("-o", "--outdir", metavar='', type=str, default='./', help="Main output directory (will be created if not existing).")
    getmaps_parser.add_argument("--keeptmp", action='store_true', help="Do not delete temporary raw files.")
    getmaps_parser.add_argument("--usecache", action='store_true', help="Do not update previously downloaded records (save time).")
    
    
    # add arguments for the 'parsedb' command
    parsedb_parser.add_argument("-h", "--help", action="help", help="Show this help message and exit.")
    parsedb_parser.add_argument("-v", "--version", action="version", version=f"v{importlib.metadata.metadata('gsrap')['Version']}", help="Show version number and exit.")
    parsedb_parser.add_argument("--verbose", action='store_true', help="Make stdout messages more verbose, including debug messages.")
    parsedb_parser.add_argument("-o", "--outdir", metavar='', type=str, default='./', help="Main output directory (will be created if not existing).")
    parsedb_parser.add_argument("-i", "--inmaps", metavar='', type=str, default='./gsrap.maps', help="Input file 'gsrap.maps' previously produced using the 'getmaps' subcommand.")
    parsedb_parser.add_argument("-p", "--progress", action='store_true', help="Show progress for each map.")
    parsedb_parser.add_argument("-m", "--module", action='store_true', help="Show progress for each module of each map (use only with --progress).")
    parsedb_parser.add_argument("-f", "--focus", metavar='', type=str, default='-', help="Focus on a particular map/module (use only with --progress).")
    parsedb_parser.add_argument("-g", "--growth", action='store_true', help="Test growth on a minimal medium.")
    parsedb_parser.add_argument("-b", "--biosynth", metavar='', type=str, default='-',  help="Test biosynthesis of metabolites.")
    parsedb_parser.add_argument("-e", "--eggnog", metavar='', type=str, default='-', help="Path to the optional eggnog-mapper annotation table.")
    parsedb_parser.add_argument("-z", "--zeroes", action='store_true', help="Show maps/modules with 0%% coverage, in addition to partials (use only with --progress).")
    parsedb_parser.add_argument("-u", "--goodbefore", metavar='', type=str, default='-', help="Syntax is {pure_mid}-{rid1}-{rid2}. From top to bottom, build the universe until reaction {rid1}, transport {rid2} and metabolite {pure_mid} are reached.")
    parsedb_parser.add_argument("-a", "--onlyauthor", metavar='', type=str, default='-', help="Build the universe by parsing contents of the specified author ID only. Contents affected by --goodbefore are parsed anyway.")
    
    
    # add arguments for the 'mkmodel' command
    mkmodel_parser.add_argument("-h", "--help", action="help", help="Show this help message and exit.")
    mkmodel_parser.add_argument("-v", "--version", action="version", version=f"v{importlib.metadata.metadata('gsrap')['Version']}", help="Show version number and exit.")
    mkmodel_parser.add_argument("--verbose", action='store_true', help="Make stdout messages more verbose, including debug messages.")
    mkmodel_parser.add_argument("-o", "--outdir", metavar='', type=str, default='./', help="Main output directory (will be created if not existing).")
    mkmodel_parser.add_argument("-e", "--eggnog", metavar='', type=str, default='-', help="Path to the eggnog-mapper annotation table.")
    mkmodel_parser.add_argument("-u", "--universe", metavar='', type=str, default='-', help="Path to the universe model (SBML format).")
    mkmodel_parser.add_argument("-i", "--force_inclusion", metavar='', type=str, default='-', help="Force the inclusion of the specified reactions (comma-separated IDs).")
    mkmodel_parser.add_argument("-f", "--gap_fill", metavar='', type=str, default='-', help="Media to use during gap-filling (comma-separated IDs); if not provided, gap-filling will be skipped.")
    mkmodel_parser.add_argument("-r", "--reference", metavar='', type=str, default='-', help="Use the specified model for gap-filling, instead of the universe.")
    mkmodel_parser.add_argument("-x", "--exclude_orphans", action='store_true', help="Exclude orphan reactions from the gap-filling repository.")
    mkmodel_parser.add_argument("-b", "--biolog", metavar='', type=str, default='-', help="Strain ID associated to binary Biolog(R) PM1, PM2A, PM3B and PM4A plates; if not provided, Biolog(R)-based model curation will be skipped (use with --cnps and --gap_fill).")
    mkmodel_parser.add_argument("--suggest", metavar='', type=str, default='-', help="Suggest missing reactions to synthetize the specified metabolites (comma-separated IDs) (use with --growth).")
    

    # add arguments for the 'runsims' command
    runsims_parser.add_argument("-h", "--help", action="help", help="Show this help message and exit.")
    runsims_parser.add_argument("-v", "--version", action="version", version=f"v{importlib.metadata.metadata('gsrap')['Version']}", help="Show version number and exit.")
    runsims_parser.add_argument("--verbose", action='store_true', help="Make stdout messages more verbose, including debug messages.")
    runsims_parser.add_argument("-o", "--outdir", metavar='', type=str, default='./', help="Main output directory (will be created if not existing).")
    runsims_parser.add_argument("-i", "--inmodel", metavar='', type=str, default='-', help="Input strain-specific GSMM.")
    runsims_parser.add_argument("-g", "--fba", "--growth", metavar='', type=str, default='-', help="Media to use during growth simulations (comma-separated IDs); if not provided, growth simulations will be skipped.")
    runsims_parser.add_argument("--fva", action='store_true', help="Perform FVA during growth simulations (use with --growth).")
    runsims_parser.add_argument("--synth", action='store_true', help="Test metabolites' synthesis on media (use with --growth).")
    runsims_parser.add_argument("-s", "--cnps", metavar='', type=str, default='-', help="Starting C, N, P and S source metabolites (comma-separated IDs); if not provided, anternative substrate analysis will be skipped.")
    runsims_parser.add_argument("-b", "--biolog", metavar='', type=str, default='-', help="Strain ID associated to binary Biolog(R) PM1, PM2A, PM3B and PM4A plates; if not provided, Biolog(R)-based model curation will be skipped (use with --cnps and --gap_fill).")
    runsims_parser.add_argument("--omission", action='store_true', help="Perform single omission experiments to study auxotrophies (use with --growth).")
    runsims_parser.add_argument("--essential", action='store_true', help="Predict essential genes (single-gene knock-out simulations) (use with --growth).")
    runsims_parser.add_argument("--factors", action='store_true', help="Predict putative growth factors (use with --growth).")

    

    # check the inputted subcommand, automatic sys.exit(1) if a bad subprogram was specied. 
    args = parser.parse_args()
    
    
    # set the multiprocessing context
    multiprocessing.set_start_method('fork') 
    
    
    # create a logging queue in a dedicated process.
    def logger_process_target(queue):
        logger = logging.getLogger('gsrap')
        while True:
            message = queue.get() # block until a new message arrives
            if message is None: # sentinel message to exit the loop
                break
            logger.handle(message)
    queue = multiprocessing.Queue()
    logger_process = multiprocessing.Process(target=logger_process_target, args=(queue,))
    logger_process.start()
    
    
    # connect the logger for this (main) process: 
    logger = logging.getLogger('gsrap')
    logger.addHandler(QueueHandler(queue))
    if args.verbose: logger.setLevel(logging.DEBUG) # debug (lvl 10) and up
    else: logger.setLevel(logging.INFO) # debug (lvl 20) and up
    
    
    # handy function to print without time/level (for header / trailer)
    def set_header_trailer_formatter(logger):
        formatter = logging.Formatter('%(message)s')
        handler = logging.StreamHandler()
        handler.setFormatter(formatter)
        logger.addHandler(handler)
        return handler
    
    
    # to print the main pipeline logging:
    def set_usual_formatter(logger):
        formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt="%H:%M:%S")
        handler = logging.StreamHandler()
        handler.setFormatter(formatter)
        logger.addHandler(handler)
        return handler
    
    
    
    # show a welcome message:
    thf_handler = set_header_trailer_formatter(logger)
    logger.info(header + '\n')
    command_line = '' # print the full command line:
    for arg, value in vars(args).items():
        if arg == 'subcommand': command_line = command_line + f"gsrap {value} "
        else: command_line = command_line + f"--{arg} {value} "
    logger.info('Inputted command line: "' + command_line.rstrip() + '".\n')
    logger.removeHandler(thf_handler)
    
    
    
    usual_handler = set_usual_formatter(logger)
    current_date_time = datetime.now()
    formatted_date = current_date_time.strftime("%Y-%m-%d")
    logger.info(f"Welcome to 'gsrap {args.subcommand}'! Launching the tool on {formatted_date}...")
    logger.debug(f'COBRApy started with solver: {solver_name}.')
    try: 
        # choose which subcommand to lauch: 
        if args.subcommand == 'getmaps':
            response = getmaps_command(args, logger)
        if args.subcommand == 'parsedb':
            response = parsedb_command(args, logger)
        if args.subcommand == 'mkmodel':
            response = mkmodel_command(args, logger)
        if args.subcommand == 'runsims':
            response = runsims_command(args, logger)
            
        if response == 0:
            logger.info(f"'gsrap {args.subcommand}' terminated without errors!")
    except: 
        # show the error stack trace for this un-handled error: 
        response = 1
        logger.error(traceback.format_exc())
    logger.removeHandler(usual_handler)


    
    # Terminate the program:
    thf_handler = set_header_trailer_formatter(logger)
    if response == 1: 
        queue.put(None) # send the sentinel message
        logger_process.join() # wait for all logs to be digested
        sys.exit(1)
    else: 
        # show a bye message
        queue.put(None) # send the sentinel message
        logger_process.join() # wait for all logs to be digested
        logger.info('\n' + header)
        sys.exit(0) # exit without errors
        
        
        
if __name__ == "__main__":
    main()