"""
repo manager cli
"""
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import glob
import json
import os
import sys
import textwrap
import traceback
import urlparse

import ga4gh.cli as cli
import ga4gh.datamodel.bio_metadata as bio_metadata
import ga4gh.datamodel.datasets as datasets
import ga4gh.datamodel.genotype_phenotype as genotype_phenotype
import ga4gh.datamodel.ontologies as ontologies
import ga4gh.datamodel.reads as reads
import ga4gh.datamodel.references as references
import ga4gh.datamodel.rna_quantification as rna_quantification
import ga4gh.datamodel.sequence_annotations as sequence_annotations
import ga4gh.datamodel.variants as variants
import ga4gh.datarepo as datarepo
import ga4gh.exceptions as exceptions
import ga4gh.repo.rnaseq2ga as rnaseq2ga


def getNameFromPath(filePath):
    """
    Returns the filename of the specified path without its extensions.
    This is usually how we derive the default name for a given object.
    """
    if len(filePath) == 0:
        raise ValueError("Cannot have empty path for name")
    fileName = os.path.split(os.path.normpath(filePath))[1]
    # We need to handle things like .fa.gz, so we can't use
    # os.path.splitext
    ret = fileName.split(".")[0]
    assert ret != ""
    return ret


def getRawInput(display):
    """
    Wrapper around raw_input; put into separate function so that it
    can be easily mocked for tests.
    """
    return raw_input(display)


