#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

"""
ABOUT: This is the main entry for the pipeline.
REQUIRES:
  - python>=3.6
  - snakemake   (recommended>=6.0.0)
  - singularity (recommended==latest)
DISCLAIMER:
                    PUBLIC DOMAIN NOTICE
        NIAID Collaborative Bioinformatics Resource (NCBR)
   National Institute of Allergy and Infectious Diseases (NIAID)
This software/database is a "United  States Government Work" under
the terms of the United  States Copyright Act.  It was written as
part of the author's official duties as a United States Government
employee and thus cannot be copyrighted. This software is freely
available to the public for use.
Although all  reasonable  efforts have been taken  to ensure  the
accuracy and reliability of the software and data, NCBR do not and
cannot warrant the performance or results that may  be obtained by
using this software or data. NCBR and NIH disclaim all warranties,
express  or  implied,  including   warranties   of   performance,
merchantability or fitness for any particular purpose.
Please cite the author and NIH resources like the "Biowulf Cluster"
in any work or product based on this material.
USAGE:
  $ cell-seek <command> [OPTIONS]
EXAMPLE:
  $ cell-seek run --input *.R?.fastq.gz --output output --pipeline gex --genome hg38/
"""

# Python standard library
from __future__ import print_function
from logging import config
import sys
import os
import subprocess
import json
import textwrap
import argparse
import shutil

# Local imports
from src import version
from src.run import init, setup, bind, dryrun, runner, finalcheck
from src.utils import (
    Colors,
    err,
    exists,
    fatal,
    permissions,
    check_cache,
    require,
)


# Pipeline Metadata
__version__ = version
__authors__ = "Skyler Kuhn, Vicky Chen"
__email__ = "skyler.kuhn@nih.gov, vicky.chen@nih.gov"
__home__ = os.path.dirname(os.path.abspath(__file__))
_name = os.path.basename(sys.argv[0])
_description = "One single-cell pipeline to rule them all!"


def unlock(sub_args):
    """Unlocks a previous runs output directory. If snakemake fails ungracefully,
    it maybe required to unlock the working directory before proceeding again.
    This is rare but it does occasionally happen. Maybe worth add a --force
    option to delete the '.snakemake/' directory in the future.
    @param sub_args <parser.parse_args() object>:
        Parsed arguments for unlock sub-command
    """
    print("Unlocking the pipeline's output directory...")
    outdir = sub_args.output

    try:
        unlock_output = subprocess.check_output(
            ["snakemake", "--unlock", "--cores", "1", "--configfile=config.json"],
            cwd=outdir,
            stderr=subprocess.STDOUT,
        )
    except subprocess.CalledProcessError as e:
        # Unlocking process returned a non-zero exit code
        sys.exit("{}\n{}".format(e, e.output))

    print("Successfully unlocked the pipeline's working directory!")


def run(sub_args):
    """Initialize, setup, and run the pipeline.
    Calls initialize() to create output directory and copy over pipeline resources,
    setup() to create the pipeline config file, dryrun() to ensure their are no issues
    before running the pipeline, and finally run() to execute the Snakemake workflow.
    @param sub_args <parser.parse_args() object>:
        Parsed arguments for run sub-command
    """
    # Step 0. Check for required dependencies
    # The pipelines has only two requirements:
    # snakemake and singularity
    require(["snakemake", "singularity"], ["snakemake", "singularity"])

    # Step 1. Initialize working directory,
    # copy over required resources to run
    # the pipeline
    git_repo = __home__
    input_files = init(
        repo_path=git_repo,
        output_path=sub_args.output,
        links=sub_args.input,
        sym_link=False,
    )

    # Step 2. Setup pipeline for execution,
    # dynamically create config.json config
    # file from user inputs and base config
    # templates
    config = setup(
        sub_args, ifiles=input_files, repo_path=git_repo, output_path=sub_args.output
    )

    # Step 3. Resolve docker/singularity bind
    # paths from the config file.
    bindpaths = bind(sub_args, config=config)

    config["bindpaths"] = bindpaths

    # Step 4. Save config to output directory
    with open(os.path.join(sub_args.output, "config.json"), "w") as fh:
        json.dump(config, fh, indent=4, sort_keys=True)

    # Optional Step: Dry-run pipeline
    if sub_args.dry_run:
        # Dryrun pipeline
        dryrun_output = dryrun(
            outdir=sub_args.output
        )  # python3 returns byte-string representation
        print(
            "\nDry-running {} pipeline:\n{}".format(
                _name, dryrun_output.decode("utf-8")
            )
        )
        for arg_check in ["rename", "libraries"]:
            if config["options"][arg_check] != "None":
                finalcheck(config=config, flag=arg_check)
        sys.exit(0)

    # Step 4b. Perform final check of input files
    for arg_check in ["rename", "libraries"]:
        if config["options"][arg_check] != "None":
            finalcheck(config=config, flag=arg_check)

    # Step 5. Orchestrate pipeline execution,
    # run pipeline in locally on a compute node
    # for debugging purposes or submit the master
    # job to the job scheduler, SLURM, and create
    # logging file
    if not exists(os.path.join(sub_args.output, "logfiles")):
        # Create directory for logfiles
        os.makedirs(os.path.join(sub_args.output, "logfiles"))
    if sub_args.mode == "local":
        log = os.path.join(sub_args.output, "logfiles", "snakemake.log")
    else:
        log = os.path.join(sub_args.output, "logfiles", "master.log")
    logfh = open(log, "w")
    mjob = runner(
        mode=sub_args.mode,
        outdir=sub_args.output,
        # additional_bind_paths = all_bind_paths,
        alt_cache=sub_args.singularity_cache,
        threads=int(sub_args.threads),
        jobname=sub_args.job_name,
        submission_script=os.path.join(__home__, "src", "run.sh"),
        logger=logfh,
        additional_bind_paths=",".join(bindpaths),
        tmp_dir=sub_args.tmp_dir,
    )

    # Step 6. Wait for subprocess to complete,
    # this is blocking and not asynchronous
    if not sub_args.silent:
        print("\nRunning {} pipeline in '{}' mode...".format(_name, sub_args.mode))
    mjob.wait()
    logfh.close()

    # Step 7. Relay information about submission
    # of the master job or the exit code of the
    # pipeline that ran in local mode
    if sub_args.mode == "local":
        if int(mjob.returncode) == 0:
            print("{} pipeline has successfully completed".format(_name))
        else:
            fatal(
                "{} pipeline failed. Please see {} for more information.".format(
                    _name, os.path.join(sub_args.output, "logfiles", "snakemake.log")
                )
            )
    elif sub_args.mode == "slurm":
        jobid = (
            open(os.path.join(sub_args.output, "logfiles", "mjobid.log")).read().strip()
        )
        if not sub_args.silent:
            if int(mjob.returncode) == 0:
                print("Successfully submitted master job: ", end="")
            else:
                fatal("Error occurred when submitting the master job.")
        print(jobid)


def cache(sub_args):
    """Caches remote resources or reference files stored on DockerHub and S3.
    Local SIFs will be created from images defined in 'config/containers/images.json'.
    @TODO: add option to cache other shared S3 resources (i.e. kraken db and fqscreen indices)
    @param sub_args <parser.parse_args() object>:
        Parsed arguments for unlock sub-command
    """
    print(sub_args)
    fatal("NotImplementedError... Coming Soon!")


def genome(sub_args):
    """Unlocks a previous runs output directory. If snakemake fails ungracefully,
    it maybe required to unlock the working directory before proceeding again.
    This is rare but it does occasionally happen. Maybe worth add a --force
    option to delete the '.snakemake/' directory in the future.
    @param sub_args <parser.parse_args() object>:
        Parsed arguments for unlock sub-command
    """
    print("Generating custom json file...")

    print(sub_args)
    if sub_args.gex is not None:
        sub_args.cite = sub_args.gex
    # print([(i[0], type(i[1]) == str) for i in vars(sub_args).items()])
    entryname = {
        "gex": "gex_transcriptome",
        "cite": "cite_transcriptome",
        "atac": "atac_ref",
        "vdj": "vdj_ref",
        "multiome": "arc_ref",
    }
    ref = {
        "references": {
            sub_args.name: {
                entryname[i[0]]: i[1]
                for i in vars(sub_args).items()
                if i[0] != "name" and isinstance(i[1], str)
            }
        }
    }
    # print(ref)

    with open(f"{sub_args.name}.json", "w") as f:
        json.dump(ref, f, indent=4)

    print(
        "Custom json file generated. File located at: {}".format(
            os.path.abspath(f"{sub_args.name}.json")
        )
    )


