#!/usr/bin/env python3

print('''
parse_json.py - a script from the AQUAMIS pipeline:
https://gitlab.com/bfr_bioinformatics/AQUAMIS.git
''')

# %% Packages

# Python Standard Packages
import sys
import os
import socket
import re
import datetime
import json
from copy import deepcopy

# Other Packages
import pandas as pd
from helper_functions import *

# %% DEBUG: Override Snakemake

try:
    snakemake.input[0]
except NameError:
    from parse_json_debug import *
    debug_mode = True
else:
    debug_mode = False

# %% Logging

with open(snakemake.log['log'], "w") as logfile:
    if not debug_mode:
        sys.stderr = sys.stdout = logfile

    # %% Module Software Versions

    version_report = pd.read_csv(snakemake.input['software_versions'], sep='\t')  # Import versions from conda environment recipe
    version_dict = {k: v for (k, v) in dict(zip(version_report.Software, version_report.Version)).items() if not pd.isnull(k)}
    version_report_json = version_report.to_json(orient='records')
    version_report_dict = json.loads(version_report_json)

    # %% Module Analysis

    timestamp = datetime.datetime.now()

    ## Get Sample Names from Sample List
    config_table = pd.read_csv(snakemake.params['config']['samples'], sep='\t',
                               converters={i: str for i in range(0, len(pd.read_csv(snakemake.params['config']['samples'], sep='\t').columns))})
    config_table_dict = config_table[config_table['sample'].eq(snakemake.params['sample'])].to_dict(orient='records')[0]

    ## Replace static container path with host volume path
    config_edit = deepcopy(snakemake.params['config'])
    if snakemake.params['config']['params']['docker']:
        docker_path_replace(nestedObj=config_edit, newPath=snakemake.params['config']['params']['docker'])
        docker_path_replace(nestedObj=config_table_dict, newPath=snakemake.params['config']['params']['docker'])

    json_module_analysis = {
        'analysis': [{'read1'      : config_table_dict['fq1'],
                      'read2'      : config_table_dict['fq2'],
                      'host_system': socket.gethostname(),
                      'timestamp'  : str(timestamp),
                      'config'     : config_edit}]
    }

    # %% JSON Aggregation - BEFORE Assembly ---------------------------------------
    # region JSON Aggregation - BEFORE Assembly

    if snakemake.rule == 'parse_json_before_assembly':

        # %% Module FastP

        with open(snakemake.input['fastp'], 'r') as file:
            fastp_json = json.load(file)

        fastp_json['version'] = version_dict['fastp']
        fastp_json['timestamp'] = str(timestamp)

        ## Select Key/Value pairs for AQUAMIS report
        fastp_json['report'] = {  # TODO: adapter_percentage
            'sample'            : snakemake.params['sample'],
            'total_reads_before': fastp_json['summary']['before_filtering']['total_reads'],
            'total_bases_before': fastp_json['summary']['before_filtering']['total_bases'],
            'total_reads_after' : fastp_json['summary']['after_filtering']['total_reads'],
            'total_bases_after' : fastp_json['summary']['after_filtering']['total_bases'],
            'q20_rate_after'    : fastp_json['summary']['after_filtering']['q20_rate'],
            'q30_rate_after'    : fastp_json['summary']['after_filtering']['q30_rate'],
            'duplication_rate'  : fastp_json['duplication']['rate'],
            'insert_size_peak'  : fastp_json['insert_size']['peak'],
            'link_to_report'    : os.path.join('..', 'trimmed', 'reports', snakemake.params['sample'] + '.json')
        }

        adapter_df = pd.DataFrame.from_dict(data=fastp_json['adapter_cutting']['read1_adapter_counts'], orient='index', columns=['counts'])
        adapter_df.index.name = 'adapter'
        fastp_json['adapter_cutting']['read1_adapter_counts'] = adapter_df.reset_index()

        adapter_df = pd.DataFrame.from_dict(data=fastp_json['adapter_cutting']['read2_adapter_counts'], orient='index', columns=['counts'])
        adapter_df.index.name = 'adapter'
        fastp_json['adapter_cutting']['read2_adapter_counts'] = adapter_df.reset_index()

        kmer_df = pd.DataFrame.from_dict(data=fastp_json['read1_before_filtering']['kmer_count'], orient='index', columns=['count'])
        kmer_df.index.name = 'kmer'
        fastp_json['read1_before_filtering']['kmer_count'] = kmer_df.reset_index()

        kmer_df = pd.DataFrame.from_dict(data=fastp_json['read1_after_filtering']['kmer_count'], orient='index', columns=['count'])
        kmer_df.index.name = 'kmer'
        fastp_json['read1_after_filtering']['kmer_count'] = kmer_df.reset_index()

        kmer_df = pd.DataFrame.from_dict(data=fastp_json['read2_before_filtering']['kmer_count'], orient='index', columns=['count'])
        kmer_df.index.name = 'kmer'
        fastp_json['read2_before_filtering']['kmer_count'] = kmer_df.reset_index()

        kmer_df = pd.DataFrame.from_dict(data=fastp_json['read2_after_filtering']['kmer_count'], orient='index', columns=['count'])
        kmer_df.index.name = 'kmer'
        fastp_json['read2_after_filtering']['kmer_count'] = kmer_df.reset_index()

        json_module_fastp = {
            'fastp': fastp_json
        }

        # %% Module ConFindr

        try:
            open(snakemake.input['confindr_report'])
        except FileNotFoundError:
            confindr_fail = True
        else:
            confindr_fail = False

        if not confindr_fail:
            confindr_report = pd.read_csv(snakemake.input['confindr_report'], sep=',')
            confindr_report_dict = {'version'  : version_dict['ConFindr'],
                                    'timestamp': str(timestamp)}

            confindr_report_dict.update(confindr_report.to_dict(orient='records')[0])  # only valid if confindr report contains ever only one record
            # ND values in contaminated samples are string, while other samples are integer, combining samples therefore fails. mitigation: convert to None

            if confindr_report_dict['ContamStatus'] is True:
                if ":" in confindr_report_dict['Genus']:
                    confindr_report_dict['ContamStatus'] = "Inter"
                else:
                    confindr_report_dict['ContamStatus'] = "Intra"
            elif confindr_report_dict['ContamStatus'] != 'ND':
                confindr_report_dict['ContamStatus'] = "False"

            # Identify database in use
            confindr_log_commands = []
            with open(os.path.join(os.path.dirname(snakemake.input['confindr_report']), "confindr_log.txt"), 'r') as file:
                for line in file:
                    if 'Command used' in line:
                        confindr_log_commands.append(line)

            if any('bbduk.sh' in s for s in confindr_log_commands):
                confindr_log_commands_filtered = [cmd for cmd in confindr_log_commands if 'bbduk.sh' in cmd and 'ref=' in cmd]  # + snakemake.params['config']['params']['confindr']['database']
                confindr_report_dict['Database'] = [os.path.basename(re.compile("ref=(\S*)").findall(cmd)[0]) for cmd in confindr_log_commands_filtered][0]
            else:
                confindr_report_dict['Database'] = 'ND'

            confindr_report_dict = {key: (None if value == 'ND' else value) for (key, value) in confindr_report_dict.items()}

            json_module_confindr = {
                'confindr': confindr_report_dict
            }
        else:
            json_module_confindr = None

        # %% Module Kraken2_Reads

        kraken_reads_report_columns = ['perc', 'tot_all', 'tot_lvl', 'clade_lvl', 'clade_taxid', 'clade_name']
        kraken_reads_report = pd.read_csv(snakemake.input['kraken_reads_report'], sep='\t', names=kraken_reads_report_columns, nrows=50)
        kraken_reads_report['clade_name'] = kraken_reads_report['clade_name'].str.strip()

        bracken_report_columns = ['name', 'taxonomy_id', 'taxonomy_lvl', 'kraken_assigned_reads', 'added_reads', 'new_est_reads', 'fraction_total_reads']
        bracken_species_report = pd.read_csv(snakemake.input['bracken_species_report'], sep='\t', names=bracken_report_columns, nrows=50)
        bracken_species_report['name'] = bracken_species_report['name'].str.strip()
        bracken_genus_report = pd.read_csv(snakemake.input['bracken_genus_report'], sep='\t', names=bracken_report_columns, nrows=50)
        bracken_genus_report['name'] = bracken_genus_report['name'].str.strip()

        json_module_kraken2_reads = {
            'kraken2': {
                'read_based': {
                    'version'                : version_dict['Kraken'],
                    'timestamp'              : str(timestamp),
                    'kraken_report'          : kraken_reads_report,
                    'bracken_summary_species': bracken_species_report,
                    'bracken_summary_genus'  : bracken_genus_report
                }
            }
        }

        # %% JSON Module Assembly - BEFORE Assembly

        json_assembly = {
            'sample'   : {
                **json_module_analysis
            },
            'pipelines': {
                **json_module_fastp,
                **json_module_confindr,
                **json_module_kraken2_reads
            }
        }
    # endregion
    # %% JSON Aggregation - AFTER  Assembly ---------------------------------------
    # region JSON Aggregation - AFTER  Assembly
    if snakemake.rule == 'parse_json_after_assembly':

        # %% Module Shovill

        assembly_fasta_header_lines = []
        with open(snakemake.input['assembly'], 'r') as file:
            for line in file:
                if '>' in line:
                    assembly_fasta_header_lines.append(line.strip('\n').split(' '))

        shovill_columns = ['>', 'len', 'cov', 'corr', 'origname', 'sw', 'date']
        assembly_fasta_header = pd.DataFrame(assembly_fasta_header_lines, columns=shovill_columns)
        assembly_fasta_header = pd.DataFrame({name: assembly_fasta_header[name].replace(rf'{name}=*', '', regex=True) for name in assembly_fasta_header.columns.values})
        assembly_fasta_header = assembly_fasta_header.apply(pd.to_numeric, errors='ignore')

        json_module_shovill = {
            'shovill': {
                'version'              : version_dict['shovill'],
                'timestamp'            : str(timestamp),
                'assembly_fasta_header': assembly_fasta_header
            }
        }


        # %% Module Samtools

        def extractSamStats(samstats, index):
            extract = [x for x in samstats if re.search(rf'^{index}', x) != None]
            extract = [re.sub(rf'^{index}\t|\n', '', x) for x in extract]
            extract = [re.sub(':\t|\t# ', '\t', x) for x in extract]
            df = pd.DataFrame([x.split('\t') for x in extract])
            return df


        with open(snakemake.input['samstats'], 'r') as file:
            samstats = file.readlines()

        samstats_index = ['SN', 'FFQ', 'LFQ', 'GCF', 'GCL', 'GCC', 'FBC', 'FTC', 'LBC', 'LTC', 'IS', 'RL', 'FRL', 'LRL', 'ID', 'IC', 'COV', 'GCD']

        samstats_summary = extractSamStats(samstats=samstats, index='SN').set_index(0)
        samstats_summary.columns = ['value', 'comment']
        samstats_summary_dict = pd.DataFrame(samstats_summary.value).T.apply(pd.to_numeric, errors='ignore').to_dict(orient='records')[0]
        samstats_summary_dict['comments'] = {k: v for (k, v) in dict(zip(samstats_summary.index, samstats_summary.comment)).items() if not pd.isnull(v)}
        samstats_summary_dict['reads mapped count'] = int(pd.read_csv(snakemake.input['samstats_view_count'], sep='\t', header=None).iloc[0, 0])

        samstats_insertsize = extractSamStats(samstats=samstats, index='IS')
        samstats_insertsize.columns = ['insert size', 'pairs total', 'inward oriented pairs', 'outward oriented pairs', 'other pairs']
        samstats_insertsize.columns = ['insert_size', 'count', 'inward_count', 'outward_count', 'other_pairs']
        samstats_insertsize = samstats_insertsize.apply(pd.to_numeric)

        samstats_coverage = extractSamStats(samstats=samstats, index='COV')
        samstats_coverage.columns = ['coverage_interval', 'coverage_bin', 'frequency']
        samstats_coverage[['coverage_bin', 'frequency']] = samstats_coverage[['coverage_bin', 'frequency']].apply(pd.to_numeric)
        # samstats_coverage_dict = samstats_coverage[['coverage_bin', 'frequency']].to_dict()  # less overhead, but does not parse in JSONschema.net

        json_module_samstats = {
            'samstats': {
                'version'    : version_dict['samtools'],
                'timestamp'  : str(timestamp),
                'summary'    : samstats_summary_dict,
                'insert_size': samstats_insertsize,
                'coverage'   : samstats_coverage.drop(columns='coverage_interval')  # TODO: Decission: If coverage_interval is always '[coverage_bin-coverage_bin]' then use samstats_coverage_dict
            }
        }

        # %% Module Kraken2_Contigs

        kraken_contigs_report_columns = ['contig', 'taxid', 'length', 'lineage']
        kraken_taxon_ranks = {'K': 'kingdom',
                              'P': 'phylum',
                              'C': 'class',
                              'O': 'order',
                              'F': 'family',
                              'G': 'genus',
                              'S': 'species'}
        kraken_contigs_report = pd.read_csv(snakemake.input['kraken_contigs_report'], sep='\t', names=kraken_contigs_report_columns)
        lineage = pd.DataFrame(kraken_contigs_report['lineage'].str.split('|').values.tolist(),
                               columns=kraken_taxon_ranks.values()).replace(regex='[kpcofgs]__', value='')
        kraken_contigs_report = kraken_contigs_report.drop(labels='lineage', axis=1).join(lineage)

        taxon_level = kraken_taxon_ranks['S']
        taxon_sum = {taxon: kraken_contigs_report.loc[kraken_contigs_report[taxon_level] == taxon, 'length'].sum() for taxon in kraken_contigs_report[taxon_level].unique()}
        taxon_fraction = {taxon: (length / kraken_contigs_report['length'].sum()) for (taxon, length) in taxon_sum.items()}
        taxonkit_species_summary = pd.DataFrame({taxon_level + '_cumlength': pd.Series(taxon_sum), taxon_level + '_fraction': pd.Series(taxon_fraction)}).sort_values(by=[taxon_level + '_cumlength'], ascending=False)
        taxonkit_species_summary.index.name = taxon_level
        taxonkit_species_summary.reset_index(inplace=True)
        taxonkit_species_summary.insert(loc=0, column='sample', value=snakemake.params['sample'])

        taxon_level = kraken_taxon_ranks['G']
        taxon_sum = {taxon: kraken_contigs_report.loc[kraken_contigs_report[taxon_level] == taxon, 'length'].sum() for taxon in kraken_contigs_report[taxon_level].unique()}
        taxon_fraction = {taxon: (length / kraken_contigs_report['length'].sum()) for (taxon, length) in taxon_sum.items()}
        taxonkit_genus_summary = pd.DataFrame({taxon_level + '_cumlength': pd.Series(taxon_sum), taxon_level + '_fraction': pd.Series(taxon_fraction)}).sort_values(by=[taxon_level + '_cumlength'], ascending=False)
        taxonkit_genus_summary.index.name = taxon_level
        taxonkit_genus_summary.reset_index(inplace=True)
        taxonkit_genus_summary.insert(loc=0, column='sample', value=snakemake.params['sample'])

        json_module_kraken2_contigs = {
            'kraken2': {
                'contig_based': {
                    'version'               : version_dict['Kraken'],
                    'timestamp'             : str(timestamp),
                    'kraken_report'         : kraken_contigs_report,
                    'kraken_summary_species': taxonkit_species_summary,
                    'kraken_summary_genus'  : taxonkit_genus_summary,
                }
            }
        }

        # %% Module Mash

        mash_distance_columns = ['reference', 'query', 'distance', 'pvalue', 'shared_hashes', 'total_hashes']
        mash_distance = pd.read_csv(snakemake.input['mash_distance'], sep='\t', names=mash_distance_columns, nrows=1)
        mash_distance = mash_distance.to_dict(orient='records')[0]

        with open(snakemake.input['reference'], 'r') as file:
            mash_reference = re.sub(r'^>(.*)\n', '\\1', file.readline())

        json_module_mash = {
            'mash': {
                'version'         : version_dict['mash'],
                'timestamp'       : str(timestamp),
                'reference_header': mash_reference,
                'reference'       : mash_distance.pop('reference'),
                **mash_distance
            }
        }

        # %% Module QUAST

        quast_report = pd.read_csv(snakemake.input['quast_report'], sep='\t')
        quast_report = quast_report.set_index('Assembly').T
        quast_report = quast_report.apply(pd.to_numeric, errors='ignore').to_dict(orient='records')[0]

        quast_misassemblies = pd.read_csv(snakemake.input['quast_misassemblies'], sep='\t')
        quast_misassemblies.columns = quast_misassemblies.columns.str.strip()
        quast_misassemblies = quast_misassemblies.to_dict(orient='records')[0]

        quast_unaligned = pd.read_csv(snakemake.input['quast_unaligned'], sep='\t')
        quast_unaligned = quast_unaligned.set_index('Assembly').T.to_dict(orient='records')[0]

        ## Parse QUAST genome_info file
        with open(snakemake.input['quast_genome_info'], 'r') as file:
            quast_genome_info_lines = file.readlines()

        quast_genome_info_lines = [x.strip() for x in quast_genome_info_lines]
        quast_genome_info_lines[0] = quast_genome_info_lines[0].replace('"', '')
        quast_genome_info_lines[1] = quast_genome_info_lines[1] + ' ' + re.sub(r'^(\S*)\s.*', '\\1', quast_genome_info_lines[2])
        quast_genome_info_lines[2] = ['reference {0}'.format(i) for i in re.sub(r'.*\((.*)\).*', '\\1', quast_genome_info_lines[2]).split(', ')]
        quast_genome_info_tableindex = [s.split('|') for s in quast_genome_info_lines if 'assembly' in s]
        quast_genome_name_trunc = snakemake.params['sample'].replace('-', '_').replace('.', '_')
        quast_genome_name_trunc = quast_genome_name_trunc[:24] if len(quast_genome_name_trunc) > 24 else quast_genome_name_trunc
        quast_genome_info_tabledata = [s.split('|') for s in quast_genome_info_lines if quast_genome_name_trunc in s]
        quast_genome_info = dict(x.split(': ') for x in quast_genome_info_lines if ':' in x)
        quast_genome_info.update(dict(x.split(': ') for x in quast_genome_info_lines[2]))
        quast_genome_info_table = dict(zip([s.strip() for s in quast_genome_info_tableindex[0]], [s.strip() for s in quast_genome_info_tabledata[0]]))
        quast_genome_info.update({k: v for (k, v) in quast_genome_info_table.items() if k != ''})
        quast_genome_info = pd.DataFrame.from_dict(
            quast_genome_info, orient='index').replace(
            ' bp', '', regex=True).T.apply(
            pd.to_numeric, errors='ignore').to_dict(orient='records')[0]

        json_module_quast = {
            'quast': {
                'version'      : version_dict['QUAST'],
                'timestamp'    : str(timestamp),
                'report'       : quast_report,
                'genome_info'  : quast_genome_info,
                'misassemblies': quast_misassemblies,
                'unaligned'    : quast_unaligned
            }
        }

        # %% Module BUSCO

        busco_file = os.path.join(os.path.dirname(snakemake.params['busco']),
                                  os.path.splitext(os.path.basename(snakemake.params['busco']))[0].replace('.', '-') +
                                  os.path.splitext(os.path.basename(snakemake.params['busco']))[1])

        try:
            open(busco_file)
        except FileNotFoundError:
            busco_fail = True
        else:
            busco_fail = False

        if not busco_fail:
            with open(busco_file) as file:
                busco_summary = file.readlines()

            busco_summary = [x.strip() for x in busco_summary]

            busco_summary_notes = list(filter(re.compile('[#].*').match, busco_summary))  # Read Note
            busco_summary_notes = [x for x in busco_summary_notes if ':' in x]
            busco_summary_notes_dict = dict(x.split(': ', maxsplit=1) for x in busco_summary_notes)

            busco_summary_results = list(filter(re.compile('[^#].*').match, busco_summary))  # Read Note
            busco_summary_results_stats = re.split('[,\[\]]', busco_summary_results[0])
            busco_summary_results_stats = [x for x in busco_summary_results_stats if x]
            busco_summary_results_stats = dict(x.split(':') for x in busco_summary_results_stats)

            busco_summary_results_dict = dict(x.split('\t') for x in ';'.join(busco_summary_results[1:]).split(';'))
            busco_summary_results_dict = {value: int(key) for key, value in busco_summary_results_dict.items()}

            json_module_busco = {
                'busco': {
                    'version'  : busco_summary_notes_dict['# BUSCO version is'],
                    'timestamp': str(timestamp),
                    'command'  : busco_summary_notes_dict['# To reproduce this run'],
                    'database' : busco_summary_notes_dict['# The lineage dataset is'],
                    'results'  : {**busco_summary_results_dict, **busco_summary_results_stats}
                }
            }
        else:
            json_module_busco = None

        # %% Module MLST

        with open(snakemake.input['mlst'], 'r') as file:
            mlst_json = json.load(file)[0]  # prerequisite: only single-sample-json loaded

        if mlst_json['scheme'] is not "-":
            mlst_json['loci_total'] = len(mlst_json['alleles'])
            mlst_json['loci_missing'] = len([v for v in mlst_json['alleles'].values() if v is '-'])
            mlst_json['multi_allele_loci'] = sum([len(re.findall(",", locus)) for locus in mlst_json['alleles'].values()])
            mlst_json_alleles_df = pd.DataFrame.from_dict(data=mlst_json['alleles'], orient='index', columns=['allele_no'])
            mlst_json_alleles_df.index.name = 'locus'
            mlst_json['alleles'] = mlst_json_alleles_df.reset_index()
        else:
            mlst_json['loci_total'] = None
            mlst_json['loci_missing'] = None
            mlst_json['multi_allele_loci'] = None

        json_module_mlst = {
            'mlst': {
                'version'  : version_dict['mlst'],
                'timestamp': str(timestamp),
                **mlst_json
            }
        }

        # %% Module AQUAMIS

        aquamis_report = {'sample': snakemake.params['sample']}
        aquamis_report['reference'] = json_module_mash['mash']['reference_header']
        aquamis_report['assembly_coverageDepth'] = sum(samstats_coverage['coverage_bin'] * samstats_coverage['frequency']) / sum(samstats_coverage['frequency'])
        aquamis_report['reads_total'] = json_module_samstats['samstats']['summary']['raw total sequences']
        aquamis_report['reads_mapped'] = json_module_samstats['samstats']['summary']['reads mapped']
        aquamis_report['fraction_reads_mapped'] = aquamis_report['reads_mapped'] / aquamis_report['reads_total']
        aquamis_report['coverage_rawDepth'] = json_module_samstats['samstats']['summary']['reads mapped count'] / sum(json_module_shovill['shovill']['assembly_fasta_header']['len'])
        aquamis_report['base_count'] = json_module_samstats['samstats']['summary']['reads mapped count']
        aquamis_report['count_circular_contigs'] = None  # TODO: adapt to shovill or remove

        try:
            aquamis_report['busco_single'] = float(json_module_busco['busco']['results']['S'].strip('%'))  # single copy orthologs
            aquamis_report['busco_duplicates'] = float(json_module_busco['busco']['results']['D'].strip('%'))  # duplicated orthologs
        except TypeError:
            aquamis_report['busco_single'] = None
            aquamis_report['busco_duplicates'] = None

        aquamis_report['genes_full'] = int(re.findall('\d+', json_module_quast['quast']['report']['# genomic features'])[0])

        try:
            aquamis_report['genes_partial'] = int(re.findall('\d+', json_module_quast['quast']['report']['# genomic features'])[1])
        except IndexError:
            aquamis_report['genes_partial'] = ''

        aquamis_report['fraction_genes_recovered'] = (aquamis_report['genes_full'] + aquamis_report['genes_partial']) / json_module_quast['quast']['genome_info']['Genomic features of type ANY loaded']
        # aquamis_report['ref_length'] = json_module_quast['quast']['report']['Reference length']  # Calculated in former snakemake rule but not used in report
        aquamis_report['version'] = snakemake.params['config']['version']  # duplicate in config, but here it conforms to module structure
        aquamis_report['timestamp'] = str(timestamp)

        json_module_aquamis = {
            'aquamis': aquamis_report
        }

        # %% Module Sample Summary

        json_module_sample = {
            'summary': {
                **json_module_aquamis['aquamis'],
                **json_module_quast['quast']['report']
            }
        }

        # %% JSON Module Assembly - AFTER Assembly

        with open(snakemake.input['json_in'], 'r') as file:
            json_sample = json.load(fp=file, object_pairs_hook=unquote_hook)

        try:
            json_sample['sample']['analysis'].append(json_module_analysis['analysis'][0])
        except KeyError:
            pass

        json_assembly = {
            'sample'   : {
                **json_module_sample
            },
            'pipelines': {
                **json_module_shovill,
                **json_module_samstats,
                **json_module_kraken2_contigs,
                **json_module_mash,
                **json_module_quast,
                **json_module_mlst,
                **json_module_aquamis
            }
        }

        try:
            json_assembly['pipelines'].update(json_module_busco)
        except TypeError:
            print('WARNING: BUSCO result aggregation into JSON failed.')

        json_assembly = merge_dict(original=json_sample, patch=json_assembly)

    # endregion
    # %% JSON Aggregation - AFTER QC
    # region JSON Aggregation - AFTER QC
    if snakemake.rule == 'parse_json_after_qc':

        # %% Module QC Assessment

        with open(snakemake.params['config']['params']['qc']['thresholds'], 'r') as file:
            json_threshold_metadata = json.load(fp=file, object_pairs_hook=unquote_hook)['metadata']

        qc_report = pd.read_csv(snakemake.params['tsv_qc_vote'], sep='\t')
        qc_report_dict = qc_report[qc_report['sample'].eq(snakemake.params['sample'])].to_dict(orient='records')[0]

        json_module_qc = {
            'qc_assessment': {
                'thresholds': json_threshold_metadata,
                'timestamp' : str(timestamp),
                **qc_report_dict,
            }
        }

        # %% JSON Module Assembly - AFTER QC

        with open(snakemake.input['json_in'], 'r') as file:
            json_sample = json.load(fp=file, object_pairs_hook=unquote_hook)

        json_assembly = {
            'sample': {
                **json_module_qc
            }
        }

        json_assembly = merge_dict(original=json_sample, patch=json_assembly)

    # endregion
    # %% Export JSON Output

    with open(snakemake.output['json_out'], 'w') as file:
        json.dump(
            obj=strictJsonObj(
                nestedObj=json.loads(
                    s=nestedDict_to_json(
                        nestedObj=json_assembly)),
                uriCompliant=True),
            fp=file,
            indent=4,
            sort_keys=False,
            ensure_ascii=False,
            allow_nan=False)  # Improvement: Use .encode('utf-8') in combo with ensure_ascii=False if None values are omitted instead of coerced to None

    print('JSON Assembly for ' + snakemake.params['sample'] + ' finished.')