class RepoManager(object):
    """
    Class that provide command line functionality to manage a
    data repository.
    """
    def __init__(self, args):
        self._args = args
        self._registryPath = args.registryPath
        self._repo = datarepo.SqlDataRepository(self._registryPath)

    def _confirmDelete(self, objectType, name, func):
        if self._args.force:
            func()
        else:
            displayString = (
                "Are you sure you want to delete the {} '{}'? "
                "[y|N] ".format(objectType, name))
            userResponse = getRawInput(displayString)
            if userResponse.strip() == 'y':
                func()
            else:
                print("Aborted")

    def _updateRepo(self, func, *args, **kwargs):
        """
        Runs the specified function that updates the repo with the specified
        arguments. This method ensures that all updates are transactional,
        so that if any part of the update fails no changes are made to the
        repo.
        """
        # TODO how do we make this properly transactional?
        self._repo.open(datarepo.MODE_WRITE)
        try:
            func(*args, **kwargs)
            self._repo.commit()
        finally:
            self._repo.close()

    def _openRepo(self):
        if not self._repo.exists():
            raise exceptions.RepoManagerException(
                "Repo '{}' does not exist. Please create a new repo "
                "using the 'init' command.".format(self._registryPath))
        self._repo.open(datarepo.MODE_READ)

    def _checkSequenceOntology(self, ontology):
        so = ontologies.SEQUENCE_ONTOLOGY_PREFIX
        if ontology.getOntologyPrefix() != so:
            raise exceptions.RepoManagerException(
                "Ontology '{}' does not have ontology prefix '{}'".format(
                    ontology.getName(), so))

    def _getFilePath(self, filePath, useRelativePath):
        return filePath if useRelativePath else os.path.abspath(filePath)

    def init(self):
        forceMessage = (
            "Respository '{}' already exists. Use --force to overwrite")
        if self._repo.exists():
            if self._args.force:
                self._repo.delete()
            else:
                raise exceptions.RepoManagerException(
                    forceMessage.format(self._registryPath))
        self._updateRepo(self._repo.initialise)

    def list(self):
        """
        Lists the contents of this repo.
        """
        self._openRepo()
        # TODO this is _very_ crude. We need much more options and detail here.
        self._repo.printSummary()

    def verify(self):
        """
        Checks that the data pointed to in the repository works and
        we don't have any broken URLs, missing files, etc.
        """
        self._openRepo()
        self._repo.verify()

    def addOntology(self):
        """
        Adds a new Ontology to this repo.
        """
        self._openRepo()
        name = self._args.name
        filePath = self._getFilePath(self._args.filePath,
                                     self._args.relativePath)
        if name is None:
            name = getNameFromPath(filePath)
        ontology = ontologies.Ontology(name)
        ontology.populateFromFile(filePath)
        self._updateRepo(self._repo.insertOntology, ontology)

    def addDataset(self):
        """
        Adds a new dataset into this repo.
        """
        self._openRepo()
        dataset = datasets.Dataset(self._args.datasetName)
        dataset.setDescription(self._args.description)
        dataset.setInfo(json.loads(self._args.info))
        self._updateRepo(self._repo.insertDataset, dataset)

    def addReferenceSet(self):
        """
        Adds a new reference set into this repo.
        """
        self._openRepo()
        name = self._args.name
        filePath = self._getFilePath(self._args.filePath,
                                     self._args.relativePath)
        if name is None:
            name = getNameFromPath(self._args.filePath)
        referenceSet = references.HtslibReferenceSet(name)
        referenceSet.populateFromFile(filePath)
        referenceSet.setDescription(self._args.description)
        referenceSet.setNcbiTaxonId(self._args.ncbiTaxonId)
        referenceSet.setIsDerived(self._args.isDerived)
        referenceSet.setAssemblyId(self._args.assemblyId)
        sourceAccessions = []
        if self._args.sourceAccessions is not None:
            sourceAccessions = self._args.sourceAccessions.split(",")
        referenceSet.setSourceAccessions(sourceAccessions)
        referenceSet.setSourceUri(self._args.sourceUri)
        self._updateRepo(self._repo.insertReferenceSet, referenceSet)

    def addReadGroupSet(self):
        """
        Adds a new ReadGroupSet into this repo.
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        dataUrl = self._args.dataFile
        indexFile = self._args.indexFile
        parsed = urlparse.urlparse(dataUrl)
        # TODO, add https support and others when they have been
        # tested.
        if parsed.scheme in ['http', 'ftp']:
            if indexFile is None:
                raise exceptions.MissingIndexException(dataUrl)
        else:
            if indexFile is None:
                indexFile = dataUrl + ".bai"
            dataUrl = self._getFilePath(self._args.dataFile,
                                        self._args.relativePath)
            indexFile = self._getFilePath(indexFile, self._args.relativePath)
        name = self._args.name
        if self._args.name is None:
            name = getNameFromPath(dataUrl)
        readGroupSet = reads.HtslibReadGroupSet(dataset, name)
        readGroupSet.populateFromFile(dataUrl, indexFile)
        referenceSetName = self._args.referenceSetName
        if referenceSetName is None:
            # Try to find a reference set name from the BAM header.
            referenceSetName = readGroupSet.getBamHeaderReferenceSetName()
        referenceSet = self._repo.getReferenceSetByName(referenceSetName)
        readGroupSet.setReferenceSet(referenceSet)
        self._updateRepo(self._repo.insertReadGroupSet, readGroupSet)

    def addVariantSet(self):
        """
        Adds a new VariantSet into this repo.
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        dataUrls = self._args.dataFiles
        name = self._args.name
        if len(dataUrls) == 1:
            if self._args.name is None:
                name = getNameFromPath(dataUrls[0])
            if os.path.isdir(dataUrls[0]):
                # Read in the VCF files from the directory.
                # TODO support uncompressed VCF and BCF files
                vcfDir = dataUrls[0]
                pattern = os.path.join(vcfDir, "*.vcf.gz")
                dataUrls = glob.glob(pattern)
                if len(dataUrls) == 0:
                    raise exceptions.RepoManagerException(
                        "Cannot find any VCF files in the directory "
                        "'{}'.".format(vcfDir))
                dataUrls[0] = self._getFilePath(dataUrls[0],
                                                self._args.relativePath)
        elif self._args.name is None:
            raise exceptions.RepoManagerException(
                "Cannot infer the intended name of the VariantSet when "
                "more than one VCF file is provided. Please provide a "
                "name argument using --name.")
        parsed = urlparse.urlparse(dataUrls[0])
        if parsed.scheme not in ['http', 'ftp']:
            dataUrls = map(lambda url: self._getFilePath(
                url, self._args.relativePath), dataUrls)
        # Now, get the index files for the data files that we've now obtained.
        indexFiles = self._args.indexFiles
        if indexFiles is None:
            # First check if all the paths exist locally, as they must
            # if we are making a default index path.
            for dataUrl in dataUrls:
                if not os.path.exists(dataUrl):
                    raise exceptions.MissingIndexException(
                        "Cannot find file '{}'. All variant files must be "
                        "stored locally if the default index location is "
                        "used. If you are trying to create a VariantSet "
                        "based on remote URLs, please download the index "
                        "files to the local file system and provide them "
                        "with the --indexFiles argument".format(dataUrl))
            # We assume that the indexes are made by adding .tbi
            indexSuffix = ".tbi"
            # TODO support BCF input properly here by adding .csi
            indexFiles = [filename + indexSuffix for filename in dataUrls]
        indexFiles = map(lambda url: self._getFilePath(
            url, self._args.relativePath), indexFiles)
        variantSet = variants.HtslibVariantSet(dataset, name)
        variantSet.populateFromFile(dataUrls, indexFiles)
        # Get the reference set that is associated with the variant set.
        referenceSetName = self._args.referenceSetName
        if referenceSetName is None:
            # Try to find a reference set name from the VCF header.
            referenceSetName = variantSet.getVcfHeaderReferenceSetName()
        if referenceSetName is None:
            raise exceptions.RepoManagerException(
                "Cannot infer the ReferenceSet from the VCF header. Please "
                "specify the ReferenceSet to associate with this "
                "VariantSet using the --referenceSetName option")
        referenceSet = self._repo.getReferenceSetByName(referenceSetName)
        variantSet.setReferenceSet(referenceSet)

        # Now check for annotations
        annotationSets = []
        if variantSet.isAnnotated() and self._args.addAnnotationSets:
            ontologyName = self._args.ontologyName
            if ontologyName is None:
                raise exceptions.RepoManagerException(
                    "A sequence ontology name must be provided")
            ontology = self._repo.getOntologyByName(ontologyName)
            self._checkSequenceOntology(ontology)
            for annotationSet in variantSet.getVariantAnnotationSets():
                annotationSet.setOntology(ontology)
                annotationSets.append(annotationSet)

        # Add the annotation sets and the variant set as an atomic update
        def updateRepo():
            self._repo.insertVariantSet(variantSet)
            for annotationSet in annotationSets:
                self._repo.insertVariantAnnotationSet(annotationSet)
        self._updateRepo(updateRepo)

    def addPhenotypeAssociationSet(self):
        """
        Adds a new phenotype association set to this repo.
        """
        self._openRepo()
        name = self._args.name
        if name is None:
            name = getNameFromPath(self._args.dirPath)
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        phenotypeAssociationSet = \
            genotype_phenotype.RdfPhenotypeAssociationSet(
                dataset, name, self._args.dirPath)
        self._updateRepo(
            self._repo.insertPhenotypeAssociationSet,
            phenotypeAssociationSet)

    def removePhenotypeAssociationSet(self):
        """
        Removes a phenotype association set from the repo
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        phenotypeAssociationSet = dataset.getPhenotypeAssociationSetByName(
            self._args.name)

        def func():
            self._updateRepo(
                self._repo.removePhenotypeAssociationSet,
                phenotypeAssociationSet)
        self._confirmDelete(
            "PhenotypeAssociationSet",
            phenotypeAssociationSet.getLocalId(),
            func)

    def removeReferenceSet(self):
        """
        Removes a referenceSet from the repo.
        """
        self._openRepo()
        referenceSet = self._repo.getReferenceSetByName(
            self._args.referenceSetName)

        def func():
            self._updateRepo(self._repo.removeReferenceSet, referenceSet)
        self._confirmDelete("ReferenceSet", referenceSet.getLocalId(), func)

    def removeReadGroupSet(self):
        """
        Removes a readGroupSet from the repo.
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        readGroupSet = dataset.getReadGroupSetByName(
            self._args.readGroupSetName)

        def func():
            self._updateRepo(self._repo.removeReadGroupSet, readGroupSet)
        self._confirmDelete("ReadGroupSet", readGroupSet.getLocalId(), func)

    def removeVariantSet(self):
        """
        Removes a variantSet from the repo.
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        variantSet = dataset.getVariantSetByName(self._args.variantSetName)

        def func():
            self._updateRepo(self._repo.removeVariantSet, variantSet)
        self._confirmDelete("VariantSet", variantSet.getLocalId(), func)

    def removeDataset(self):
        """
        Removes a dataset from the repo.
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)

        def func():
            self._updateRepo(self._repo.removeDataset, dataset)
        self._confirmDelete("Dataset", dataset.getLocalId(), func)

    def addFeatureSet(self):
        """
        Adds a new feature set into this repo
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        filePath = self._getFilePath(self._args.filePath,
                                     self._args.relativePath)
        name = getNameFromPath(self._args.filePath)
        featureSet = sequence_annotations.Gff3DbFeatureSet(
            dataset, name)
        referenceSetName = self._args.referenceSetName
        if referenceSetName is None:
            raise exceptions.RepoManagerException(
                "A reference set name must be provided")
        referenceSet = self._repo.getReferenceSetByName(referenceSetName)
        featureSet.setReferenceSet(referenceSet)
        ontologyName = self._args.ontologyName
        if ontologyName is None:
            raise exceptions.RepoManagerException(
                "A sequence ontology name must be provided")
        ontology = self._repo.getOntologyByName(ontologyName)
        self._checkSequenceOntology(ontology)
        featureSet.setOntology(ontology)
        featureSet.populateFromFile(filePath)
        self._updateRepo(self._repo.insertFeatureSet, featureSet)

    def removeFeatureSet(self):
        """
        Removes a feature set from this repo
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        featureSet = dataset.getFeatureSetByName(self._args.featureSetName)

        def func():
            self._updateRepo(self._repo.removeFeatureSet, featureSet)
        self._confirmDelete("FeatureSet", featureSet.getLocalId(), func)

    def addBioSample(self):
        """
        Adds a new biosample into this repo
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        bioSample = bio_metadata.BioSample(
            dataset, self._args.bioSampleName)
        bioSample.populateFromJson(self._args.bioSample)
        self._updateRepo(self._repo.insertBioSample, bioSample)

    def removeBioSample(self):
        """
        Removes a biosample from this repo
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        bioSample = dataset.getBioSampleByName(self._args.bioSampleName)

        def func():
            self._updateRepo(self._repo.removeBioSample, bioSample)
        self._confirmDelete("BioSample", bioSample.getLocalId(), func)

    def addIndividual(self):
        """
        Adds a new individual into this repo
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        individual = bio_metadata.Individual(
            dataset, self._args.individualName)
        individual.populateFromJson(self._args.individual)
        self._updateRepo(self._repo.insertIndividual, individual)

    def removeIndividual(self):
        """
        Removes an individual from this repo
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        individual = dataset.getIndividualByName(self._args.individualName)

        def func():
            self._updateRepo(self._repo.removeIndividual, individual)
        self._confirmDelete("Individual", individual.getLocalId(), func)

    def removeOntology(self):
        """
        Removes an ontology from the repo.
        """
        self._openRepo()
        ontology = self._repo.getOntologyByName(self._args.ontologyName)

        def func():
            self._updateRepo(self._repo.removeOntology, ontology)
        self._confirmDelete("Ontology", ontology.getName(), func)

    def addRnaQuantification(self):
        """
        Adds an rnaQuantification into this repo
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        bioSampleId = ""
        if self._args.bioSampleName:
            bioSample = dataset.getBioSampleByName(self._args.bioSampleName)
            bioSampleId = bioSample.getId()
        if self._args.name is None:
            name = getNameFromPath(self._args.quantificationFilePath)
        else:
            name = self._args.name
        # TODO: programs not fully supported by GA4GH yet
        programs = ""
        featureType = "gene"
        if self._args.transcript:
            featureType = "transcript"
        rnaseq2ga.rnaseq2ga(
            self._args.quantificationFilePath, self._args.filePath, name,
            self._args.format, dataset=dataset, featureType=featureType,
            description=self._args.description, programs=programs,
            featureSetNames=self._args.featureSetNames,
            readGroupSetNames=self._args.readGroupSetName,
            bioSampleId=bioSampleId)

    def initRnaQuantificationSet(self):
        """
        Initialize an empty RNA quantification set
        """
        store = rnaseq2ga.RnaSqliteStore(self._args.filePath)
        store.createTables()

    def addRnaQuantificationSet(self):
        """
        Adds an rnaQuantificationSet into this repo
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        if self._args.name is None:
            name = getNameFromPath(self._args.filePath)
        else:
            name = self._args.name
        rnaQuantificationSet = rna_quantification.SqliteRnaQuantificationSet(
            dataset, name)
        referenceSetName = self._args.referenceSetName
        if referenceSetName is None:
            raise exceptions.RepoManagerException(
                "A reference set name must be provided")
        referenceSet = self._repo.getReferenceSetByName(referenceSetName)
        rnaQuantificationSet.setReferenceSet(referenceSet)
        rnaQuantificationSet.populateFromFile(self._args.filePath)
        self._updateRepo(
            self._repo.insertRnaQuantificationSet, rnaQuantificationSet)

    def removeRnaQuantificationSet(self):
        """
        Removes an rnaQuantificationSet from this repo
        """
        self._openRepo()
        dataset = self._repo.getDatasetByName(self._args.datasetName)
        rnaQuantSet = dataset.getRnaQuantificationSetByName(
            self._args.rnaQuantificationSetName)

        def func():
            self._updateRepo(self._repo.removeRnaQuantificationSet,
                             rnaQuantSet)
        self._confirmDelete(
            "RnaQuantificationSet", rnaQuantSet.getLocalId(), func)

    #
    # Methods to simplify adding common arguments to the parser.
    #

    @classmethod
    def addRepoArgument(cls, subparser):
        subparser.add_argument(
            "registryPath",
            help="the location of the registry database")

    @classmethod
    def addForceOption(cls, subparser):
        subparser.add_argument(
            "-f", "--force", action='store_true',
            default=False, help="do not prompt for confirmation")

    @classmethod
    def addRelativePathOption(cls, subparser):
        subparser.add_argument(
            "-r", "--relativePath", action='store_true',
            default=False, help="store relative path in database")

    @classmethod
    def addDescriptionOption(cls, subparser, objectType):
        subparser.add_argument(
            "-d", "--description", default="",
            help="The human-readable description of the {}.".format(
                objectType))

    @classmethod
    def addDatasetNameArgument(cls, subparser):
        subparser.add_argument(
            "datasetName", help="the name of the dataset")

    @classmethod
    def addDatasetInfoArgument(cls, subparser):
        subparser.add_argument(
            "-i", "--info", default="{}", help="the info of the dataset")

    @classmethod
    def addReferenceSetNameOption(cls, subparser, objectType):
        helpText = (
            "the name of the reference set to associate with this {}"
        ).format(objectType)
        subparser.add_argument(
            "-R", "--referenceSetName", default=None, help=helpText)

    @classmethod
    def addSequenceOntologyNameOption(cls, subparser, objectType):
        helpText = (
            "the name of the sequence ontology instance used to "
            "translate ontology term names to IDs in this {}"
        ).format(objectType)
        subparser.add_argument(
            "-O", "--ontologyName", default=None, help=helpText)

    @classmethod
    def addOntologyNameArgument(cls, subparser):
        subparser.add_argument(
            "ontologyName",
            help="the name of the ontology")

    @classmethod
    def addReadGroupSetNameArgument(cls, subparser):
        subparser.add_argument(
            "readGroupSetName",
            help="the name of the read group set")

    @classmethod
    def addVariantSetNameArgument(cls, subparser):
        subparser.add_argument(
            "variantSetName",
            help="the name of the variant set")

    @classmethod
    def addFeatureSetNameArgument(cls, subparser):
        subparser.add_argument(
            "featureSetName",
            help="the name of the feature set")

    @classmethod
    def addIndividualNameArgument(cls, subparser):
        subparser.add_argument(
            "individualName",
            help="the name of the individual")

    @classmethod
    def addBioSampleNameArgument(cls, subparser):
        subparser.add_argument(
            "bioSampleName",
            help="the name of the biosample")

    @classmethod
    def addBioSampleArgument(cls, subparser):
        subparser.add_argument(
            "bioSample",
            help="the JSON of the biosample")

    @classmethod
    def addIndividualArgument(cls, subparser):
        subparser.add_argument(
            "individual",
            help="the JSON of the individual")

    @classmethod
    def addFilePathArgument(cls, subparser, helpText):
        subparser.add_argument("filePath", help=helpText)

    @classmethod
    def addDirPathArgument(cls, subparser, helpText):
        subparser.add_argument("dirPath", help=helpText)

    @classmethod
    def addNameOption(cls, parser, objectType):
        parser.add_argument(
            "-n", "--name", default=None,
            help="The name of the {}".format(objectType))

    @classmethod
    def addNameArgument(cls, parser, objectType):
        parser.add_argument(
            "name", help="The name of the {}".format(objectType))

    @classmethod
    def addRnaQuantificationNameArgument(cls, subparser):
        subparser.add_argument(
            "rnaQuantificationName",
            help="the name of the RNA Quantification")

    @classmethod
    def addClassNameOption(cls, subparser, objectType):
        helpText = (
            "the name of the class used to "
            "fetch features in this {}"
        ).format(objectType)
        subparser.add_argument(
            "-C", "--className",
            default="ga4gh.datamodel.sequence_annotations.Gff3DbFeatureSet",
            help=helpText)

    @classmethod
    def addRnaQuantificationSetNameArgument(cls, subparser):
        subparser.add_argument(
            "rnaQuantificationSetName",
            help="the name of the RNA Quantification Set")

    @classmethod
    def addQuantificationFilePathArgument(cls, subparser, helpText):
        subparser.add_argument("quantificationFilePath", help=helpText)

    @classmethod
    def addRnaFormatArgument(cls, subparser):
        subparser.add_argument(
            "format", help="format of the quantification input data")

    @classmethod
    def addRnaFeatureTypeOption(cls, subparser):
        subparser.add_argument(
            "-t", "--transcript", action="store_true", default=False,
            help="sets the quantification type to transcript")

    @classmethod
    def getParser(cls):
        parser = cli.createArgumentParser(
            "GA4GH data repository management tool")
        subparsers = parser.add_subparsers(title='subcommands',)
        cli.addVersionArgument(parser)

        initParser = cli.addSubparser(
            subparsers, "init", "Initialize a data repository")
        initParser.set_defaults(runner="init")
        cls.addRepoArgument(initParser)
        cls.addForceOption(initParser)

        verifyParser = cli.addSubparser(
            subparsers, "verify",
            "Verifies the repository by examing all data files")
        verifyParser.set_defaults(runner="verify")
        cls.addRepoArgument(verifyParser)

        listParser = cli.addSubparser(
            subparsers, "list", "List the contents of the repo")
        listParser.set_defaults(runner="list")
        cls.addRepoArgument(listParser)

        addDatasetParser = cli.addSubparser(
            subparsers, "add-dataset", "Add a dataset to the data repo")
        addDatasetParser.set_defaults(runner="addDataset")
        cls.addRepoArgument(addDatasetParser)
        cls.addDatasetNameArgument(addDatasetParser)
        cls.addDatasetInfoArgument(addDatasetParser)
        cls.addDescriptionOption(addDatasetParser, "dataset")

        removeDatasetParser = cli.addSubparser(
            subparsers, "remove-dataset",
            "Remove a dataset from the data repo")
        removeDatasetParser.set_defaults(runner="removeDataset")
        cls.addRepoArgument(removeDatasetParser)
        cls.addDatasetNameArgument(removeDatasetParser)
        cls.addForceOption(removeDatasetParser)

        objectType = "reference set"
        addReferenceSetParser = cli.addSubparser(
            subparsers, "add-referenceset",
            "Add a reference set to the data repo")
        addReferenceSetParser.set_defaults(runner="addReferenceSet")
        cls.addRepoArgument(addReferenceSetParser)
        cls.addFilePathArgument(
            addReferenceSetParser,
            "The path of the FASTA file to use as a reference set. This "
            "file must be bgzipped and indexed.")
        cls.addRelativePathOption(addReferenceSetParser)
        cls.addNameOption(addReferenceSetParser, objectType)
        cls.addDescriptionOption(addReferenceSetParser, objectType)
        addReferenceSetParser.add_argument(
            "--ncbiTaxonId", default=None, help="The NCBI Taxon Id")
        addReferenceSetParser.add_argument(
            "--isDerived", default=False, type=bool,
            help="Indicates if this reference set is derived from another")
        addReferenceSetParser.add_argument(
            "--assemblyId", default=None,
            help="The assembly id")
        addReferenceSetParser.add_argument(
            "--sourceAccessions", default=None,
            help="The source accessions (pass as comma-separated list)")
        addReferenceSetParser.add_argument(
            "--sourceUri", default=None,
            help="The source URI")

        removeReferenceSetParser = cli.addSubparser(
            subparsers, "remove-referenceset",
            "Remove a reference set from the repo")
        removeReferenceSetParser.set_defaults(runner="removeReferenceSet")
        cls.addRepoArgument(removeReferenceSetParser)
        removeReferenceSetParser.add_argument(
            "referenceSetName",
            help="the name of the reference set")
        cls.addForceOption(removeReferenceSetParser)

        objectType = "ReadGroupSet"
        addReadGroupSetParser = cli.addSubparser(
            subparsers, "add-readgroupset",
            "Add a read group set to the data repo")
        addReadGroupSetParser.set_defaults(runner="addReadGroupSet")
        cls.addRepoArgument(addReadGroupSetParser)
        cls.addDatasetNameArgument(addReadGroupSetParser)
        cls.addNameOption(addReadGroupSetParser, objectType)
        cls.addReferenceSetNameOption(addReadGroupSetParser, "ReadGroupSet")
        cls.addRelativePathOption(addReadGroupSetParser)
        addReadGroupSetParser.add_argument(
            "dataFile",
            help="The file path or URL of the BAM file for this ReadGroupSet")
        addReadGroupSetParser.add_argument(
            "-I", "--indexFile", default=None,
            help=(
                "The file path of the BAM index for this ReadGroupSet. "
                "If the dataFile argument is a local file, this will "
                "be automatically inferred by appending '.bai' to the "
                "file name. If the dataFile is a remote URL the path to "
                "a local file containing the BAM index must be provided"))

        addOntologyParser = cli.addSubparser(
            subparsers, "add-ontology",
            "Adds an ontology in OBO format to the repo. Currently, "
            "a sequence ontology (SO) instance is required to translate "
            "ontology term names held in annotations to ontology IDs. "
            "Sequence ontology files can be found at "
            "https://github.com/The-Sequence-Ontology/SO-Ontologies")
        addOntologyParser.set_defaults(runner="addOntology")
        cls.addRepoArgument(addOntologyParser)
        cls.addFilePathArgument(
            addOntologyParser,
            "The path of the OBO file defining this ontology.")
        cls.addRelativePathOption(addOntologyParser)
        cls.addNameOption(addOntologyParser, "ontology")

        removeOntologyParser = cli.addSubparser(
            subparsers, "remove-ontology",
            "Remove an ontology from the repo")
        removeOntologyParser.set_defaults(runner="removeOntology")
        cls.addRepoArgument(removeOntologyParser)
        cls.addOntologyNameArgument(removeOntologyParser)
        cls.addForceOption(removeOntologyParser)

        removeReadGroupSetParser = cli.addSubparser(
            subparsers, "remove-readgroupset",
            "Remove a read group set from the repo")
        removeReadGroupSetParser.set_defaults(runner="removeReadGroupSet")
        cls.addRepoArgument(removeReadGroupSetParser)
        cls.addDatasetNameArgument(removeReadGroupSetParser)
        cls.addReadGroupSetNameArgument(removeReadGroupSetParser)
        cls.addForceOption(removeReadGroupSetParser)

        objectType = "VariantSet"
        addVariantSetParser = cli.addSubparser(
            subparsers, "add-variantset",
            "Add a variant set to the data repo based on one or "
            "more VCF files. ")
        addVariantSetParser.set_defaults(runner="addVariantSet")
        cls.addRepoArgument(addVariantSetParser)
        cls.addDatasetNameArgument(addVariantSetParser)
        cls.addRelativePathOption(addVariantSetParser)
        addVariantSetParser.add_argument(
            "dataFiles", nargs="+",
            help=(
                "The VCF/BCF files representing the new VariantSet. "
                "These may be specified either one or more paths "
                "to local files or remote URLS, or as a path to "
                "a local directory containing VCF files. Either "
                "a single directory argument may be passed or a "
                "list of file paths/URLS, but not a mixture of "
                "directories and paths.")
            )
        addVariantSetParser.add_argument(
            "-I", "--indexFiles", nargs="+", metavar="indexFiles",
            help=(
                "The index files for the VCF/BCF files provided in "
                "the dataFiles argument. These must be provided in the "
                "same order as the data files."))
        cls.addNameOption(addVariantSetParser, objectType)
        cls.addReferenceSetNameOption(addVariantSetParser, objectType)
        cls.addSequenceOntologyNameOption(addVariantSetParser, objectType)
        addVariantSetParser.add_argument(
            "-a", "--addAnnotationSets", action="store_true",
            help=(
                "If the supplied VCF file contains annotations, create the "
                "corresponding VariantAnnotationSet."))

        removeVariantSetParser = cli.addSubparser(
            subparsers, "remove-variantset",
            "Remove a variant set from the repo")
        removeVariantSetParser.set_defaults(runner="removeVariantSet")
        cls.addRepoArgument(removeVariantSetParser)
        cls.addDatasetNameArgument(removeVariantSetParser)
        cls.addVariantSetNameArgument(removeVariantSetParser)
        cls.addForceOption(removeVariantSetParser)

        addFeatureSetParser = cli.addSubparser(
            subparsers, "add-featureset", "Add a feature set to the data repo")
        addFeatureSetParser.set_defaults(runner="addFeatureSet")
        cls.addRepoArgument(addFeatureSetParser)
        cls.addDatasetNameArgument(addFeatureSetParser)
        cls.addRelativePathOption(addFeatureSetParser)
        cls.addFilePathArgument(
            addFeatureSetParser,
            "The path to the converted SQLite database containing Feature "
            "data")
        cls.addReferenceSetNameOption(addFeatureSetParser, "feature set")
        cls.addSequenceOntologyNameOption(addFeatureSetParser, "feature set")
        cls.addClassNameOption(addFeatureSetParser, "feature set")

        removeFeatureSetParser = cli.addSubparser(
            subparsers, "remove-featureset",
            "Remove a feature set from the repo")
        removeFeatureSetParser.set_defaults(runner="removeFeatureSet")
        cls.addRepoArgument(removeFeatureSetParser)
        cls.addDatasetNameArgument(removeFeatureSetParser)
        cls.addFeatureSetNameArgument(removeFeatureSetParser)
        cls.addForceOption(removeFeatureSetParser)

        addBioSampleParser = cli.addSubparser(
            subparsers, "add-biosample", "Add a BioSample to the dataset")
        addBioSampleParser.set_defaults(runner="addBioSample")
        cls.addRepoArgument(addBioSampleParser)
        cls.addDatasetNameArgument(addBioSampleParser)
        cls.addBioSampleNameArgument(addBioSampleParser)
        cls.addBioSampleArgument(addBioSampleParser)

        removeBioSampleParser = cli.addSubparser(
            subparsers, "remove-biosample",
            "Remove a BioSample from the repo")
        removeBioSampleParser.set_defaults(runner="removeBioSample")
        cls.addRepoArgument(removeBioSampleParser)
        cls.addDatasetNameArgument(removeBioSampleParser)
        cls.addBioSampleNameArgument(removeBioSampleParser)
        cls.addForceOption(removeBioSampleParser)

        addIndividualParser = cli.addSubparser(
            subparsers, "add-individual", "Add an Individual to the dataset")
        addIndividualParser.set_defaults(runner="addIndividual")
        cls.addRepoArgument(addIndividualParser)
        cls.addDatasetNameArgument(addIndividualParser)
        cls.addIndividualNameArgument(addIndividualParser)
        cls.addIndividualArgument(addIndividualParser)

        removeIndividualParser = cli.addSubparser(
            subparsers, "remove-individual",
            "Remove an Individual from the repo")
        removeIndividualParser.set_defaults(runner="removeIndividual")
        cls.addRepoArgument(removeIndividualParser)
        cls.addDatasetNameArgument(removeIndividualParser)
        cls.addIndividualNameArgument(removeIndividualParser)
        cls.addForceOption(removeIndividualParser)

        objectType = "RnaQuantification"
        addRnaQuantificationParser = cli.addSubparser(
            subparsers, "add-rnaquantification",
            "Add an RNA quantification to the data repo")
        addRnaQuantificationParser.set_defaults(
            runner="addRnaQuantification")
        cls.addFilePathArgument(
            addRnaQuantificationParser,
            "The path to the RNA SQLite database to create or modify")
        cls.addQuantificationFilePathArgument(
            addRnaQuantificationParser, "The path to the expression file.")
        cls.addRnaFormatArgument(addRnaQuantificationParser)
        cls.addRepoArgument(addRnaQuantificationParser)
        cls.addDatasetNameArgument(addRnaQuantificationParser)
        addRnaQuantificationParser.add_argument(
            "--bioSampleName", default=None, help="BioSample Name")
        addRnaQuantificationParser.add_argument(
            "--readGroupSetName", default=None, help="Read Group Set Name")
        addRnaQuantificationParser.add_argument(
            "--featureSetNames", default=None, help="Comma separated list")
        cls.addNameOption(addRnaQuantificationParser, "rna quantification")
        cls.addDescriptionOption(addRnaQuantificationParser, objectType)
        cls.addRnaFeatureTypeOption(addRnaQuantificationParser)

        objectType = "RnaQuantificationSet"
        initRnaQuantificationSetParser = cli.addSubparser(
            subparsers, "init-rnaquantificationset",
            "Initializes an RNA quantification set")
        initRnaQuantificationSetParser.set_defaults(
            runner="initRnaQuantificationSet")
        cls.addRepoArgument(initRnaQuantificationSetParser)
        cls.addFilePathArgument(
            initRnaQuantificationSetParser,
            "The path to the resulting Quantification Set")

        addRnaQuantificationSetParser = cli.addSubparser(
            subparsers, "add-rnaquantificationset",
            "Add an RNA quantification set to the data repo")
        addRnaQuantificationSetParser.set_defaults(
            runner="addRnaQuantificationSet")
        cls.addRepoArgument(addRnaQuantificationSetParser)
        cls.addDatasetNameArgument(addRnaQuantificationSetParser)
        cls.addFilePathArgument(
            addRnaQuantificationSetParser,
            "The path to the converted SQLite database containing RNA data")
        cls.addReferenceSetNameOption(
            addRnaQuantificationSetParser, objectType)
        cls.addNameOption(addRnaQuantificationSetParser, objectType)

        removeRnaQuantificationSetParser = cli.addSubparser(
            subparsers, "remove-rnaquantificationset",
            "Remove an RNA quantification set from the repo")
        removeRnaQuantificationSetParser.set_defaults(
            runner="removeRnaQuantificationSet")
        cls.addRepoArgument(removeRnaQuantificationSetParser)
        cls.addDatasetNameArgument(removeRnaQuantificationSetParser)
        cls.addRnaQuantificationSetNameArgument(
            removeRnaQuantificationSetParser)
        cls.addForceOption(removeRnaQuantificationSetParser)

        addPhenotypeAssociationSetParser = cli.addSubparser(
            subparsers, "add-phenotypeassociationset",
            "Adds phenotypes in ttl format to the repo.")
        addPhenotypeAssociationSetParser.set_defaults(
            runner="addPhenotypeAssociationSet")
        cls.addRepoArgument(addPhenotypeAssociationSetParser)
        cls.addDatasetNameArgument(addPhenotypeAssociationSetParser)
        cls.addDirPathArgument(
            addPhenotypeAssociationSetParser,
            "The path of the directory containing ttl files.")
        cls.addNameOption(
            addPhenotypeAssociationSetParser,
            "PhenotypeAssociationSet")

        removePhenotypeAssociationSetParser = cli.addSubparser(
            subparsers, "remove-phenotypeassociationset",
            "Remove an phenotypes from the repo")
        removePhenotypeAssociationSetParser.set_defaults(
            runner="removePhenotypeAssociationSet")
        cls.addRepoArgument(removePhenotypeAssociationSetParser)
        cls.addDatasetNameArgument(removePhenotypeAssociationSetParser)
        cls.addNameArgument(
            removePhenotypeAssociationSetParser,
            "phenotype association set")
        cls.addForceOption(removePhenotypeAssociationSetParser)

        return parser

    @classmethod
    def runCommand(cls, args):
        parser = cls.getParser()
        parsedArgs = parser.parse_args(args)
        if "runner" not in parsedArgs:
            parser.print_help()
        manager = RepoManager(parsedArgs)
        runMethod = getattr(manager, parsedArgs.runner)
        runMethod()