def genome_options(parser, user_option, prebuilt):
    """Dynamically checks if --genome option is a valid choice. Compares against a
    list of prebuilt or bundled genome reference genomes and accepts a custom reference
    genome that was built using the 'rna-seek build' commmand. The ability to also
    accept a custom reference JSON file allows for chaining of the rna-seek build and run
    commands in succession.
    @param parser <argparse.ArgumentParser object>:
        Parser object from which an exception is raised not user_option is not valid
    @param user_option <str>:
        Provided value to the rna-seek run, --genome argument
    @param prebuilt list[<str>]:
        List of prebuilt or builded reference genomes
    return user_option <str>:
        Provided value to the rna-seek run, --genome argument
        If vaule is not valid or custom reference genome JSON file not readable,
        an exception is raised.
    """
    # Checks for custom built genomes using rna-seek build
    if user_option.endswith(".json"):
        with open(user_option, "r") as _f:
            ref = json.load(_f)
        # Basic check to see if user provided
        # JSON file is formatted properly
        if "references" not in ref.keys():
            raise argparse.ArgumentTypeError(
                "User provided Genome JSON does not appear to be valid, missing section containing 'references'!"
            )
        genome_names = list(
            ref["references"].keys()
        )  # resolves to reference genome name, should only be one
        if len(genome_names) > 1:
            genomes = ",".join(genome_names)
            raise argparse.ArgumentTypeError(
                f"User provided Genome JSON contains multiple genome(s): {genomes}"
            )
        # Check file is readable or accessible
        permissions(parser, user_option, os.R_OK)
    # Checks against vaild pre-built options
    # TODO: makes this more dynamic in the future to have it check against
    # a list of genomes (files) in config/genomes/*.json
    elif user_option not in prebuilt:
        # User did NOT provide a vaild choice
        parser.error(
            """provided invalid choice, '{}', to --genome argument!\n
        Choose from one of the following pre-built genome options: \n
        \t{}\n
        or supply a custom reference genome JSON file with reference generated using cellranger mkref.\n
        Custom reference genom JSON file can be created using the cell-seek genome command.
        """.format(user_option, prebuilt)
        )

    return user_option


def shinycell(args):
    require(["snakemake", "singularity"], ["snakemake", "singularity"])

    if (args.markers is None) != (args.cluster_labels is None):
        fatal("--markers requires --cluster_labels to be set as well (and vice versa)")

    if args.markers is not None:
        markers_path = os.path.abspath(os.path.expanduser(args.markers))
        if not os.path.isfile(markers_path) or not os.access(markers_path, os.R_OK):
            fatal(f"Markers file not found or not readable: {args.markers}")
        args.markers = markers_path

    if not os.path.exists(args.object) or not os.access(args.object, os.R_OK):
        fatal(f"Seurat object not found or not readable: {args.object}")

    cfg = {}
    containers = os.path.join(os.path.dirname(__file__), "config", "containers.json")
    cfg.update(json.loads(open(containers).read()))
    cfg["seurat_object"] = os.path.abspath(args.object)
    cfg["run_dir"] = os.path.abspath(args.outdir)
    cfg["project_title"] = args.project
    cfg["marker_file"] = "" if args.markers is None else args.markers
    cfg["cluster_labels"] = "" if args.cluster_labels is None else args.cluster_labels
    cfg["rmmeta"] = args.meta_to_rm
    cfg["defaultreduction"] = args.defaultreduction
    cfg["max_levels"] = args.max_levels
    cfg["assaytouse"] = args.assaytouse
    cfg["tmpdir"] = args.tmpdir
    cfg["project"] = {}
    cfg["project"]["datapath"] = os.path.abspath(os.path.dirname(args.object))
    cfg["project"]["workpath"] = os.path.abspath(args.outdir)

    if not os.path.exists(args.outdir):
        os.makedirs(args.outdir, mode=0o755)
    json.dump(cfg, open(os.path.join(args.outdir, "config.json"), "w"), indent=4)

    scrules = os.path.join(
        os.path.dirname(__file__), "workflow", "rules", "shinycell2.smk"
    )
    cluster_resources = os.path.join(
        os.path.dirname(__file__), "config", "cluster.json"
    )
    to_copy = {
        scrules: os.path.join(args.outdir, "workflow", "Snakefile"),
        cluster_resources: os.path.join(args.outdir, "config", "cluster.json"),
    }
    for src, dest in to_copy.items():
        if not os.path.exists(os.path.dirname(dest)):
            os.makedirs(os.path.dirname(dest), exist_ok=True)
        shutil.copy(src, dest)

    if not exists(os.path.join(args.outdir, "logfiles")):
        os.makedirs(os.path.join(args.outdir, "logfiles"))
    if args.mode == "local":
        log = os.path.join(args.outdir, "logfiles", "snakemake.log")
    else:
        log = os.path.join(args.outdir, "logfiles", "master.log")
    logfh = open(log, "w")

    bindpaths = bind(args, config=cfg)

    mjob = runner(
        mode=args.mode,
        outdir=args.outdir,
        alt_cache=None,
        threads=int(args.threads),
        jobname=args.job_name,
        submission_script=os.path.join(__home__, "src", "run.sh"),
        logger=logfh,
        additional_bind_paths=",".join(bindpaths),
        tmp_dir=args.tmpdir,
    )

    if not args.silent:
        print("\nRunning {} pipeline in '{}' mode...".format(_name, args.mode))
    mjob.wait()
    logfh.close()

    if args.mode == "local":
        if int(mjob.returncode) == 0:
            print("{} pipeline has successfully completed".format(_name))
        else:
            fatal(
                "{} pipeline failed. Please see {} for more information.".format(
                    _name, os.path.join(args.outdir, "logfiles", "snakemake.log")
                )
            )
    elif args.mode == "slurm":
        jobid = open(os.path.join(args.outdir, "logfiles", "mjobid.log")).read().strip()
        if not args.silent:
            if int(mjob.returncode) == 0:
                print("Successfully submitted master job: ", end="")
            else:
                fatal("Error occurred when submitting the master job.")
        print(jobid)

    return