def getRepoManagerParser():
    """
    Used by sphinx.argparse.
    """
    return RepoManager.getParser()


def repoExitError(message):
    """
    Exits the repo manager with error status.
    """
    wrapper = textwrap.TextWrapper(
        break_on_hyphens=False, break_long_words=False)
    formatted = wrapper.fill("{}: error: {}".format(sys.argv[0], message))
    sys.exit(formatted)


def repo_main(args=None):
    try:
        RepoManager.runCommand(args)
    except exceptions.RepoManagerException as exception:
        # These are exceptions that happen throughout the CLI, and are
        # used to communicate back to the user
        repoExitError(str(exception))
    except exceptions.NotFoundException as exception:
        # We expect NotFoundExceptions to occur when we're looking for
        # datasets, readGroupsets, etc.
        repoExitError(str(exception))
    except exceptions.DataException as exception:
        # We expect DataExceptions to occur when a file open fails,
        # a URL cannot be reached, etc.
        repoExitError(str(exception))
    except Exception as exception:
        # Uncaught exception: this is a bug
        message = """
An internal error has occurred.  Please file a bug report at
https://github.com/ga4gh/server/issues
with all the relevant details, and the following stack trace.
"""
        print("{}: error:".format(sys.argv[0]), file=sys.stderr)
        print(message, file=sys.stderr)
        traceback.print_exception(*sys.exc_info())
        sys.exit(1)