def parsed_arguments(name, description):
    """Parses user-provided command-line arguments. Requires argparse and textwrap
    package. argparse was added to standard lib in python 3.5 and textwrap was added
    in python 3.5. To create custom help formatting for subparsers a docstring is
    used create the help message for required options. argparse does not support named
    subparser groups, which is normally what would be used to accomphish this reformatting.
    As so, the help message for require options must be suppressed. If a new required arg
    is added to a subparser, it must be added to the docstring and the usage statement
    also must be updated.
    @param name <str>:
        Name of the pipeline or command-line tool
    @param description <str>:
        Short description of pipeline or command-line tool
    """
    # Add styled name and description
    c = Colors
    styled_name = "{0}{1}{2}cell-seek{3}".format(c.bold, c.bg_black, c.cyan, c.end)
    description = "{0}{1}{2}".format(c.bold, description, c.end)

    # Create a top-level parser
    parser = argparse.ArgumentParser(
        description="{}: {}".format(styled_name, description)
    )

    # Adding Verison information
    parser.add_argument(
        "--version", action="version", version="%(prog)s {}".format(__version__)
    )

    # Create sub-command parser
    subparsers = parser.add_subparsers(help="List of available sub-commands")

    # Sub-parser for the "run" sub-command
    # Grouped sub-parser arguments are currently
    # not supported: https://bugs.python.org/issue9341
    # Here is a work around to create more useful help message for named
    # options that are required! Please note: if a required arg is added the
    # description below should be updated (i.e. update usage and add new option)
    required_run_options = textwrap.dedent(
        """\
        {0}: {1}

        {3}{4}Synopsis:{5}
          $ {2} run [--help] \\
                [--dry-run] [--job-name JOB_NAME] [--mode {{slurm,local}}] \\
                [--sif-cache SIF_CACHE] [--singularity-cache SINGULARITY_CACHE] \\
                [--silent] [--threads THREADS] [--tmp-dir TMP_DIR] \\
                [--aggregate {{mapped, none}}][--libraries LIBRARIES] \\
                [--ambient-rna-method {{none,soupx}}] \\
                [--features FEATURES] [--cmo-reference CMOREFERENCE] \\
                [--cmo-sample CMOSAMPLE] [--hto-sample HTOSAMPLE] \\ 
                [--ocm-sample OCMSAMPLE] [--probe-set PROBESET] \\
                [--probe-sample PROBESAMPLE] [--chain  {{auto,TR,IG}}] \\
                [--exclude-introns] [--filter FILTER] \\
                [--create-bam] [--rename RENAMEFILE] [--forcecells FORCECELLS]\\
                --cellranger VERSION \\
                --input INPUT [INPUT ...] \\
                --output OUTPUT \\
                --pipeline {{gex, ...}} \\
                --genome {{hg38, ...}}

        Optional arguments are shown in square brackets above.

        {3}{4}Description:{5}
          To run the cell-seek pipeline with your data raw data, please
        provide a space separated list of FastQ (globbing is supported) and an output
        directory to store results.

        {3}{4}Required arguments:{5}
          --input INPUT [INPUT ...]
                                Input FastQ file(s) or Cell Ranger output folders to
                                process. The pipeline does NOT support single-end data. 
                                FastQ files for one or more samples can be provided. 
                                Multiple input FastQ files per sample can be provided. 
                                Multiple input FastQ files should be separated by a 
                                space.
                                Cell Ranger output folders can be provided. It is 
                                expected that the outs folder is contained within the 
                                Cell Ranger output folders.
                                Globbing for multiple files/folders is also supported.
                                FastQ Input:
                                  Example: --input .tests/*.R?.fastq.gz
                                Cell Ranger Input:
                                  Example: --input .tests/*/
          --output OUTPUT
                                Path to an output directory. This location is where
                                the pipeline will create all of its output files, also
                                known as the pipeline's working directory. If the user
                                provided working directory has not been initialized,
                                it will be created automatically.
                                  Example: --output /data/$USER/output
          --pipeline {{gex,vdj,cite,multi,atac,multiome}}
                                The pipeline to run. Can be chosen from
                                the options gene expression (GEX), VDJ, feature
                                barcode (CITE), Cell Ranger multi (Multi), ATAC,
                                or multiome.
                                  Example: --pipeline gex
          --genome  {{hg38,mm10,hg2024,mm2024,custom.json}}
                                Reference genome. This option defines the reference
                                genome of the samples. {2} does comes bundled with
                                prebuilt reference files for human and mouse samples.
                                The newest 10X reference (2024-A) is available for
                                GEX, CITE, ATAC, Multi, and Multiome analysis (hg2024, mm2024).
                                A custom reference genome can also be provided. For
                                prebuilt references, please select one of the following
                                options: hg38, mm10, hg2024, mm2024.
                                  Example: --genome hg38
          --cellranger {{2.0.1, 2.1.0, 2.2.0, 7.1.0, 7.2.0, 8.0.0, 9.0.0, 10.0.0}}
                                The version of CellRanger, CellRanger ATAC, or CellRanger 
                                ARC to run. This option specifies which version of 
                                the corresponding CellRanger software to use when running 
                                ATAC, ARC, GEX, CITE, or MULTI. 
                                Please select one of the following options:
                                GEX, CITE, MULTI: 7.1.0, 7.2.0, 8.0.0, 9.0.0, 10.0.0
                                ARC: 2.0.1, 2.1.0
                                ATAC: 2.1.0, 2.2.0
                                  Example: --cellranger 7.1.0
        {3}{4}Analysis options:{5}
          --ambient-rna-method {{none,soupx}}
                                Optional ambient RNA correction (default: none).
                                SoupX requires GEX-only inputs with raw/filtered
                                v3 matrices and graph clusters. Omit --aggregate.

          --aggregate  {{mapped,none}}
                                Cell Ranger aggregate. This option defines the
                                normalization mode that should be used. Mapped is what
                                Cell Ranger would run by default, which subsamples reads
                                from higher depth samples until each library type has an
                                equal number of reads per cell that are confidently mapped.
                                None means to not normalize at all. If this flag is not
                                used then aggregate will not be run. Aggregate analysis
                                is generally not needed, but it can be used to generate a
                                Loupe Browser file for interactive exploration of the data.
                                To run Cell Ranger aggregate, please select one of the 
                                following options: mapped, none.
                                  Example: --aggregate mapped
          --libraries LIBRARIES
                                Libraries file. A CSV file containing information about
                                each library.  This file is used in feature barcode (cite),
                                multi, and multiome analysis.It contains each sample's
                                name, flowcell, demultiplexed name, and library type.
                                  Here is an example libraries.csv file:
                                    Name,Flowcell,Sample,Type
                                    IL15_LNs,H7CNNBGXG,IL15_LNs,Gene Expression
                                    IL15_LNs,H7CT7BGXG,IL15_LNs_BC,Antibody Capture
                                  where:
                                    • Name: name of the sample passed to CellRanger.
                                    • Flowcell: the flowcell that contains the FASTQ
                                      files for this set of data.
                                    • Sample: name that was used when demultiplexing,
                                      this should match the FASTQ files.
                                    • Type: library type for each sample. List of
                                      supported options:
                                        • Gene Expression
                                        • CRISPR Guide Capture
                                        • Antibody Capture
                                        • Multiplexing Capture
                                        • VDJ
                                        • Chromatin Accessibility
                                        • Custom
                                  Example: --libraries libraries.csv
          --features FEATURES
                                Features file. A feature reference CSV file containing
                                information for processing a feature barcode data. This
                                file should contain a unique ID for the feature, a human
                                readable name, sequence, feature type, read, and pattern.
                                  Here is an example features.csv file:
                                    id,name,sequence,feature_type,read,pattern
                                    CITE_CD64,CD64,AGTGGG,Antibody Capture,R2,5PNN(BC)
                                    CITE_CD8,CD8,TCACCGT,Antibody Capture,R2,5PNNN(BC)
                                  where:
                                    • id: Unique ID for this feature. Must not contain
                                      whitespace, quote or comma characters. Each ID
                                      must be unique and must not collide with a gene
                                      identifier from the transcriptome.
                                    • name: Human-readable name for this feature. Must
                                      not contain whitespace.
                                    • sequence: Nucleotide barcode sequence associated
                                      with this feature, e.g. the antibody barcode or
                                      sgRNA protospacer sequence.
                                    • read: Specifies which RNA sequencing read contains
                                      the Feature Barcode sequence. Must be R1 or R2, but
                                      in most cases R2 is the correct read.
                                    • pattern: Specifies how to extract the sequence of
                                      the feature barcode from the read.
                                    • Type of the feature. List of supported options:
                                        • Gene Expression
                                        • CRISPR Guide Capture,
                                        • Antibody Capture
                                        • Custom
                                  Example: --features features.csv
          --cmo-reference CMOREFERENCE
                                CMO reference file. A CMO reference CSV file containing
                                information for processing hashtag data, which is used in
                                multi analysis if custom hashtags will be processed. This
                                file should contain a unique ID for the hashtag, a human
                                readable name, sequence, feature type, read, and pattern.
                                  Here is an example cmo_reference.csv file:
                                    id,name,sequence,feature_type,read,pattern
                                    CMO301,CMO301,ATGAGGAATTCCTGC,Multiplexing Capture,R2,5P(BC)
                                    CMO302,CMO302,CATGCCAATAGAGCG,Multiplexing Capture,R2,5P(BC)
                                  where:
                                    • id: Unique ID for this feature. Must not contain
                                      whitespace, quote or comma characters. Each ID
                                      must be unique and must not collide with a gene
                                      identifier from the transcriptome.
                                    • name: Human-readable name for this feature. Must
                                      not contain whitespace.
                                    • sequence: Nucleotide barcode sequence associated
                                      with this hashtag.
                                    • feature_type: Type of the feature. This should always be
                                      multiplexing capture.
                                    • read: Specifies which RNA sequencing read contains
                                      the Feature Barcode sequence. Must be R1 or R2, but
                                      in most cases R2 is the correct read.
                                    • pattern: Specifies how to extract the sequence of
                                      the feature barcode from the read.
                                    • Type of the feature. This should always be Multiplexing Capture
                                  Example: --cmo-reference cmo_reference.csv
          --cmo-sample CMOSAMPLE
                                CMO sample file. A CMO sample CSV file containing
                                sample to hashtag information used for multi analysis.
                                CMO IDs should match the ones used in the CMO reference
                                file. If no CMO reference is provided then CMO ID should
                                match the ones used by 10x. The same CMO sample will be
                                used on all multi libraries. This file should contain a
                                unique sample ID for the sample, and the CMO IDs associated
                                with that sample. If more than one CMO ID is associated with
                                a sample then a | should be used to separate the tags.
                                  Here is an example cmo_sample.csv file:
                                    sample_id,cmo_ids
                                    sample1,CMO301
                                    sample2,CMO302|CMO303
                                  where:
                                    • sample_id: Unique sample ID for this tagged sample.
                                      Must not contain, whitespace, quote or comma characters.
                                      Each sample ID must be unique.
                                    • cmo_ids: Unique CMO ID(s) that the sample is tagged
                                      with. Must match either entries in cmo_reference.csv file
                                      or 10x CMO IDs.
                                  Example: --cmo-sample cmo_sample.csv
          --hto-sample HTOSAMPLE
                                 A HTO sample CSV file containing sample to hashtag information 
                                 used for multi analysis. This should always be used in 
                                 conjunction with the feature file provided using the feature 
                                 flag.
                                 HTO IDs should match the ones used in the feature reference 
                                 file. The same HTO sample will be used on all multi libraries. 
                                 This file should contain a unique sample ID for the sample, 
                                 and the HTO ID(s) associated with that sample. If more than one 
                                 HTO ID is associated with a sample then a | should be used to 
                                 separate the tags. 
                                  Here is an example hto_sample.csv file:
                                    sample_id,hashtag_ids
                                    sample1,HTO1
                                    sample2,HTO2|HTO3
                                  where:
                                    • sample_id: Unique sample ID for this hashtagged sample.
                                      Must not contain, whitespace, quote or comma characters.
                                      Each sample ID must be unique.
                                    • hashtag_ids: Unique HTO ID(s) that the sample is hashtagged
                                      with. Must match entries in the features.csv file.
                                  Example: --hto-sample hto_sample.csv
          --ocm-sample OCMSAMPLE
                                 An OCM sample CSV file containing sample to OCM barcode used 
                                 for multi analysis. The same OCM sample will be used on all 
                                 multi libraries. This file should contain a unique sample ID for 
                                 the sample, and the OCM ID(s) associated with that sample. If 
                                 more than one OCM ID is associated with a sample then a | should 
                                 be used to separate the tags.
                                  Here is an example ocm_sample.csv file:
                                    sample_id,ocm_barcode_ids
                                    sample1,OB1
                                    sample2,OB2
                                    sample3,OB3|OB4
                                  where:
                                    • sample_id: Unique sample ID for this tagged sample.
                                      Must not contain, whitespace, quote or comma characters.
                                      Each sample ID must be unique.
                                    • ocm_barcode_ids: Unique OCM ID(s) that the sample is tagged
                                      with. 
                                  Example: --ocm-sample ocm_sample.csv
          --probe-set PROBESET`
                                 The probe set reference CSV file that matches the probes that 
                                 were used in the capture. These are provided by 10x and can be 
                                 found at their downloads page.
                                 For those running the pipeline on the NIH's Biowulf, these 
                                 references can also be found in the cell-seek references section 
                                 in OpenOmics. The probe set reference files currently available 
                                 in OpenOmics are:
                                  - Human
                                    - Chromium_Human_Transcriptome_Probe_Set_v1.0_GRCh38-2020-A.csv
                                    - Chromium_Human_Transcriptome_Probe_Set_v1.0.1_GRCh38-2020-A.csv
                                    - Chromium_Human_Transcriptome_Probe_Set_v1.1.0_GRCh38-2024-A.csv
                                  - Mouse
                                    - Chromium_Mouse_Transcriptome_Probe_Set_v1.0.1_mm10-2020-A.csv
                                    - Chromium_Mouse_Transcriptome_Probe_Set_v1.1.1_GRCm39-2024-A.csv
                                  Example: --probe-set Chromium_Human_Transcriptome_Probe_Set_v1.1.0_GRCh38-2024-A.csv
          --probe-sample PROBESAMPLE
                                 A probe sample CSV file containing sample to probe barcode 
                                 information used for multi analysis. This file should be used if 
                                 multiplexing was performed. This file should contain a unique 
                                 sample ID for the sample, and the probe barcode(s) associated 
                                 with that sample. If more than one probe barcode is associated 
                                 with a sample then a | should be used to separate the tags. 
                                  Here is an example probe_sample.csv file:
                                    sample_id,probe_barcode_ids
                                    sample1,BC001
                                    sample2,BC002|BC003
                                  where:
                                    • sample_id: Unique sample ID for this tagged sample.
                                      Must not contain, whitespace, quote or comma characters.
                                      Each sample ID must be unique.
                                    • probe_barcode_ids: Unique probe barcode(s) that the sample 
                                      is tagged with. 
                                  Example: --probe-sample probe_sample.csv
          --chain  {{auto,TR,IG}}
                                Chain type for cellranger to run the analysis for. Default 
                                is set to 'auto'. Other options are 'TR' for T cell receptors, 
                                and 'IG' for B cell receptors.
                                  Example: --chain TR
          --exclude-introns
                                Exclude introns from the count alignment. This flag is
                                only applicable when dealing with gene expression related
                                data.
                                  Example: --exclude-introns
          --filter FILTER
                                Filter threshold file. A CSV file containing the different
                                thresholds to be applied for individual samples within the
                                project during the QC analysis. The file should contain a
                                header row with Sample as the column name for the sample IDs,
                                and the name of each metric that will be filtered along with
                                if it is the high or low threshold for that metric. Each row
                                is then the entries for each sample that the manual thresholds
                                will be applied. If no file is provided then the default
                                thresholds will be used. If a cell is left blank for a sample
                                then that sample would not be filtered based on that criteria.
                                This flag is currently only applicable when dealing with GEX
                                projects.
                                  Here is an example filter.csv file:
                                    Sample,nFeature_RNA_low,nFeature_RNA_high,percent.mito_high
                                    sample1,500,6000,15
                                    sample2,500,6000,5
                                    sample4,500,6000,5
                                  where:
                                    • Sample: Unique sample ID that should match the sample name
                                      used for Cell Ranger count.
                                    • nFeature_RNA_low,nFeature_RNA_high,percent.mito_high: Example
                                      entries that can be used for manual thresholding. The column
                                      names need to be formatted as metadataname_high/low. Entries
                                      that ends with high will be treated as the upper threshold.
                                      Entries that ends with low will be treated as the lower
                                      threshold. Valid metadata names include nCount_RNA,
                                      nFeature_RNA, and percent.mito.
                                  Example: --filter filter.csv
          --metadata METADATA
                                Sample metadata file. A CSV file containing sample level
                                metadata information that will be included as new metadata
                                columns during QC analysis. The file should contain a header
                                row with Sample as the column name for the sample IDs, and
                                the name of each metadata column that will be added to their
                                associated samples. Each row is then the entries for each
                                sample with the values that will be included as metadata. If
                                no file is provided then no metadata will be added to the
                                samples. If a cell is left blank for a sample then the metadata
                                column for that sample would be an empty string. This flag is
                                currently only applicable when dealing with GEX projects.
                                  Here is an example metadata.csv file:
                                    Sample,Type,batch
                                    sample1,tumor,1
                                    sample2,normal,1
                                    sample4,tumor,2
                                  where:
                                    • Sample: Unique sample ID that should match the sample name
                                      used for Cell Ranger count.
                                    • Type,batch: Example metadata entries that can be applied
                                      to the created Seurat object. The column names will be the
                                      resulting metadata entries, so it is recommended to use ones
                                      that do not overlap with column names that would be created
                                      automatically.
                                  Example: --metadata metadata.csv
          --create-bam
                                Create bam files. By default the no-bam flag is used when running
                                Cell Ranger. Use this flag to ensure that a bam file is created for
                                each sample during analysis. This flag is only applicable when
                                dealing with gene expression related data.
                                  Example: --create-bam
          --rename RENAME
                                Rename sample file. A CSV file containing the name of the FASTQ
                                file and the new name of the sample. Only the samples listed in
                                the CSV files will be run. This flag is only applicable when
                                dealing with GEX, VDJ, or ATAC projects. Renaming samples in the
                                other pipelines can be achieved via the libraries file.
                                  Here is an example rename.csv file:
                                    FASTQ,Name
                                    original_name1,new_name1
                                    original_name2,new_name2
                                    original_name3,new_name3
                                    original_name3-2,new_name3
                                    original_name4,original_name4
                                  where:
                                    • FASTQ: The name that is used in the FASTQ file
                                    • Name: Unique sample ID that is the sample name used for
                                      Cell Ranger count.
                                In this example, new_name3 has FASTQ files with two different
                                names. With this input, both sets of FASTQ files will be used
                                when processing the sample as new_name3. original_name4 will not
                                be renamed. Any FASTQ file that does not have the name
                                original_name1, original_name2, original_name3, or original_name4
                                will not be run.
                                  Example: --rename rename.csv
          --forcecells FORCECELLS
                                Force cells file. A CSV file containing the name of the sample
                                (the Cell Ranger outputted name) and the number of cells to
                                force the sample to. This flag is applicable when using the GEX,
                                CITE, MULTI, and ATAC pipelines. It will generally be used if
                                the first analysis run appears to do a poor job at estimating
                                the number of cells, and a re-run is needed to adjust the number
                                of cells in the sample.

                                This file can created in two different formats. The first one
                                can be used for the GEX, CITE, MULTI, and ATAC pipelines. It
                                will contain the name of the sample and the number of cells
                                to be forced to.
                                  Here is an example forcecells.csv file:
                                    Sample,Cells
                                    Sample1,3000
                                    Sample2,5000
                                  where:
                                    • Sample: The sample name used as the Cell Ranger output
                                    • Cells: The number of cells the sample should be forced to
                                In this example, Sample1 and Sample2 will be run while being forced
                                to have 3000 and 5000 cells respectively. Any other samples that
                                are processed will be run without using the force cells flag and
                                will use the default cell calling algorithm.

                                The second format is only compatible with the MULTI pipeline and
                                would be used when hashtag multiplexing is used and the number of
                                cells needs to be forced for a specific hashtagged sample.
                                  Here is an example forcecells.csv file:
                                    Name,Sample,Cells
                                    Library1,HTO_1,3000
                                    Library1,HTO_2,5000
                                  where:
                                    • Library: The name of the library that is provided as to Cell
                                      Ranger when running multi analysis. This should match the
                                      name that is given in the libraries.csv file.
                                    • Sample: The sample ID used for the associated hashtag. This
                                      will have to match the value used in the CMO sample file or
                                      the CMO reference file that is provided as input. If only a
                                      CMO reference file is provided, the pipeline default assigns
                                      each hashtag with the IDs of HTO_1, HTO_2, etc.
                                    • Cells: The number of cells the sample should be forced to
                                  In this example, the hashtags HTO_1 and HTO_2 in Library 1 will
                                  be run while being forced to 3000 and 5000 cells respectively.
                                  Any other libraries or samples that are processed will be run
                                  without using the force cells flag.

        {3}{4}Orchestration options:{5}
          --mode {{slurm,local}}
                                Method of execution. Defines the mode of execution.
                                Vaild options for this mode include: local or slurm.
                                Additional modes of exection are coming soon, default:
                                slurm.
                                Here is a brief description of each mode:
                                   • local: uses local method of execution. local runs
                                will run serially on compute instance. This is useful
                                for testing, debugging, or when a users does not have
                                access to a  high  performance  computing environment.
                                If this option is not provided, it will default to a
                                slurm mode of execution.
                                   • slurm: uses slurm execution backend. This method
                                will submit jobs to a  cluster  using sbatch. It is
                                recommended running the pipeline in this mode as it
                                will be significantly faster.
                                  Example: --mode slurm
          --job-name JOB_NAME
                                Overrides the name of the pipeline's master job. When
                                submitting the pipeline to a jobscheduler, this option
                                overrides the default name of the master job. This can
                                be useful for tracking the progress or status of a run,
                                default: pl:{2}.
                                  Example: --job-name {2}_03-14.1592
          --dry-run
                                Does not execute anything. Only displays what steps in
                                the pipeline remain or will be run.
                                  Example: --dry-run
          --silent
                                Silence standard output. This will reduces the amount
                                of information displayed to standard  output  when the
                                master job is submitted to the job scheduler. Only the
                                job id of the master job is returned.
                                  Example: --silent
          --singularity-cache SINGULARITY_CACHE
                                Overrides the $SINGULARITY_CACHEDIR variable. Images
                                from remote registries are cached locally on the file
                                system. By default, the singularity cache is set to:
                                '/path/to/output/directory/.singularity/'. Please note
                                that this cache cannot be shared across users.
                                  Example: --singularity-cache /data/$USER
          --sif-cache SIF_CACHE
                                Path where a local cache of SIFs are stored. This cache
                                can be shared across users if permissions are properly
                                setup. If a SIF does not exist in the SIF cache, the
                                image will be pulled from Dockerhub. {2} cache
                                sub command can be used to create a local SIF cache.
                                Please see {2} cache for more information.
                                   Example: --sif-cache /data/$USER/sifs/
          --tmp-dir TMP_DIR
                                Path on the file system for writing temporary output
                                files. By default, the temporary directory is set to
                                '/lscratch/$SLURM_JOBID' for backwards compatibility
                                with the NIH's Biowulf cluster; however, if you are
                                running the pipeline on another cluster, this option
                                will need to be specified. Ideally, this path should
                                point to a dedicated location on the filesystem for
                                writing tmp files. On many systems, this location is
                                set to somewhere in /scratch. If you need to inject a
                                variable into this string that should NOT be expanded,
                                please quote this options value in single quotes.
                                  Example: --tmp-dir '/scratch/$USER/'
          --threads THREADS
                                Max number of threads for local processes. It is
                                recommended setting this vaule to the maximum number
                                of CPUs available on the host machine, default: 2.
                                  Example: --threads: 16

        {3}{4}Misc Options:{5}
          -h, --help            Show usage information, help message, and exit.
                                  Example: --help
        """.format(styled_name, description, name, c.bold, c.url, c.end)
    )

    # Display example usage in epilog
    run_epilog = textwrap.dedent(
        """\
        {2}{3}Example:{4}
          # Step 1.) Grab an interactive node,
          # do not run on head node!
          srun -N 1 -n 1 --time=1:00:00 --mem=8gb  --cpus-per-task=2 --pty bash
          module purge
          module load singularity snakemake

          # Step 2A.) Dry-run the pipeline
          ./{0} run --input .tests/*.R?.fastq.gz \\
                         --output /data/$USER/output \\
                         --pipeline gex \\
                         --genome hg38 \\
                         --cellranger 8.0.0 \\
                         --mode slurm \\
                         --dry-run

          # Step 2B.) Run the {0} pipeline
          # The slurm mode will submit jobs to
          # the cluster. It is recommended running
          # the pipeline in this mode.
          ./{0} run --input .tests/*.R?.fastq.gz \\
                         --output /data/$USER/output \\
                         --pipeline gex \\
                         --genome hg38 \\
                         --cellranger 8.0.0 \\
                         --mode slurm

        {2}{3}Version:{4}
          {1}
        """.format(name, __version__, c.bold, c.url, c.end)
    )

    # Supressing help message of required args to overcome no sub-parser named groups
    subparser_run = subparsers.add_parser(
        "run",
        help="Run the {} pipeline with input files.".format(name),
        usage=argparse.SUPPRESS,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=required_run_options,
        epilog=run_epilog,
        add_help=False,
    )

    # Required Arguments
    # Input FastQ files
    subparser_run.add_argument(
        "--input",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=True,
        nargs="+",
        help=argparse.SUPPRESS,
    )

    # Output Directory, i.e
    # working directory
    subparser_run.add_argument(
        "--output",
        type=lambda option: os.path.abspath(os.path.expanduser(option)),
        required=True,
        help=argparse.SUPPRESS,
    )

    # Version of pipeline to run
    # i.e. GEX, VDJ, Multi, etc.
    subparser_run.add_argument(
        "--pipeline",
        type=str.lower,
        required=True,
        default="gex",
        choices=["gex", "cite", "multi", "vdj", "atac", "multiome"],
        help=argparse.SUPPRESS,
    )

    # Execution Method, run locally
    # on a compute node, submit to
    # SLURM job scheduler, etc.
    subparser_run.add_argument(
        "--genome",
        # type = str,
        required=True,
        type=lambda option: str(
            genome_options(subparser_run, option, ["hg38", "mm10", "hg2024", "mm2024"])
        ),
        # choices = ['hg38', 'mm10', 'hg2024', 'mm2024'],
        help=argparse.SUPPRESS,
    )

    # Optional Arguments
    # Add custom help message
    subparser_run.add_argument("-h", "--help", action="help", help=argparse.SUPPRESS)

    # Analysis options
    # Rename fastq files for
    # cellranger count
    subparser_run.add_argument(
        "--rename",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )
    # Input Libraries files for
    # cellranger count
    subparser_run.add_argument(
        "--libraries",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Input Libraries files
    subparser_run.add_argument(
        "--features",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Input CMO Reference files
    subparser_run.add_argument(
        "--cmo-reference",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Input CMO Sample files
    subparser_run.add_argument(
        "--cmo-sample",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Input HTO Sample files
    subparser_run.add_argument(
        "--hto-sample",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Input OCM Sample files
    subparser_run.add_argument(
        "--ocm-sample",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Input Probe Sample files
    subparser_run.add_argument(
        "--probe-sample",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Input Probe Set file
    subparser_run.add_argument(
        "--probe-set",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Chain type for cellranger to run the analysis for
    subparser_run.add_argument(
        "--chain",
        type=str,
        required=False,
        default="auto",
        choices=["auto", "TR", "IG"],
        help=argparse.SUPPRESS,
    )

    # Exclude introns from count
    subparser_run.add_argument(
        "--exclude-introns",
        action="store_true",
        required=False,
        default=False,
        help=argparse.SUPPRESS,
    )

    # Optional ambient RNA correction
    subparser_run.add_argument(
        "--ambient-rna-method",
        choices=["none", "soupx"],
        default="none",
        help=argparse.SUPPRESS,
    )

    # How to run Cell Ranger aggregate
    subparser_run.add_argument(
        "--aggregate",
        type=str.lower,
        required=False,
        default="",
        choices=["none", "mapped"],
        help=argparse.SUPPRESS,
    )

    # Thresholds to use for filtering in QC Analysis
    subparser_run.add_argument(
        "--filter",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Metadata to add to Seurat Objects during QC Analysis
    subparser_run.add_argument(
        "--metadata",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Create BAM file during run
    subparser_run.add_argument(
        "--create-bam",
        action="store_true",
        required=False,
        default=False,
        help=argparse.SUPPRESS,
    )

    # Create BAM file during run
    subparser_run.add_argument(
        "--cellranger",
        type=str.lower,
        required=True,
        default="",
        choices=[
            "2.0.1",
            "2.1.0",
            "2.2.0",
            "7.1.0",
            "7.2.0",
            "8.0.0",
            "9.0.0",
            "10.0.0",
        ],
        help=argparse.SUPPRESS,
    )

    # Number of cells to force samples to when running Cell Ranger analysis
    subparser_run.add_argument(
        "--forcecells",
        # Check if the file exists and if it is readable
        type=lambda file: permissions(parser, file, os.R_OK),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Orchestration Options
    # Execution Method, run locally
    # on a compute node or submit to
    # a supported job scheduler, etc.
    subparser_run.add_argument(
        "--mode",
        type=str,
        required=False,
        default="slurm",
        choices=["slurm", "local"],
        help=argparse.SUPPRESS,
    )

    # Name of master job
    subparser_run.add_argument(
        "--job-name",
        type=str,
        required=False,
        default="pl:{}".format(name),
        help=argparse.SUPPRESS,
    )

    # Dry-run
    # Do not execute the workflow,
    # prints what steps remain
    subparser_run.add_argument(
        "--dry-run",
        action="store_true",
        required=False,
        default=False,
        help=argparse.SUPPRESS,
    )

    # Silent output mode
    subparser_run.add_argument(
        "--silent",
        action="store_true",
        required=False,
        default=False,
        help=argparse.SUPPRESS,
    )

    # Singularity cache directory,
    # default uses output directory
    subparser_run.add_argument(
        "--singularity-cache",
        type=lambda option: check_cache(
            parser, os.path.abspath(os.path.expanduser(option))
        ),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Local SIF cache directory,
    # default pull from Dockerhub
    subparser_run.add_argument(
        "--sif-cache",
        type=lambda option: os.path.abspath(os.path.expanduser(option)),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Base directory to write
    # temporary/intermediate files
    subparser_run.add_argument(
        "--tmp-dir",
        type=str,
        required=False,
        default="/lscratch/$SLURM_JOBID/",
        help=argparse.SUPPRESS,
    )

    # Number of threads for the
    # pipeline's main proceess
    # This is only applicable for
    # local rules or when running
    # in local mode.
    subparser_run.add_argument(
        "--threads", type=int, required=False, default=2, help=argparse.SUPPRESS
    )

    # Sub-parser for the "unlock" sub-command
    # Grouped sub-parser arguments are currently
    # not supported: https://bugs.python.org/issue9341
    # Here is a work around to create more useful help message for named
    # options that are required! Please note: if a required arg is added the
    # description below should be updated (i.e. update usage and add new option)
    required_unlock_options = textwrap.dedent(
        """\
        {0}: {1}

        {3}{4}Synopsis:{5}
          $ {2} unlock [-h] --output OUTPUT

        Optional arguments are shown in square brackets above.

        {3}{4}Description:{5}
          If the pipeline fails ungracefully, it maybe required to unlock
        the working directory before proceeding again. Please verify that
        the pipeline is not running before running this command. If the
        pipeline is still running, the workflow manager will report the
        working directory is locked. This is normal behavior. Do NOT run
        this command if the pipeline is still running.

        {3}{4}Required arguments:{5}
          --output OUTPUT       Path to a previous run's output directory
                                to unlock. This will remove a lock on the
                                working directory. Please verify that the
                                pipeline is not running before running
                                this command.
                                  Example: --output /data/$USER/output

        {3}{4}Misc Options:{5}
          -h, --help            Show usage information, help message,
                                and exit.
                                  Example: --help
        """.format(styled_name, description, name, c.bold, c.url, c.end)
    )

    # Display example usage in epilog
    unlock_epilog = textwrap.dedent(
        """\
        {2}{3}Example:{4}
          # Unlock output directory of pipeline
          {0} unlock --output /data/$USER/output

        {2}{3}Version:{4}
          {1}
        """.format(name, __version__, c.bold, c.url, c.end)
    )

    # Supressing help message of required args to overcome no sub-parser named groups
    subparser_unlock = subparsers.add_parser(
        "unlock",
        help="Unlocks a previous runs output directory.",
        usage=argparse.SUPPRESS,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=required_unlock_options,
        epilog=unlock_epilog,
        add_help=False,
    )

    # Required Arguments
    # Output Directory (analysis working directory)
    subparser_unlock.add_argument(
        "--output", type=str, required=True, help=argparse.SUPPRESS
    )

    # Add custom help message
    subparser_unlock.add_argument("-h", "--help", action="help", help=argparse.SUPPRESS)

    # Sub-parser for the "unlock" sub-command
    # Grouped sub-parser arguments are currently
    # not supported: https://bugs.python.org/issue9341
    # Here is a work around to create more useful help message for named
    # options that are required! Please note: if a required arg is added the
    # description below should be updated (i.e. update usage and add new option)
    required_genome_options = textwrap.dedent(
        """\
        {0}: {1}

        {3}{4}Synopsis:{5}
          $ {2} genome [-h] \\
                [--gex GEX_PATH] [--vdj VDJ_PATH] \\
                [--atac ATAC_PATH] [--multiome MULTIOME_PATH] \\
                --name NAME

        Optional arguments are shown in square brackets above.

        {3}{4}Description:{5}
          If a genome reference that is not bundled with the pipeline
        needs to be used, a custom json file needs to be provided. This
        command helps to create the json file in a compatible format using
        the provided paths to the reference folders.

        {3}{4}Required arguments:{5}
          --name NAME           The name of the reference, which will be
                                used as the name of the generated reference
                                file.
                                  Example: --name NAME
        {3}{4}Optionl arguments:{5}
          --gex GEX_PATH
                                The path to the gene expression reference. This
                                reference is the same one used for the gene
                                expression capture for the GEX, CITE, and MULTI
                                pipelines. This flag is the only one needed to
                                supply the value for all three pipelines.
                                  Example: --gex GEX_PATH
          --vdj VDJ_PATH
                                The path to the vdj reference. This reference is
                                the one used for the VDJ capture for the VDJ and
                                MULTI pipelines. This flag is the only one
                                needed to supply the value for the two pipelines.
                                  Example: --vd VDJ_PATH
          --atac ATAC_PATH
                                The path to the atac reference. This reference is
                                used in the ATAC pipeline.
                                  Example: --atac ATAC_PATH
          --multiome MULTIOME_PATH
                                The path to the multiome reference. This reference
                                is used in the multiome pipeline.
                                  Example: -- MULTIOME_PATH
        {3}{4}Misc Options:{5}
          -h, --help            Show usage information, help message,
                                and exit.
                                  Example: --help
        """.format(styled_name, description, name, c.bold, c.url, c.end)
    )

    # Display example usage in epilog
    genome_epilog = textwrap.dedent(
        """\
        {2}{3}Example:{4}
          # Create custom json file
          {0} genome --name NAME --gex /data/REF_PATH_GEX

        {2}{3}Version:{4}
          {1}
        """.format(name, __version__, c.bold, c.url, c.end)
    )

    # Supressing help message of required args to overcome no sub-parser named groups
    subparser_genome = subparsers.add_parser(
        "genome",
        help="Create a custom genome json file using provided reference paths.",
        usage=argparse.SUPPRESS,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=required_genome_options,
        epilog=genome_epilog,
        add_help=False,
    )

    # Name of the reference and generated output file
    subparser_genome.add_argument(
        "--name", type=str, required=True, help=argparse.SUPPRESS
    )

    # Path to the GEX/CITE reference
    subparser_genome.add_argument(
        "--gex",
        type=lambda option: (
            os.path.abspath(os.path.expanduser(option))
            if os.path.isdir(option)
            else parser.error(
                "Path '{}' does not exist! Failed to provide valid input.".format(
                    option
                )
            )
        ),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Path to the VDJ reference
    subparser_genome.add_argument(
        "--vdj",
        type=lambda option: (
            os.path.abspath(os.path.expanduser(option))
            if os.path.isdir(option)
            else parser.error(
                "Path '{}' does not exist! Failed to provide valid input.".format(
                    option
                )
            )
        ),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Path to the ATAC reference
    subparser_genome.add_argument(
        "--atac",
        type=lambda option: (
            os.path.abspath(os.path.expanduser(option))
            if os.path.isdir(option)
            else parser.error(
                "Path '{}' does not exist! Failed to provide valid input.".format(
                    option
                )
            )
        ),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Path to the ARC reference
    subparser_genome.add_argument(
        "--multiome",
        type=lambda option: (
            os.path.abspath(os.path.expanduser(option))
            if os.path.isdir(option)
            else parser.error(
                "Path '{}' does not exist! Failed to provide valid input.".format(
                    option
                )
            )
        ),
        required=False,
        help=argparse.SUPPRESS,
    )

    # Add custom help message
    subparser_genome.add_argument("-h", "--help", action="help", help=argparse.SUPPRESS)

    # Sub-parser for the "cache" sub-command
    # Grouped sub-parser arguments are
    # not supported: https://bugs.python.org/issue9341
    # Here is a work around to create more useful help message for named
    # options that are required! Please note: if a required arg is added the
    # description below should be updated (i.e. update usage and add new option)
    required_cache_options = textwrap.dedent(
        """\
        {0}: {1}

        {3}{4}Synopsis:{5}
          $ {2} cache [-h] [--dry-run] --sif-cache SIF_CACHE

        Optional arguments are shown in square brackets above.

        {3}{4}Description:{5}
          Creates a local cache resources hosted on DockerHub or AWS S3.
        These resources are normally pulled onto the filesystem when the
        pipeline runs; however, due to network issues or DockerHub pull
        rate limits, it may make sense to pull the resources once so a
        shared cache can be created. It is worth noting that a singularity
        cache cannot normally be shared across users. Singularity strictly
        enforces that a cache is owned by the user. To get around this
        issue, the cache subcommand can be used to create local SIFs on
        the filesystem from images on DockerHub.

        {3}{4}Required arguments:{5}
          --sif-cache SIF_CACHE
                                Path where a local cache of SIFs will be
                                stored. Images defined in containers.json
                                will be pulled into the local filesystem.
                                The path provided to this option can be
                                passed to the --sif-cache option of the
                                run sub command. Please see {2}
                                run sub command for more information.
                                  Example: --sif-cache /data/$USER/cache

        {3}{4}Orchestration options:{5}
          --dry-run             Does not execute anything. Only displays
                                what remote resources would be pulled.
                                  Example: --dry-run

        {3}{4}Misc Options:{5}
          -h, --help            Show usage information, help message,
                                and exits.
                                  Example: --help

        """.format(styled_name, description, name, c.bold, c.url, c.end)
    )

    # Display example usage in epilog
    cache_epilog = textwrap.dedent(
        """\
        {2}{3}Example:{4}
          # Cache remote resources of pipeline
          {0} cache --sif-cache /data/$USER/cache

        {2}{3}Version:{4}
          {1}
        """.format(name, __version__, c.bold, c.url, c.end)
    )

    # Supressing help message of required args to overcome no sub-parser named groups
    subparser_cache = subparsers.add_parser(
        "cache",
        help="Cache remote resources locally.",
        usage=argparse.SUPPRESS,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=required_cache_options,
        epilog=cache_epilog,
        add_help=False,
    )

    # Required Arguments
    # Output Directory (analysis working directory)
    subparser_cache.add_argument(
        "--sif-cache",
        type=lambda option: os.path.abspath(os.path.expanduser(option)),
        required=True,
        help=argparse.SUPPRESS,
    )

    # Optional Arguments
    # Dry-run cache command (do not pull any remote resources)
    subparser_cache.add_argument(
        "--dry-run",
        action="store_true",
        required=False,
        default=False,
        help=argparse.SUPPRESS,
    )

    # Add custom help message
    subparser_cache.add_argument("-h", "--help", action="help", help=argparse.SUPPRESS)

    # shinycell builder subcommands
    required_shinycell_options = textwrap.dedent(
        """\
        {0}: {1}

        {3}{4}Synopsis:{5}
        $ {2} shinycell [-h] \\
                -j OBJ -o OUTDIR --proj PROJECT \\
                [--markers MARKERS] [--cluster_labels CLUSTER_LABELS] \\
                [--rmmeta META1,META2] [--defred DEFAULTRED] \\
                [-l MAXLEVELS] [--filesonly] [--codesonly] [-a ASSAYS]

        Optional arguments are shown in square brackets above.

        {3}{4}Description:{5}
        Builds a ShinyCell2 application from a Seurat RDS object. The command
        validates inputs, creates configuration, and writes the files and/or
        Shiny code required to deploy an interactive single-cell web app.

        {3}{4}Required arguments:{5}
        -j, --obj OBJ         Path to an RDS file produced with saveRDS()
                                that contains a Seurat object. The script will
                                attempt to read and validate the Seurat object.
                                Example: -j seurat_object.rds
        -o, --outdir OUTDIR   Output directory where the Shiny app files and
                                generated code will be written. Directory will be
                                created if it does not exist.
                                Example: -o /srv/shiny-server/my_app
        --proj PROJECT        Project name used as the app title and prefix
                                for generated assets.
                                Example: --proj NCBR-34

        {3}{4}Optional arguments:{5}
        --markers MARKERS     Path to a plain text file with marker genes,
                                one gene per line. When provided, requires
                                --cluster_labels to be set as well. Used to
                                generate marker-based DEG pages or panels.
                                Default: None (No DEG pages/panels will be created)
                                Example: --markers markers.txt
        --cluster_labels NAME
                                Metadata column name in seurat_obj@meta.data that
                                contains cluster identities corresponding to the
                                markers file. Required when --markers is provided.
                                Default: None (No DEG pages/panels will be created)
                                Example: --cluster_labels seurat_clusters
        --rmmeta META1,META2  Comma-delimited list of metadata column names to
                                drop from the Seurat object before deploying.
                                Useful to remove large or sensitive columns.
                                Default: None (No metadata columns are removed)
                                Example: --rmmeta barcode,unwanted_col
        --defred DEFAULTRED   Default dimensionality reduction to use for the
                                app (must exist in Seurat object reductions).
                                The code expects the reduction to expose a key
                                so UMAP1/UMAP2 (or <KEY>1/<KEY>2) coordinate names
                                can be derived.
                                Default: None (Uses all active reduction)
                                Example: --defred UMAP
        -l, --maxlevels INT   Maximum allowed number of factor levels for
                                categorical metadata fields. Fields exceeding this
                                will be treated as continuous / filtered out.
                                Default: 50
                                Example: -l 50
        -a, --assay ASSAYS    Comma-delimited list of assays to include in the
                                Shiny app (e.g. RNA,spatial,ATAC). If omitted,
                                default assays present in the Seurat object are
                                used when compatible.
                                Default: None (Uses all assays in Seurat object)
                                Example: -a RNA,spatial

        {3}{4}Misc Options:{5}
        -h, --help            Show usage information, help message, and exit.
                                    Example: --help
        """.format(styled_name, description, name, c.bold, c.url, c.end)
    )

    shinycell_epilog = textwrap.dedent(
        """\
        {2}{3}Examples:{4}
        # Basic: create a ShinyCell2 app from a Seurat RDS file
        ./{0} shinycell -j seurat_obj.rds -o ./ShinyCell2_test --proj NCBR-34

        # Include markers and cluster labels (markers file must exist)
        ./{0} shinycell -j seurat_obj.rds -o ./ShinyCell2_markers \\
                        --proj NCBR-34 --markers markers.txt \\
                        --cluster_labels seurat_clusters

        # Specify assays to include and drop unwanted metadata columns
        ./{0} shinycell -j seurat_obj.rds -o ./ShinyCell2_assays \\
                        --proj NCBR-34 -a RNA,spatial --rmmeta large_matrix

        {2}{3}Notes:{4}
        - When using --markers you MUST also specify --cluster_labels.
        - --defred must correspond to an existing reduction name in the Seurat
            object; the script will resolve the reduction key to form e.g. UMAP1/2.
        - Use --rmmeta to remove columns that are large, private, or not
            appropriate for public deployment.

        {2}{3}Version:{4}
        {1}
        """.format(name, __version__, c.bold, c.url, c.end)
    )
    subparser_shinycell = subparsers.add_parser(
        "shinycell",
        help="Run the ShinyCell2 application creation pipeline.",
        usage=argparse.SUPPRESS,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=required_shinycell_options,
        epilog=shinycell_epilog,
        add_help=False,
    )
    subparser_shinycell.add_argument(
        "-j",
        "--obj",
        dest="object",
        help=argparse.SUPPRESS,
        required=True,
    )
    subparser_shinycell.add_argument(
        "-o",
        "--outdir",
        dest="outdir",
        help=argparse.SUPPRESS,
        required=True,
    )
    subparser_shinycell.add_argument(
        "--proj",
        dest="project",
        help=argparse.SUPPRESS,
        required=True,
    )
    subparser_shinycell.add_argument(
        "--markers",
        dest="markers",
        help=argparse.SUPPRESS,
        default=None,
    )
    subparser_shinycell.add_argument(
        "--cluster_labels",
        dest="cluster_labels",
        help=argparse.SUPPRESS,
        default=None,
    )
    subparser_shinycell.add_argument(
        "--rmmeta",
        dest="meta_to_rm",
        help=argparse.SUPPRESS,
        default=None,
        metavar="<META.VAR.TO.RM>, [<META.VAR.TO.RM>,] [<META.VAR.TO.RM>,] [...]",
    )
    subparser_shinycell.add_argument(
        "--defred",
        dest="defaultreduction",
        help=(
            "The default reduction to use with ShinyCell2, this value must exist in Seurat::DefaultDimReduc(obj).\n"
            "The two major principal components for this reductions must be labeled the same label with a 1 and a 2 trailing it.\n"
            "i.e. defaultreduction = UMAP, UMAP1 and UMAP2 are the two components that must exist"
        ),
        default=None,
    )
    subparser_shinycell.add_argument(
        "-l",
        "--maxlevels",
        dest="max_levels",
        type=int,
        help="The maximum allowable amount levels/factors of categorical values (int).",
        default=50,
    )
    subparser_shinycell.add_argument(
        "-a",
        "--assay",
        dest="assaytouse",
        metavar="<ASSAY1>, <ASSAY2>, <ASSAY3>, ...",
        help=(
            "The assay to utilize for ShinyCell2 web application.\n"
            "Comma delimit multiple assays in a single string e.g.: RNA,spatial,ATAC,etc."
        ),
        default=None,
    )
    subparser_shinycell.add_argument(
        "--tmp-dir",
        dest="tmpdir",
        type=str,
        required=False,
        default="/lscratch/$SLURM_JOBID/",
        help=argparse.SUPPRESS,
    )
    subparser_shinycell.add_argument(
        "--mode",
        type=str,
        required=False,
        default="slurm",
        choices=["slurm", "local"],
        help=argparse.SUPPRESS,
    )
    subparser_shinycell.add_argument(
        "--threads", type=int, required=False, default=2, help=argparse.SUPPRESS
    )
    subparser_shinycell.add_argument(
        "--job-name",
        type=str,
        required=False,
        default="pl:{}:shinycell".format(name),
        help=argparse.SUPPRESS,
    )
    subparser_shinycell.add_argument(
        "--silent",
        action="store_true",
        required=False,
        default=False,
        help=argparse.SUPPRESS,
    )
    subparser_shinycell.add_argument(
        "-h", "--help", action="help", help=argparse.SUPPRESS
    )

    # Define handlers for each sub-parser
    subparser_run.set_defaults(func=run)
    subparser_unlock.set_defaults(func=unlock)
    subparser_cache.set_defaults(func=cache)
    subparser_genome.set_defaults(func=genome)
    subparser_shinycell.set_defaults(func=shinycell)

    # Parse command-line args
    args = parser.parse_args()
    return args


def main():
    # Sanity check for usage
    if len(sys.argv) == 1:
        # Nothing was provided
        fatal("Invalid usage: {} [-h] [--version] ...".format(_name))

    # Collect args for sub-command
    args = parsed_arguments(name=_name, description=_description)

    # Display version information
    err("{} ({})".format(_name, __version__))

    # Mediator method to call sub-command's set handler function
    args.func(args)


if __name__ == "__main__":
    main()
