'''

This is a base client that imports functions depending on the API it is
    using. Currently, it supports singularity hub and registry, with default
    to use Singularity Hub.

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.main.base import ApiConnection
from sregistry.utils import (
    check_install,
    get_uri
)
from sregistry.auth import get_credential_cache
from sregistry.defaults import SREGISTRY_DATABASE
from sregistry.logger import bot
import os

def get_client(image=None, quiet=False, **kwargs):
    '''
       get the correct client depending on the driver of interest. The
       selected client can be chosen based on the environment variable
       SREGISTRY_CLIENT, and later changed based on the image uri parsed
       If there is no preference, the default is to load the singularity
       hub client.

       Parameters
       ==========
       image: if provided, we derive the correct client based on the uri
       of an image. If not provided, we default to environment, then hub.
       quiet: if True, suppress most output about the client (e.g. speak)

    '''
    from sregistry.defaults import SREGISTRY_CLIENT

    # Give the user a warning:
    if not check_install():
        bot.warning('Singularity is not installed, function might be limited.')

    # If an image is provided, use to determine client
    client_name = get_uri(image)
    if client_name is not None:
        SREGISTRY_CLIENT = client_name

    # If no obvious credential provided, we can use SREGISTRY_CLIENT
    if   SREGISTRY_CLIENT == 'aws': from .aws import Client
    elif SREGISTRY_CLIENT == 'docker': from .docker import Client
    elif SREGISTRY_CLIENT == 'dropbox': from .dropbox import Client
    elif SREGISTRY_CLIENT == 'gitlab': from .gitlab import Client
    elif SREGISTRY_CLIENT == 'globus': from .globus import Client
    elif SREGISTRY_CLIENT == 'nvidia': from .nvidia import Client
    elif SREGISTRY_CLIENT == 'hub': from .hub import Client
    elif SREGISTRY_CLIENT == 'google-drive': from .google_drive import Client
    elif SREGISTRY_CLIENT == 'google-compute': from .google_storage import Client
    elif SREGISTRY_CLIENT == 'google-storage': from .google_storage import Client
    elif SREGISTRY_CLIENT == 'google-build': from .google_build import Client
    elif SREGISTRY_CLIENT == 'registry': from .registry import Client
    elif SREGISTRY_CLIENT == 's3': from .s3 import Client
    elif SREGISTRY_CLIENT == 'swift': from .swift import Client
    else: from .hub import Client

    Client.client_name = SREGISTRY_CLIENT
    Client.quiet = quiet

    # Create credentials cache, if it doesn't exist
    Client._credential_cache = get_credential_cache()

    # Add the database, if wanted
    if SREGISTRY_DATABASE is not None:

        # These are global functions used across modules
        from sregistry.database import (
            init_db, add, cp, get, mv, rm, rmi,
            images, inspect,
            rename,
            get_container,
            get_collection,
            get_or_create_collection
        )

        # Actions
        Client._init_db = init_db
        Client.add = add
        Client.cp = cp
        Client.get = get
        Client.inspect = inspect
        Client.mv = mv
        Client.rename = rename
        Client.rm = rm
        Client.rmi = rmi
        Client.images = images

        # Collections
        Client.get_or_create_collection = get_or_create_collection
        Client.get_container = get_container
        Client.get_collection = get_collection

    # If no database, import dummy functions that return the equivalent
    else:
        from sregistry.database import ( add, init_db )
        Client.add = add
        Client._init_db = init_db

    # Initialize the database
    cli = Client()

    if hasattr(Client, '_init_db'):
        cli._init_db(SREGISTRY_DATABASE)
    return cli

Client = get_client()


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.utils import ( parse_image_name, remove_uri )
import os
import sys

def pull(self, images, file_name=None, save=True, force=False, **kwargs):
    ''' pull an image from an endpoint

        Parameters
        ==========
        images: refers to the uri given by the user to pull in the format
        <collection>/<namespace>. You should have an API that is able to
        retrieve a container based on parsing this uri.
        file_name: the user's requested name for the file. It can
               optionally be None if the user wants a default.
        save: if True, you should save the container to the database
          using self.add()

        Returns
        =======
        finished: a single container path, or list of paths
    '''

    if not isinstance(images,list):
        images = [images]

    bot.debug('Execution of PULL for %s images' % len(images))

    finished = []
    for image in images:

        q = parse_image_name(remove_uri(image), lowercase=False)

        # Verify image existence, and obtain id
        url = "%s/container/%s/%s:%s" %(self.base, q['collection'],
                                                   q['image'],
                                                   q['tag'])

        bot.debug('Retrieving manifest at %s' % url)

        manifest = self._get(url)
        manifest['selfLink'] = url

        # If the manifest reveals a version, update names
        if "version" in manifest:
            q = parse_image_name('%s@%s' %(q['uri'], manifest['version']))

        if file_name is None:
            file_name = self._get_storage_name(q)
        file_name = os.path.abspath(file_name)

        # Determine if the user already has the image
        if os.path.exists(file_name) and force is False:
            bot.error('Image exists! Remove first, or use --force to overwrite')
            sys.exit(1)

        show_bar = not bool(self.quiet)
        image_file = self.download(url=manifest['image'],
                                   file_name=os.path.basename(file_name),
                                   show_progress=show_bar)

        # If the user is saving to local storage
        if save is True:
            image_uri = "%s:%s@%s" %(manifest['name'], manifest['tag'], manifest['version'])
            container = self.add(image_path=image_file,
                                 image_uri=image_uri,
                                 image_name=file_name,
                                 metadata=manifest,
                                 url=manifest['image'])
            image_file = container.image

        if os.path.exists(image_file):
            bot.debug('Retrieved image file %s' %image_file)
            bot.custom(prefix="Success!", message=image_file)
            finished.append(image_file)

        # Reset file name back to None in case of multiple downloads
        file_name = None

    # If the user is only asking for one image
    if len(finished) == 1:
        finished = finished[0]
    return finished


'''

Copyright (C) 2016-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

import sys

def main(args,parser,subparser):

    from sregistry.main import Client as cli

    lookup = { 'ipython': ipython,
               'python': python,
               'bpython': bpython }

    shells = ['ipython', 'python', 'bpython']

    # If the user supplies a client choice, make into a uri
    if args.endpoint is not None:
        if not args.endpoint.endswith('://'):
            args.endpoint = '%s://' %args.endpoint

    # If the user asked for a specific shell via environment
    shell = cli._get_and_update_setting('SREGISTRY_SHELL')
    if shell is not None:
        shell = shell.lower()
        if shell in lookup:
            try:
                return lookup[shell](args)
            except ImportError:
                pass

    # Otherwise present order of liklihood to have on system
    for shell in shells:
        try:
            return lookup[shell](args)
        except ImportError:
            pass

def ipython(args):
    '''give the user an ipython shell, optionally with an endpoint of choice.
    '''

    # The client will announce itself (backend/database) unless it's get
    from sregistry.main import get_client
    client = get_client(args.endpoint)
    client.announce(args.command)
    from IPython import embed
    embed()

def bpython(args):
    import bpython
    from sregistry.main import get_client
    client = get_client(args.endpoint)
    client.announce(args.command)
    from sregistry.database.models import Container, Collection
    bpython.embed(locals_={'client': client,
                           'Container': Container,
                           'Collection': Collection})

def python(args):
    import code
    from sregistry.main import get_client
    client = get_client(args.endpoint)
    client.announce(args.command)
    from sregistry.database.models import Container, Collection
    code.interact(local={"client":client,
                         'Container': Container,
                         'Collection': Collection})


'''

Copyright (C) 2016-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.defaults import SINGULARITY_CACHE
from sregistry.logger import bot
from sregistry.utils import (
    get_tmpdir,
    mkdir_p,
    print_json,
    get_template,
    create_tar
)
import json
import math
import os
import re
import requests
import shutil
import sys
import tempfile

###############################################################################

def update_token(self, response):
    '''update_token uses HTTP basic authentication to get a token for
    Docker registry API V2 operations. We get here if a 401 is
    returned for a request.

    Parameters
    ==========
    response: the http request response to parse for the challenge.

    https://docs.docker.com/registry/spec/auth/token/
    '''

    not_asking_auth = "Www-Authenticate" not in response.headers
    if response.status_code != 401 or not_asking_auth:
        bot.error("Authentication error, exiting.")
        sys.exit(1)

    challenge = response.headers["Www-Authenticate"]
    regexp = '^Bearer\s+realm="(.+)",service="(.+)",scope="(.+)",?'
    match = re.match(regexp, challenge)

    if not match:
        bot.error("Unrecognized authentication challenge, exiting.")
        sys.exit(1)

    realm = match.group(1)
    service = match.group(2)
    scope = match.group(3).split(',')[0]

    token_url = realm + '?service=' + service + '&expires_in=900&scope=' + scope

    # Default headers must be False so that client's current headers not used
    response = self._get(token_url)

    try:
        token = response["token"]
        token = {"Authorization": "Bearer %s" % token}
        self.headers.update(token)

    except Exception:
        bot.error("Error getting token.")
        sys.exit(1)

def get_manifests(self, repo_name, digest=None):
    '''get_manifests calls get_manifest for each of the schema versions,
       including v2 and v1. Version 1 includes image layers and metadata,
       and version 2 must be parsed for a specific manifest, and the 2nd
       call includes the layers. If a digest is not provided
       latest is used.

       Parameters
       ==========
       repo_name: reference to the <username>/<repository>:<tag> to obtain
       digest: a tag or shasum version

    '''

    if not hasattr(self, 'manifests'):
        self.manifests = {}

    # Obtain schema version 1 (metadata) and 2, and image config
    schemaVersions = ['v1', 'v2', 'config']
    for schemaVersion in schemaVersions:
        manifest = self._get_manifest(repo_name, digest, schemaVersion)
        if manifest is not None:

            # If we don't have a config yet, try to get from version 2 manifest
            if schemaVersion == "v2" and "config" in manifest:
                bot.debug('Attempting to get config as blob in verison 2 manifest')
                url = self._get_layerLink(repo_name, manifest['config']['digest'])
                headers = {'Accept': manifest['config']['mediaType']}
                self.manifests['config'] = self._get(url, headers=headers)

            self.manifests[schemaVersion] = manifest

    return self.manifests

def get_manifest_selfLink(self, repo_name, digest=None):
    ''' get a selfLink for the manifest, for use by the client get_manifest
        function, along with the parents pull

       Parameters
       ==========
       repo_name: reference to the <username>/<repository>:<tag> to obtain
       digest: a tag or shasum version

    '''
    url = "%s/%s/manifests" % (self.base, repo_name)

    # Add a digest - a tag or hash (version)
    if digest is None:
        digest = 'latest'
    return "%s/%s" % (url, digest)

def get_manifest(self, repo_name, digest=None, version="v1"):
    '''
       get_manifest should return an image manifest
       for a particular repo and tag.  The image details
       are extracted when the client is generated.

       Parameters
       ==========
       repo_name: reference to the <username>/<repository>:<tag> to obtain
       digest: a tag or shasum version
       version: one of v1, v2, and config (for image config)

    '''

    accepts = {'config': "application/vnd.docker.container.image.v1+json",
               'v1': "application/vnd.docker.distribution.manifest.v1+json",
               'v2': "application/vnd.docker.distribution.manifest.v2+json" }

    url = self._get_manifest_selfLink(repo_name, digest)

    bot.verbose("Obtaining manifest: %s %s" % (url, version))
    headers = {'Accept': accepts[version] }

    try:
        manifest = self._get(url, headers=headers, quiet=True)
        manifest['selfLink'] = url
    except:
        manifest = None

    return manifest

def download_layers(self, repo_name, digest=None, destination=None):
    ''' download layers is a wrapper to do the following for a client loaded
        with a manifest for an image:

        1. use the manifests to retrieve list of digests (get_digests)
        2. atomically download the list to destination (get_layers)

        This function uses the MultiProcess client to download layers
        at the same time.
    '''
    from sregistry.main.workers import ( Workers, download_task )

    # 1. Get manifests if not retrieved
    if not hasattr(self, 'manifests'):
        self._get_manifests(repo_name, digest)

    # Obtain list of digets, and destination for download
    digests = self._get_digests()
    destination = self._get_download_cache(destination)

    # Create multiprocess download client
    workers = Workers()

    # Download each layer atomically
    tasks = []
    layers = []
    for digest in digests:

        targz = "%s/%s.tar.gz" % (destination, digest)

        # Only download if not in cache already
        if not os.path.exists(targz):
            url = "%s/%s/blobs/%s" % (self.base, repo_name, digest)
            tasks.append((url, self.headers, targz))
        layers.append(targz)

    # Download layers with multiprocess workers
    if len(tasks) > 0:
        download_layers = workers.run(func=download_task,
                                      tasks=tasks)
    # Create the metadata tar
    metadata = self._create_metadata_tar(destination)
    if metadata is not None:
        layers.append(metadata)

    return layers

def get_download_cache(self, destination, subfolder='docker'):
    '''determine the user preference for atomic download of layers. If
       the user has set a singularity cache directory, honor it. Otherwise,
       use the Singularity default.
    '''
    # First priority after user specification is Singularity Cache
    if destination is None:
        destination = self._get_setting('SINGULARITY_CACHEDIR',
                                         SINGULARITY_CACHE)

        # If not set, the user has disabled (use tmp)
        destination = get_tmpdir(destination)

    if not destination.endswith(subfolder):
        destination = "%s/%s" %(destination, subfolder)

    # Create subfolders, if don't exist
    mkdir_p(destination)
    return destination

def get_digests(self):
    '''return a list of layers from a manifest.
       The function is intended to work with both version
       1 and 2 of the schema. All layers (including redundant)
       are returned. By default, we try version 2 first,
       then fall back to version 1.

       For version 1 manifests: extraction is reversed

       Parameters
       ==========
       manifest: the manifest to read_layers from

    '''
    if not hasattr(self, 'manifests'):
        bot.error('Please retrieve manifests for an image first.')
        sys.exit(1)

    digests = []

    reverseLayers = False
    schemaVersions = list(self.manifests.keys())
    schemaVersions.reverse()

    # Select the manifest to use
    for schemaVersion in schemaVersions:

        manifest = self.manifests[schemaVersion]

        if manifest['schemaVersion'] == 1:
            reverseLayers = True

        # version 2 indices used by default
        layer_key = 'layers'
        digest_key = 'digest'

        # Docker manifest-v2-2.md#image-manifest
        if 'layers' in manifest:
            bot.debug('Image manifest version 2.2 found.')
            break

        # Docker manifest-v2-1.md#example-manifest  # noqa
        elif 'fsLayers' in manifest:
            layer_key = 'fsLayers'
            digest_key = 'blobSum'
            bot.debug('Image manifest version 2.1 found.')
            break

        else:
            msg = "Improperly formed manifest, "
            msg += "layers, manifests, or fsLayers must be present"
            bot.error(msg)
            sys.exit(1)

    for layer in manifest[layer_key]:
        if digest_key in layer:
            bot.debug("Adding digest %s" % layer[digest_key])
            digests.append(layer[digest_key])

    # Reverse layer order for manifest version 1.0
    if reverseLayers is True:
        message = 'v%s manifest, reversing layers' % schemaVersion
        bot.debug(message)
        digests.reverse()

    return digests

def get_layerLink(self, repo_name, digest):
    '''get the url for a layer based on a digest and repo name

       Parameters
       ==========
       digest: The image digest to obtain
       repo_name: the image name (library/ubuntu) to retrieve

    '''
    return "%s/%s/blobs/%s" % (self.base,
                               repo_name,
                               digest)

def get_layer(self, image_id, repo_name, download_folder=None):
    '''download an image layer (.tar.gz) to a specified download folder.

       Parameters
       ==========
       download_folder: download to this folder. If not set, uses temp.
       repo_name: the image name (library/ubuntu) to retrieve

    '''
    url = self._get_layerLink(repo_name, image_id)

    bot.verbose("Downloading layers from %s" % url)

    download_folder = get_tmpdir(download_folder)
    download_folder = "%s/%s.tar.gz" % (download_folder, image_id)

    # Update user what we are doing
    bot.debug("Downloading layer %s" % image_id)

    # Step 1: Download the layer atomically
    file_name = "%s.%s" % (download_folder,
                           next(tempfile._get_candidate_names()))

    tar_download = self.download(url, file_name)

    try:
        shutil.move(tar_download, download_folder)
    except Exception:
        msg = "Cannot untar layer %s," % tar_download
        msg += " was there a problem with download?"
        bot.error(msg)
        sys.exit(1)
    return download_folder

def get_size(self, add_padding=True, round_up=True, return_mb=True):
    '''get_size will return the image size (must use v.2.0 manifest)

       Parameters
       ==========
       add_padding: if true, return reported size * 5
       round_up: if true, round up to nearest integer
       return_mb: if true, defaults bytes are converted to MB

    '''
    if not hasattr(self,'manifests'):
        bot.error('Please retrieve manifests for an image first.')
        sys.exit(1)

    size = 768  # default size
    for schemaVersion, manifest in self.manifests.items():
        if "layers" in manifest:
            size = 0
            for layer in manifest["layers"]:
                if "size" in layer:
                    size += layer['size']

            if add_padding is True:
                size = size * 5

            if return_mb is True:
                size = size / (1024 * 1024)  # 1MB = 1024*1024 bytes

            if round_up is True:
                size = math.ceil(size)
            size = int(size)

        return size

################################################################################
# Metadata (Environment, Labels, Runscript)
################################################################################

def get_config(self, key="Entrypoint", delim=None):
    '''get_config returns a particular key (default is Entrypoint)
        from a VERSION 1 manifest obtained with get_manifest.

        Parameters
        ==========
        key: the key to return from the manifest config
        delim: Given a list, the delim to use to join the entries.
        Default is newline

    '''
    if not hasattr(self,'manifests'):
        bot.error('Please retrieve manifests for an image first.')
        sys.exit(1)

    cmd = None

    # If we didn't find the config value in version 2
    for version in ['config', 'v1']:
        if cmd is None and 'config' in self.manifests:

            # First try, version 2.0 manifest config has upper level config
            manifest = self.manifests['config']
            if "config" in manifest:
                if key in manifest['config']:
                    cmd = manifest['config'][key]

            # Second try, config manifest (not from verison 2.0 schema blob)

            if cmd is None and "history" in manifest:
                for entry in manifest['history']:
                    if 'v1Compatibility' in entry:
                        entry = json.loads(entry['v1Compatibility'])
                        if "config" in entry:
                            if key in entry["config"]:
                                cmd = entry["config"][key]

    # Standard is to include commands like ['/bin/sh']
    if isinstance(cmd, list):
        if delim is not None:
            cmd = delim.join(cmd)
    bot.verbose("Found Docker config (%s) %s" % (key, cmd))
    return cmd

def get_environment_tar(self):
    '''return the environment.tar generated with the Singularity software.
       We first try the Linux Filesystem expected location in /usr/libexec
       If not found, we detect the system archicture

       dirname $(singularity selftest 2>&1 | grep 'lib' | awk '{print $4}' | sed -e 's@\(.*/singularity\).*@\1@')
    '''
    from sregistry.utils import ( which, run_command )

    # First attempt - look at File System Hierarchy Standard (FHS)
    res = which('singularity')['message']
    libexec = res.replace('/bin/singularity','')
    envtar = '%s/libexec/singularity/bootstrap-scripts/environment.tar' %libexec

    if os.path.exists(envtar):
        return envtar

    # Second attempt, debian distribution will identify folder
    try:
        res = which('dpkg-architecture')['message']
        if res is not None:
            cmd = ['dpkg-architecture', '-qDEB_HOST_MULTIARCH']
            triplet = run_command(cmd)['message'].strip('\n')
            envtar = '/usr/lib/%s/singularity/bootstrap-scripts/environment.tar' %triplet
            if os.path.exists(envtar):
                return envtar
    except:
        pass

    # Final, return environment.tar provided in package
    return "%s/environment.tar" %os.path.abspath(os.path.dirname(__file__))

def create_metadata_tar(self, destination=None, metadata_folder=".singularity.d"):
    '''create a metadata tar (runscript and environment) to add to the
       downloaded image. This function uses all functions in this section
       to obtain key--> values from the manifest config, and write
       to a .tar.gz

       Parameters
       ==========
       metadata_folder: the metadata folder in the singularity image.
                        default is .singularity.d
    '''
    tar_file = None

    # We will add these files to it
    files = []

    # Extract and add environment
    environ = self._extract_env()
    if environ not in [None, ""]:
        bot.verbose3('Adding Docker environment to metadata tar')
        template = get_template('tarinfo')
        template['name'] = './%s/env/10-docker.sh' % (metadata_folder)
        template['content'] = environ
        files.append(template)

    # Extract and add labels
    labels = self._extract_labels()
    if labels is not None:
        labels = print_json(labels)
        bot.verbose3('Adding Docker labels to metadata tar')
        template = get_template('tarinfo')
        template['name'] = "./%s/labels.json" % metadata_folder
        template['content'] = labels
        files.append(template)

    # Runscript
    runscript = self._extract_runscript()
    if runscript is not None:
        bot.verbose3('Adding Docker runscript to metadata tar')
        template = get_template('tarinfo')
        template['name'] = "./%s/runscript" % metadata_folder
        template['content'] = runscript
        files.append(template)

    if len(files) > 0:
        dest = self._get_download_cache(destination, subfolder='metadata')
        tar_file = create_tar(files, dest)
    else:
        bot.warning("No metadata will be included.")
    return tar_file

def extract_env(self):
    '''extract the environment from the manifest, or return None.
       Used by functions env_extract_image, and env_extract_tar
    '''
    environ = self._get_config('Env')
    if environ is not None:
        if not isinstance(environ, list):
            environ = [environ]

        lines = []
        for line in environ:
            line = re.findall("(?P<var_name>.+?)=(?P<var_value>.+)", line)
            line = ['export %s="%s"' % (x[0], x[1]) for x in line]
            lines = lines + line

        environ = "\n".join(lines)
        bot.verbose3("Found Docker container environment!")
    return environ

def extract_runscript(self):
    '''extract the runscript (EntryPoint) as first priority, unless the
       user has specified to use the CMD. If Entrypoint is not defined,
       we default to None:

       1. IF SREGISTRY_DOCKERHUB_CMD is set, use Cmd
       2. If not set, or Cmd is None/blank, try Entrypoint
       3. If Entrypoint is not set, use default /bin/bash

    '''
    use_cmd = self._get_setting('SREGISTRY_DOCKER_CMD')

    # Does the user want to use the CMD instead of ENTRYPOINT?
    commands = ["Entrypoint", "Cmd"]
    if use_cmd is not None:
        commands.reverse()

    # Parse through commands until we hit one
    for command in commands:
        cmd = self._get_config(command)
        if cmd is not None:
            break

    # Only continue if command still isn't None
    if cmd is not None:
        bot.verbose3("Adding Docker %s as Singularity runscript..."
                     % command.upper())

        # If the command is a list, join. (eg ['/usr/bin/python','hello.py']
        bot.debug(cmd)
        if not isinstance(cmd, list):
            cmd = [cmd]

        cmd = " ".join(['"%s"' % x for x in cmd])

        cmd = 'exec %s "$@"' % cmd
        cmd = "#!/bin/sh\n\n%s\n" % cmd
        return cmd

    bot.debug("CMD and ENTRYPOINT not found, skipping runscript.")
    return cmd

def extract_labels(self):
    '''extract_labels will write a file of key value pairs including
       maintainer, and labels.

    Parameters
    ==========
    manifest: the manifest to use

    '''
    labels = self._get_config('Labels')
    if labels in [[],'',None]:
        labels = None

    return labels


'''

Copyright (C) 2018-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.main import ApiConnection
import sys
import re

from .pull import pull
from .query import ( search, search_all )

class Client(ApiConnection):

    def __init__(self, secrets=None, base=None, **kwargs):

        self._reset_headers()
        self._update_secrets()
        self._update_base()
        super(ApiConnection, self).__init__(**kwargs)

    def _update_base(self):
        '''update the base, including the URL for GitLab and the API endpoint.
        '''
        self.base = self._get_and_update_setting('SREGISTRY_GITLAB_BASE',
                                                 "https://gitlab.com/")
        self.api_base = "%s/api/v4" % self.base.strip('/')
        self.artifacts = self._get_and_update_setting('SREGISTRY_GITLAB_FOLDER',
                                                      'build')

        self.job = self._get_and_update_setting('SREGISTRY_GITLAB_JOB', 'build')

        bot.debug('      Api: %s' % self.api_base)
        bot.debug('Artifacts: %s' % self.artifacts)
        bot.debug('      Job: %s' % self.job)

    def _update_secrets(self):
        '''update secrets will update metadata needed for pull and search
        '''
        self.token = self._required_get_and_update('SREGISTRY_GITLAB_TOKEN')
        self.headers["Private-Token"] = self.token

    def __str__(self):
        return type(self)

    def _get_metadata(self):
        '''since the user needs a job id and other parameters, save this
           for them.
        '''
        metadata = {'SREGISTRY_GITLAB_FOLDER': self.artifacts,
                    'api_base': self.api_base,
                    'SREGISTRY_GITLAB_BASE': self.base,
                    'SREGISTRY_GITLAB_JOB': self.job }
        return metadata

    def artifact_to_tag(self, filename):
        '''since the artifacts represent tags in GitLab (e.g., Singularity
           converts to latest, others in format Singularity.<tag>.simg
           convert to <tag> this function provides a consistent means to do
           that conversion across the library.

           Parameters
           ==========
           filename: the filename in storage to return
        '''
        if filename == "Singularity.simg":
            return "latest"
        return re.sub('.*Singularity[.]', '',filename).replace('.simg', '')

    def _parse_image_name(self, image, retry=True):
        '''starting with an image string in either of the following formats:
           job_id|collection
           job_id|collection|job_name

           Parse the job_name, job_id, and collection uri from it. If the user
           provides the first option, we use the job_name set by the client
           (default is build).

           Parameters
           ==========
           image: the string to parse, with values separated by |
           retry: the client can call itself recursively once, providing the
                  default job_name if the user doesn't.
        '''
        try:
            job_id, collection, job_name = image.split(',')
        except:
            # Retry and add job_name
            if retry:
                return self._parse_image_name("%s,%s" %(image, self.job),
                                              retry=False)

            # Or fail
            bot.exit('''Malformed image string! Please provide:
                        job_id,collection           (or)
                        job_id,collection,job_name''')

        return job_id, collection, job_name

# Add your different functions imported at the top to the client here
Client.pull = pull

# Query functions
Client.search = search
Client._search_all = search_all


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.utils import mkdir_p
from sregistry.logger import bot
from sregistry.auth import ( read_client_secrets, update_client_secrets )
import re
import os

# Secrets and Settings

def get_settings(self, client_name=None):
    '''get all settings, either for a particular client if a name is provided,
       or across clients.

       Parameters
       ==========
       client_name: the client name to return settings for (optional)

    '''
    settings = read_client_secrets()
    if client_name is not None and client_name in settings:
        return settings[client_name]
    return settings

def get_setting(self, name, default=None):
    '''return a setting from the environment (first priority) and then
       secrets (second priority) if one can be found. If not, return None.

       Parameters
       ==========
       name: they key (index) of the setting to look up
       default: (optional) if not found, return default instead.
    '''

    # First priority is the environment
    setting = os.environ.get(name)

    # Second priority is the secrets file
    if setting is None:
        secrets = read_client_secrets()
        if self.client_name in secrets:
            secrets = secrets[self.client_name]
            if name in secrets:
                setting = secrets[name]

    if setting is None and default is not None:
        setting = default
    return setting

def get_and_update_setting(self, name, default=None):
    '''Look for a setting in the environment (first priority) and then
       the settings file (second). If something is found, the settings
       file is updated. The order of operations works as follows:

       1. The .sregistry settings file is used as a cache for the variable
       2. the environment variable always takes priority to cache, and if
          found, will update the cache.
       3. If the variable is not found and the cache is set, we are good
       5. If the variable is not found and the cache isn't set, return
          default (default is None)

       So the user of the function can assume a return of None equates to
       not set anywhere, and take the appropriate action.
    '''

    setting = self._get_setting(name)

    if setting is None and default is not None:
        setting = default

    # If the setting is found, update the client secrets
    if setting is not None:
        updates = {name : setting}
        update_client_secrets(backend=self.client_name,
                              updates=updates)

    return setting

def required_get_and_update(self, name, default=None):
    '''a wrapper to get_and_update, but if not successful, will print an
       error and exit.
    '''
    setting = self._get_and_update_setting(name, default=None)
    if setting in [None, ""]:
        bot.exit('You must export %s' % name)
    return setting

def update_setting(self, name, value):
    '''Just update a setting, doesn't need to be returned.
    '''

    if value is not None:
        updates = {name : value}
        update_client_secrets(backend=self.client_name,
                              updates=updates)

def get_storage_name(self, names, remove_dir=False):
    '''use a parsed names dictionary from get_image_name (above) to return
       the path in storage based on the user's preferences

       Parameters
       ==========
       names: the output from parse_image_name
    '''
    storage_folder = os.path.dirname(names['storage'])

    # If the client doesn't have a database, default to PWD
    if not hasattr(self, 'storage'):
        return os.path.basename(names['storage'])

    storage_folder = "%s/%s" %(self.storage, storage_folder)
    mkdir_p(storage_folder)
    file_name = names['storage'].replace('/','-')
    storage_path = "%s/%s" %(self.storage, file_name)
    if remove_dir is True:
        return file_name
    return storage_path


'''

auth.py: authorization functions for client

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.main.registry.utils import (
    generate_signature,
    generate_credential,
    generate_timestamp
)

def authorize(self, names, payload=None, request_type="push"):
    '''Authorize a client based on encrypting the payload with the client
       token, which should be matched on the receiving server'''

    if self.secrets is not None:

        if "registry" in self.secrets:

            # Use the payload to generate a digest   push|collection|name|tag|user
            timestamp = generate_timestamp()
            credential = generate_credential(self.secrets['registry']['username'])
            credential = "%s/%s/%s" %(request_type,credential,timestamp)

            if payload is None:
                payload = "%s|%s|%s|%s|%s|" %(request_type,
                                              names['collection'],
                                              timestamp,
                                              names['image'],
                                              names['tag'])

            signature = generate_signature(payload,self.secrets['registry']['token'])
            return "SREGISTRY-HMAC-SHA256 Credential=%s,Signature=%s" %(credential,signature)


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.utils import (
    parse_image_name,
    remove_uri
)

import sys
import os

def push(self, path, name, tag=None):
    '''push an image to your Storage. If the collection doesn't exist,
       it is created.

       Parameters
       ==========
       path: should correspond to an absolute image path (or derive it)
       name: should be the complete uri that the user has requested to push.
       tag: should correspond with an image tag. This is provided to mirror Docker

    '''
    path = os.path.abspath(path)
    bot.debug("PUSH %s" % path)

    if not os.path.exists(path):
        bot.error('%s does not exist.' %path)
        sys.exit(1)

    # Parse image names
    names = parse_image_name(remove_uri(name), tag=tag)

    # Get the size of the file
    file_size = os.path.getsize(path)
    chunk_size = 4 * 1024 * 1024
    storage_path = "/%s" %names['storage']

    # Create / get the collection
    collection = self._get_or_create_collection(names['collection'])

    # The image name is the name followed by tag
    image_name = os.path.basename(names['storage'])

    # prepare the progress bar
    progress = 0
    bot.show_progress(progress, file_size, length=35)

    # Put the (actual) container into the collection
    with open(path, 'rb') as F:
        self.conn.put_object(names['collection'], image_name,
                             contents= F.read(),
                             content_type='application/octet-stream')

    # Finish up
    bot.show_progress(iteration=file_size,
                      total=file_size,
                      length=35,
                      carriage_return=True)

    # Newline to finish download
    sys.stdout.write('\n')


'''

Copyright (C) 2018-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from googleapiclient.errors import HttpError
from sregistry.utils import (
    get_image_hash,
    get_installdir,
    get_recipe_tag,
    read_json,
    parse_image_name,
    remove_uri
)

from sregistry.main.google_storage.utils import get_build_template

from sregistry.logger import RobotNamer
from retrying import retry
from time import sleep
import json
import sys
import os

def build(self, repo,
                config=None,
                name=None,
                commit=None,
                tag="latest",
                recipe="Singularity",
                preview=False):

    '''trigger a build on Google Cloud (storage then compute) given a name
       recipe, and Github URI where the recipe can be found.

       Parameters
       ==========
       name: should be the complete uri that the user has requested to push.
       commit: a commit to use, not required, and can be parsed from URI
       repo: should correspond to a Github URL or (if undefined) used local repo.
       tag: a user specified tag, to take preference over tag in name
       config: The local config file to use. If the file doesn't exist, then
               we attempt looking up the config based on the name.
       recipe: If defined, limit builder to build a single recipe

    '''

    bot.debug("BUILD %s" % repo)

    # Ensure that repo exists (200 response)
    if not self._healthy(repo):
        sys.exit(1)

    config = self._load_build_config(config)

    # If name not provided, parse name based on repository
    if name is None:
        name = '/'.join(repo.split('/')[-2:])

    # This returns a data structure with collection, container, based on uri
    names = parse_image_name(remove_uri(name))

    # First priority - user has provided a tag
    names['tag'] = tag or names['tag']

    # If we still don't have custom tag, check the recipe
    if names['tag'] == "latest" and recipe != "Singularity":
        tag = get_recipe_tag(recipe)
    names = parse_image_name(remove_uri(name), tag=tag)

    # The commit is the version (after the @)
    commit = commit or names['version']

    # Setup the build
    config = self._setup_build(name=names['url'], recipe=recipe,
                               repo=repo, config=config,
                               tag=tag, commit=commit)

    # The user only wants to preview the configuration
    if preview is True:
        return config

    # Otherwise, run the build!
    return self._run_build(config)

def list_builders(self, project=None, zone='us-west1-a'):
    '''list builders, or instances for the project. They should start with
       sregistry-builder

       Parameters
       ==========
       project: specify a project, will default to environment first
       zone: the zone to use, defaults to us-west1-a if environment not set

    '''
    builders = []
    instances = self._get_instances(project, zone)

    for instance in instances['items']:
        builders.append([instance['name'], instance['status']])

    bot.info("[google-compute] Found %s instances" %(len(builders)))
    bot.table(builders)
    bot.newline()

def list_templates(self, name=None):
    '''list templates in the builder bundle library. If a name is provided,
       look it up

       Parameters
       ==========
       name: the name of a template to look up
    '''
    configs = self._get_templates()
    rows = []

    # DETAIL: The user wants to retrieve a particular configuration
    if name:
        matches = self._load_templates(name)
        bot.debug('Found %s matches for %s' %(len(matches), name))
        for match in matches:
            print(json.dumps(match, indent=4, sort_keys=True))

    # LISTING: If we don't have a specific name, just show all
    else:
        for config in configs['data']:
            rows.append([config['name']])
        bot.table(rows)

def get_templates(self):
    '''list templates in the builder bundle library. If a name is provided,
       look it up

    '''

    base = 'https://singularityhub.github.io/builders'
    base = self._get_and_update_setting('SREGISTRY_BUILDER_REPO', base)
    base = "%s/configs.json" %base
    return self._get(base)

def load_templates(self, name):
    '''load a particular template based on a name. We look for a name IN data,
       so the query name can be a partial string of the full name.

       Parameters
       ==========
       name: the name of a template to look up
    '''
    configs = self._get_templates()
    templates = []

    # The user wants to retrieve a particular configuration
    matches = [x for x in configs['data'] if name in x['name']]
    if len(matches) > 0:
        for match in matches:
            response = self._get(match['id'])
            templates.append(response)
        return templates

    bot.info('No matches found for %s' %name)

def get_instances(self, project=None, zone='us-west1-a'):
    '''get instances will return the (unparsed) list of instances, for
       functions for the user. This is primarily used by get_builders
       to print a list of builder instances.

       Parameters
       ==========
       project: specify a project, will default to environment first
       zone: the zone to use, defaults to us-west1-a if environment not set

    '''
    project = self._get_project(project)
    zone = self._get_zone(zone)

    return self._compute_service.instances().list(project=project,
                                                  zone=zone).execute()

def get_ipaddress(self, name, retries=3, delay=3):
    '''get the ip_address of an inserted instance. Will try three times with
       delay to give the instance time to start up.

       Parameters
       ==========
       name: the name of the instance to get the ip address for.
       retries: the number of retries before giving up
       delay: the delay between retry

       Note from @vsoch: this function is pretty nasty.

    '''
    for rr in range(retries):

        # Retrieve list of instances
        instances = self._get_instances()

        for instance in instances['items']:
            if instance['name'] == name:

                # Iterate through network interfaces
                for network in instance['networkInterfaces']:
                    if network['name'] == 'nic0':

                        # Access configurations
                        for subnet in network['accessConfigs']:
                            if subnet['name'] == 'External NAT':
                                if 'natIP' in subnet:
                                    return subnet['natIP']

        sleep(delay)
    bot.warning('Did not find IP address, check Cloud Console!')

def load_build_config(self, config=None):
    '''load a google compute config, meaning that we have the following cases:

       1. the user has not provided a config file directly, we look in env.
       2. the environment is not set, so we use a reasonable default
       3. if the final string is not found as a file, we look for it in library
       4. we load the library name, or the user file, else error

       Parameters
       ==========
       config: the config file the user has provided, or the library URI

    '''
    # If the config is already a dictionary, it's loaded
    if isinstance(config, dict):
        bot.debug('Config is already loaded.')
        return config

    # if the config is not defined, look in environment, then choose a default
    if config is None:
        config = self._get_and_update_setting('SREGISTRY_COMPUTE_CONFIG',
                                     'google/compute/ubuntu/securebuild-2.4.3')

    # If the config is a file, we read it
    elif os.path.exists(config):
        return read_json(config)

    # otherwise, try to look it up in library
    configs = self._load_templates(config)
    if configs is None:
        bot.error('%s is not a valid config. %s' %name)
        sys.exit(1)

    bot.info('Found config %s in library!' %config)
    config = configs[0]

    return config

def setup_build(self, name, repo, config, tag=None, commit=None,
                      recipe="Singularity", startup_script=None):

    '''setup the build based on the selected configuration file, meaning
       producing the configuration file filled in based on the user's input

       Parameters
       ==========
       config: the complete configuration file provided by the client
       template: an optional custom start script to use
       tag: a user specified tag for the build, derived from uri or manual
       recipe: a recipe, if defined, overrides recipe set in config.
       commit: a commit to check out, if needed
       start_script: the start script to use, if not defined
                     defaults to apt (or manager) base in main/templates/build

    '''

    manager = self._get_and_update_setting('SREGISTRY_BUILDER_MANAGER', 'apt')
    startup_script = get_build_template(startup_script, manager)

    # Read in the config to know what we can edit
    config = self._load_build_config(config)
    if not config:
        bot.error('Cannot find config, check path or URI.')
        sys.exit(1)

    # Ensure that the builder config is intended for the client
    self._client_tagged(config['data']['tags'])

    # Compute settings that are parsed into runscript via metadata
    defaults = config['data']['metadata']
    selfLink = config['links']['self']

    # Make sure the builder repository and folder is passed forward
    builder_repo = config['data']['repo']
    builder_bundle = config['data']['path']
    builder_id = config['data']['id']
    config = config['data']['config']

    # Config settings from the environment, fall back to defaults
    image_project = defaults.get('GOOGLE_COMPUTE_PROJECT', 'debian-cloud')
    image_family = defaults.get('GOOGLE_COMPUTE_IMAGE_FAMILY', 'debian-8')

    # Generate names, description is for repo, name is random
    instance_name = "%s-builder %s" %(name.replace('/','-'), selfLink)
    robot_name = RobotNamer().generate()

    project = self._get_project()
    zone = self._get_zone()

    # Machine Type
    machine_type = defaults.get('SREGISTRY_BUILDER_machine_type', 'n1-standard-1')
    machine_type = "zones/%s/machineTypes/%s" %(zone, machine_type)

    # Disk Size
    disk_size = defaults.get('SREGISTRY_BUILDER_disk_size', '100')

    # Get the image type
    image_response = self._compute_service.images().getFromFamily(
                              project=image_project,
                              family=image_family).execute()
    source_disk_image = image_response['selfLink']
    storage_bucket = self._bucket_name

    # Add the machine parameters to the config
    config['name'] = robot_name
    config['description'] = instance_name
    config['machineType'] = machine_type
    config['disks'].append({
                    "autoDelete": True,
                    "boot": True,
                    "initializeParams": { 'sourceImage': source_disk_image,
                                          'diskSizeGb': disk_size }
                   })

    # Metadata base
    metadata = {'items':

                    [{ 'key': 'startup-script',
                       'value': startup_script },

                    # Storage Settings from Host

                     { 'key':'SREGISTRY_BUILDER_STORAGE_BUCKET',
                       'value':self._bucket_name }]}

    # Runtime variables take priority over defaults from config
    # and so here we update the defaults with runtime
    # ([defaults], [config-key], [runtime-setting])

    # User Repository
    defaults = setconfig(defaults, 'SREGISTRY_USER_REPO', repo)

    # Container Namespace (without tag/version)
    defaults = setconfig(defaults, 'SREGISTRY_CONTAINER_NAME', name)

    # User Repository Commit
    defaults = setconfig(defaults, 'SREGISTRY_USER_COMMIT', commit)

    # User Repository Branch
    defaults = setconfig(defaults, 'SREGISTRY_USER_BRANCH', "master")

    # User Repository Tag
    defaults = setconfig(defaults, 'SREGISTRY_USER_TAG', tag)

    # Builder repository url
    defaults = setconfig(defaults, 'SREGISTRY_BUILDER_REPO', builder_repo)

    # Builder commit
    defaults = setconfig(defaults, 'SREGISTRY_BUILDER_COMMIT')

    # Builder default runscript
    defaults = setconfig(defaults, 'SREGISTRY_BUILDER_RUNSCRIPT', "run.sh")

    # Builder repository url
    defaults = setconfig(defaults, 'SREGISTRY_BUILDER_BRANCH', "master")

    # Builder id in repository
    defaults = setconfig(defaults, 'SREGISTRY_BUILDER_ID', builder_id)

    # Builder repository relative folder path
    defaults = setconfig(defaults, 'SREGISTRY_BUILDER_BUNDLE', builder_bundle)

    # Number of extra hours to debug
    defaults = setconfig(defaults, 'SREGISTRY_BUILDER_DEBUGHOURS', "4")

    # Hours to kill running job
    defaults = setconfig(defaults, 'SREGISTRY_BUILDER_KILLHOURS', "10")

    # Recipe set at runtime
    defaults = setconfig(defaults, 'SINGULARITY_RECIPE', recipe)

    # Branch of Singularity to install
    defaults = setconfig(defaults, 'SINGULARITY_BRANCH')

    # Singularity commit to use (if needed)
    defaults = setconfig(defaults, 'SINGULARITY_COMMIT')

    # Singularity Repo to Use
    defaults = setconfig(defaults,'SINGULARITY_REPO',
                                  'https://github.com/cclerget/singularity.git')

    # Update metadata config object

    seen = ['SREGISTRY_BUILDER_STORAGE_BUCKET', 'startup-script']

    for key, value in defaults.items():

        # This also appends empty values, they are meaningful
        if value not in seen:
            entry = { "key": key, 'value': value }
            metadata['items'].append(entry)
            seen.append(key)

    config['metadata'] = metadata
    return config

def setconfig(lookup, key, value=None):
    '''setconfig will update a lookup to give priority based on the following:

       1. If both values are None, we set the value to None
       2. If the currently set (the config.json) is set but not runtime, use config
       3. If the runtime is set but not config.json, we use runtime
       4. If both are set, we use runtime

    '''
    lookup[key] = value or lookup.get(key)
    return lookup

@retry(wait_exponential_multiplier=1000,
       wait_exponential_max=10000,
       stop_max_attempt_number=3)

def run_build(self, config):
    '''run a build, meaning inserting an instance. Retry if there is failure

       Parameters
       ==========
       config: the configuration dictionary generated by setup_build

    '''
    project = self._get_project()
    zone = self._get_zone()

    bot.custom(prefix='INSTANCE', message=config['name'], color="CYAN")
    bot.info(config['description'])

    response = self._compute_service.instances().insert(project=project,
                                                        zone=zone,
                                                        body=config).execute()

    # Direct the user to the web portal with log
    ipaddress = self._get_ipaddress(config['name'])
    bot.info('Robot Logger: http://%s' %ipaddress)
    bot.info('Allow a few minutes for web server install, beepboop!')
    return response


'''

ls: search and query functions for client

Copyright (C) 2018-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.utils import remove_uri
from dateutil import parser

import json
import sys
import os

def search(self, query=None, args=None):
    '''query a Singularity registry for a list of images.
     If query is None, collections are listed.

    '''

    if query is not None:

        # Here you might do a function that is a general list
        # Note that this means adding the function Client in __init__
        return self._container_query(query)

    # or default to listing (searching) all things.
    return self._search_all()

def search_all(self):
    '''a "show all" search that doesn't require a query'''

    results = set()

    # Here we get names of collections, and then look up containers
    for container in self.conn.get_account()[1]:

        # The result here is just the name
        for result in self.conn.get_container(container['name'])[1]:
            results.add('%s/%s' %(container['name'], result['name']))

    if len(results) == 0:
        bot.info("No container collections found.")
        sys.exit(1)

    bot.info("Collections")
    bot.table([[x] for x in list(results)])
    return list(results)

def container_query(self, query):
    '''search for a specific container.
    This function would likely be similar to the above, but have different
    filter criteria from the user (based on the query)
    '''

    results = set()

    query = remove_uri(query)

    # Here we get names of collections, and then look up containers
    for container in self.conn.get_account()[1]:

        # The result here is just the name
        for result in self.conn.get_container(container['name'])[1]:
            if query in collection['name']:
                results.add('%s/%s' %(container['name'], result['name']))

    if len(results) == 0:
        bot.info("No container collections found.")
        sys.exit(1)

    bot.info("Collections")
    bot.table([list(results)])
    return list(results)


'''

ls: search and query functions for client

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
import sys
import os

def search(self, query=None, args=None):
    '''query a bucket for images that match a particular pattern. If no
       query is provided, all images in the bucket are listed that have type
       "container" in the metadata and client "sregistry"
    '''

    if query is not None:
        return self._container_query(query)
    return self._search_all()

def list_containers(self):
    '''return a list of containers, determined by finding the metadata field
       "type" with value "container." We alert the user to no containers
       if results is empty, and exit

       {'metadata': {'items':
                              [
                               {'key': 'type', 'value': 'container'}, ...
                              ]
                    }
       }

    '''
    results = []
    for image in self._bucket.list_blobs():
        if image.metadata is not None:
            if "type" in image.metadata:
                if image.metadata['type'] == "container":
                    results.append(image)

    if len(results) == 0:
        bot.info("No containers found, based on metadata type:container")

    return results

def search_all(self):
    '''a "list all" search that doesn't require a query. Here we return to
       the user all objects that have custom metadata value of "container"

       IMPORTANT: the upload function adds this metadata. For a container to
       be found by the client, it must have the type as container in metadata.
    '''

    results = self._list_containers()

    bot.info("[gs://%s] Containers" %self._bucket_name)

    rows = []
    for i in results:
        size = round(i.size / (1024*1024.0))
        size = ("%s MB" %size).rjust(10)
        rows.append([size, i.metadata['name']])

    bot.table(rows)
    return rows

def container_query(self, query, quiet=False):
    '''search for a specific container.
       This function would likely be similar to the above, but have different
       filter criteria from the user (based on the query)
    '''
    results = self._list_containers()

    matches = []
    for result in results:
        for key,val in result.metadata.items():
            if query in val and result not in matches:
                matches.append(result)

    if not quiet:
        bot.info("[gs://%s] Found %s containers" %(self._bucket_name,len(matches)))
        for image in matches:
            size = round(image.size / (1024*1024.0))
            bot.custom(prefix=image.name, color="CYAN")
            bot.custom(prefix='id:     ', message=image.id)
            bot.custom(prefix='uri:    ', message=image.metadata['name'])
            bot.custom(prefix='updated:', message=image.updated)
            bot.custom(prefix='size:  ',  message=' %s MB' %(size))
            bot.custom(prefix='md5:    ', message=image.md5_hash)
            if "public_url" in image.metadata:
                public_url = image.metadata['public_url']
                bot.custom(prefix='url:    ', message=public_url)
            bot.newline()
    return matches


'''

ls: search and query functions for client

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.utils import remove_uri
from dateutil import parser

import json
import sys
import os

def search(self, query=None, args=None):
    '''query a Singularity registry for a list of images.
     If query is None, collections are listed.

    '''

    if query is not None:

        # Here you might do a function that is a general list
        # Note that this means adding the function Client in __init__
        return self._container_query(query)

    # or default to listing (searching) all things.
    return self._search_all()

def search_all(self):
    '''a "show all" search that doesn't require a query'''

    results = []

    # Parse through folders (collections):
    for entry in self.dbx.files_list_folder('').entries:

        # Parse through containers
        for item in self.dbx.files_list_folder(entry.path_lower).entries:
            name = item.name.replace('.simg','')
            results.append([ "%s/%s" % (entry.name, name) ])

    if len(results) == 0:
        bot.info("No container collections found.")
        sys.exit(1)

    bot.info("Collections")
    bot.table(results)
    return results

def container_query(self, query):
    '''search for a specific container.
    This function would likely be similar to the above, but have different
    filter criteria from the user (based on the query)
    '''

    results = []

    query = remove_uri(query)

    # Parse through folders (collections):
    for entry in self.dbx.files_list_folder('').entries:

        # Parse through containers
        for item in self.dbx.files_list_folder(entry.path_lower).entries:
            name = item.name.replace('.simg','')
            name = "%s/%s" % (entry.name, name)
            if query in name:
                results.append([ name ])

    if len(results) == 0:
        bot.info("No container collections found.")
        sys.exit(1)

    bot.info("Collections")
    bot.table(results)
    return results


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

def main(args,parser,subparser):
    '''the list command corresponds with listing images for an external
       resource. This is different from listing images that are local to the
       database, which should be done with "images"
    '''
    from sregistry.main import get_client
    cli = get_client(quiet=args.quiet)

    for query in args.query:
        if query in ['','*']:
            query = None

        cli.ls(query=query)


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.utils import ( parse_image_name, remove_uri )
import os
import sys

def pull(self, images, file_name=None, save=True, **kwargs):
    '''pull an image from google storage, based on the identifier

       Parameters
       ==========
       images: refers to the uri given by the user to pull in the format
               <collection>/<namespace>. You should have an API that is able to
               retrieve a container based on parsing this uri.
       file_name: the user's requested name for the file. It can
               optionally be None if the user wants a default.
       save: if True, you should save the container to the database
             using self.add()

       Returns
       =======
       finished: a single container path, or list of paths
    '''

    if not isinstance(images,list):
        images = [images]

    bot.debug('Execution of PULL for %s images' %len(images))

    # If used internally we want to return a list to the user.
    finished = []
    for image in images:

        q = parse_image_name(remove_uri(image))

        # Use container search to find the container based on uri
        bot.info('Searching for %s in gs://%s' %(q['tag_uri'],self._bucket_name))
        matches = self._container_query(q['tag_uri'], quiet=True)

        if len(matches) == 0:
            bot.info('No matching containers found.')
            sys.exit(0)

        # If the user didn't provide a file, make one based on the names
        if file_name is None:
            file_name = q['storage_version'].replace('/','-')

        # We give the first match, the uri should be unique and known
        image = matches[0]
        image_file = self.download(url=image.media_link,
                                   file_name=file_name,
                                   show_progress=True)

        # If the user is saving to local storage, you need to assumble the uri
        # here in the expected format <collection>/<namespace>:<tag>@<version>
        if save is True:
            image_uri = q['tag_uri']

            # Update metadata with selfLink
            metadata = image.metadata

            # Rename public URL to URL so it's found by add client
            if "public_url" in metadata:
                metadata['url'] = metadata['public_url']

            metadata['selfLink'] = image.self_link

            container = self.add(image_path=image_file,
                                 image_uri=image_uri,
                                 metadata=metadata,
                                 url=image.media_link)

            # When the container is created, this is the path to the image
            image_file = container.image

        if os.path.exists(image_file):
            bot.debug('Retrieved image file %s' % image_file)
            bot.custom(prefix="Success!", message=image_file)
            finished.append(image_file)

    if len(finished) == 1:
        finished = finished[0]
    return finished


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

def main(args, parser, subparser):
    '''sharing an image means sending a remote share from an image you
       control to a contact, usually an email.
    '''
    from sregistry.main import get_client
    images = args.image

    if not isinstance(images,list):
        images = [images]

    for image in images:
        print(image)

        # Detect any uri, and refresh client if necessary
        cli = get_client(image, quiet=args.quiet)
        cli.announce(args.command)
        cli.share(image, share_to=args.share_to)


'''

models.py: models for a local singularity registry

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sqlalchemy import (
    Column,
    DateTime,
    Integer,
    String,
    Text,
    ForeignKey,
    func
)

from sqlalchemy.schema import UniqueConstraint
from sregistry.logger import bot
from sregistry.defaults import ( SREGISTRY_DATABASE, SREGISTRY_STORAGE )
from sqlalchemy import create_engine
from sqlalchemy.orm import ( backref,
                             relationship,
                             scoped_session,
                             sessionmaker )
from uuid import uuid4

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Collection(Base):
    '''A participant in a local assessment. id must be unique. If a token is
       revoked or finished, it will end with `_revoked` or `_finished`. A
       user generated without a token will have value of None
    '''
    __tablename__ = 'collection'
    id = Column(Integer, primary_key=True)
    name = Column(String(150), unique=True)
    token = Column(String(50))
    created_at = Column(DateTime, default=func.now())
    containers = relationship('Container',
                              lazy='select',
                              cascade='delete,all',
                              backref=backref('collection', lazy='joined'))

    def __init__(self, name=None, token=None):
        self.name = name
        if token is None:
            token = str(uuid4())
        self.token = token

    def __repr__(self):
        return '<Collection %r>' % (self.name)

    def __str__(self):
        return '<Collection %r>' % (self.name)

    def url(self):
        '''return the collection url'''
        return "file://"

class Container(Base):
    '''a container belongs to a collection

    Parameters
    ==========
    created_at: the creation date of the image / container
    metrics: typically the inspection of the image. If not possible, then the
             basic name (uri) derived from the user is used.
    tag: the image tag
    image: the path to the image on the filesystem (can be Null)
    url: the url where the imate was ultimately retrieved, call be Null
    client: the client backend associated with the image, the type(client)
    version: a version string associated with the image
    :collection_id: the id of the colletion to which the image belongs.

    We index / filter containers based on the full uri, which is assembled
               from the <collection>/<namespace>:<tag>@<version>, then stored
               as a variable, and maintained separately for easier query.
    '''
    __tablename__ = 'container'
    id = Column(Integer, primary_key=True)
    created_at = Column(DateTime, default=func.now())
    metrics = Column(Text, nullable=False)
    name = Column(String(250), nullable=False)
    tag = Column(String(250), nullable=False)
    image = Column(String(250), nullable=True)
    url = Column(String(500), nullable=True)
    uri = Column(String(500), nullable=True)
    client = Column(String(50), nullable=False)
    version = Column(String(250), nullable=True)
    collection_id = Column(Integer,
                           ForeignKey('collection.id'),
                           nullable=False)

    __table_args__ = (UniqueConstraint('collection_id',
                                       'name',
                                       'tag',
                                       'client',
                                       'version', name='_container_uc'),)

    def __repr__(self):
        if self.uri is None:
            return '<Container %r>' % (self.name)
        return '<Container %r>' % (self.uri)

    def __str__(self):
        if self.uri is None:
            return '<Container %r>' % (self.name)
        return '<Container %r>' % (self.uri)

    def get_uri(self):
        '''generate a uri on the fly from database parameters if one is not
        saved with the initial model (it should be, but might not be possible)
        '''
        uri = "%s/%s:%s" %(self.collection.name, self.name, self.tag)
        if self.version not in [None,'']:
            uri = "%s@%s" %(uri, self.version)
        return uri

def init_db(self, db_path):
    '''initialize the database, with the default database path or custom of

       the format sqlite:////scif/data/expfactory.db

    The custom path can be set with the environment variable SREGISTRY_DATABASE
    when a user creates the client, we must initialize this db
    the database should use the .singularity cache folder to cache
    layers and images, and .singularity/sregistry.db as a database
    '''

    # Database Setup, use default if uri not provided
    self.database = 'sqlite:///%s' % db_path
    self.storage = SREGISTRY_STORAGE

    bot.debug("Database located at %s" % self.database)
    self.engine = create_engine(self.database, convert_unicode=True)
    self.session = scoped_session(sessionmaker(autocommit=False,
                                               autoflush=False,
                                               bind=self.engine))

    Base.query = self.session.query_property()

    # import all modules here that might define models so that
    # they will be registered properly on the metadata.  Otherwise
    # you will have to import them first before calling init_db()
    Base.metadata.create_all(bind=self.engine)
    self.Base = Base


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.utils import ( read_json, get_installdir )
import sys
import os

def get_build_template():
    '''get default build template.
    '''
    base = get_installdir()
    name = "%s/main/templates/build/singularity-cloudbuild.json" % base

    if os.path.exists(name):
        bot.debug("Found template %s" %name)
        return read_json(name)

    bot.warning("Template %s not found." % name)


'''

ls: search and query functions for client

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from globus_sdk.exc import TransferAPIError
from sregistry.logger import bot
import sys
import os

def search(self, query=None, args=None):
    '''query will show images determined by the extension of img
       or simg.

       Parameters
       ==========
       query: the container name (path) or uri to search for
       args.endpoint: can be an endpoint id and optional path, e.g.:

             --endpoint  6881ae2e-db26-11e5-9772-22000b9da45e:.singularity'
             --endpoint  6881ae2e-db26-11e5-9772-22000b9da45e'

       if not defined, we show the user endpoints to choose from

       Usage
       =====
       If endpoint is defined with a query, then we search the given endpoint
       for a container of interested (designated by ending in .img or .simg

       If no endpoint is provided but instead just a query, we use the query
       to search endpoints.

    '''

    # No query is defined
    if query is None:

        # Option 1: No query or endpoints lists all shared and personal
        if args.endpoint is None:
            bot.info('Listing shared endpoints. Add query to expand search.')
            return self._list_endpoints()

        # Option 2: An endpoint without query will just list containers there
        else:
            return self._list_endpoint(args.endpoint)

    # Option 3: A query without an endpoint will search endpoints for it
    if args.endpoint is None:
        bot.info('You must specify an endpoint id to query!')
        return self._list_endpoints(query)

    # Option 4: A query with an endpoint will search the endpoint for pattern
    return self._list_endpoint(endpoint=args.endpoint,
                               query=query)

def list_endpoints(self, query=None):
    '''list all endpoints, providing a list of endpoints to the user to
       better filter the search. This function takes no arguments,
       as the user has not provided an endpoint id or query.
    '''
    bot.info('Please select an endpoint id to query from')

    endpoints = self._get_endpoints(query)

    # Iterate through endpoints to provide user a list

    bot.custom(prefix="Globus", message="Endpoints", color="CYAN")
    rows = []
    for kind,eps in endpoints.items():
        for epid,epmeta in eps.items():
            rows.append([epid, '[%s]' %kind, epmeta['name']])

    bot.table(rows)
    return rows

def list_endpoint(self, endpoint, query=None):
    '''An endpoint is required here to list files within. Optionally, we can
       take a path relative to the endpoint root.

       Parameters
       ==========
       endpoint: a single endpoint ID or an endpoint id and relative path.
                 If no path is provided, we use '', which defaults to scratch.

       query: if defined, limit files to those that have query match

    '''
    if not hasattr(self, 'transfer_client'):
        self._init_transfer_client()

    # Separate endpoint id from the desired path

    endpoint, path = self._parse_endpoint_name(endpoint)

    # Get a list of files at endpoint, under specific path
    try:
        result = self.transfer_client.operation_ls(endpoint, path=path)
    except TransferAPIError as err:

        # Tell the user what went wrong!
        bot.custom(prefix='ERROR', message=err, color='RED')
        sys.exit(1)

    rows = []

    for filey in result:

        # Highlight container contenders with purple
        name = filey['name']
        if query is None or query in name:
            if name.endswith('img'):
                name = bot.addColor('PURPLE',name)

            rows.append([filey['type'],
                         filey['permissions'],
                         str(filey['size']),
                         name ])

    if len(rows) > 0:
        rows = [["type","[perm]","[size]","[name]"]] + rows
        bot.custom(prefix="Endpoint Listing %s" %path, message='', color="CYAN")
        bot.table(rows)
    else:
        bot.info('No content was found at the selected endpoint.')
    return rows


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from dropbox.exceptions import ApiError
from sregistry.utils import ( parse_image_name, remove_uri )
from sregistry.logger import bot

def share(self, query, share_to=None):
    '''share will use the client to get a shareable link for an image of choice.
       the functions returns a url of choice to send to a recipient.
    '''

    names = parse_image_name(remove_uri(query))

    # Dropbox path is the path in storage with a slash
    dropbox_path = '/%s' % names['storage']

    # First ensure that exists
    if self.exists(dropbox_path) is True:

        # Create new shared link
        try:
            share = self.dbx.sharing_create_shared_link_with_settings(dropbox_path)

        # Already exists!
        except ApiError as err:
            share = self.dbx.sharing_create_shared_link(dropbox_path)

        bot.info(share.url)
    return share.url


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.auth import basic_auth_header
from sregistry.main import ApiConnection
import json
import sys
import os

# The core of nvidia is actually docker
from sregistry.main.docker.api import (
    create_metadata_tar,
    download_layers,
    get_manifest_selfLink,
    get_config,
    get_digests,
    get_environment_tar,
    get_layer,
    get_layerLink,
    get_manifest,
    get_manifests,
    get_download_cache,
    get_size,
    extract_env,
    extract_labels,
    extract_runscript,
    update_token
)

from sregistry.main.docker.pull import _pull
from .pull import pull

class Client(ApiConnection):

    def __init__(self, secrets=None, **kwargs):
        '''to work with nvidia container registry, we do the following:

        1. set the base to index.docker.io, or defined in environment
        2. assume starting with v2 schema, we won't reverse layers
        3. set headers to ask for version 2 schema first
        4. update secrets, 1st priority environment, then .docker/config.json
        5. update headers based on secrets

        '''
        self._set_base()
        self.reverseLayers = False
        self._reset_headers()
        self._update_secrets()
        self._update_headers()
        super(ApiConnection, self).__init__(**kwargs)

    def _speak(self):
        '''if you want to add an extra print (of a parameter, for example)
           for the user when the client initalizes, write it here, eg:
           bot.info('[setting] value')
        '''
        pass

    def _reset_headers(self):
        '''reset headers is called from update_headers, and will update the
           headers based on what is found with the client secrets
        '''

        # specify wanting version 2 schema
        # meaning the correct order of digests
        # returned (base to child)
        accept = 'application/vnd.docker.distribution.manifest.v2+json,'
        accept += 'application/vnd.docker.distribution.manifest.list.v2+json,'
        accept += 'application/json'

        self.headers = {"Accept": accept,
                        'Content-Type': 'application/json; charset=utf-8'}

    def _set_base(self):
        '''set the API base or default to use Docker Hub. The user is able
           to set the base, api version, and protocol via a settings file
           of environment variables:

           SREGISTRY_NVIDIA_BASE: defaults to nvcr.io
           SREGISTRY_NVIDIA_TOKEN: defaults to $oauthtoken
           SREGISTRY_NVIDIA_VERSION: defaults to v2
           SREGISTRY_NVIDIA_NO_HTTPS: defaults to not set (so https)

        '''
        base = self._get_setting('SREGISTRY_NVIDIA_BASE')
        version = self._get_setting('SREGISTRY_NVIDIA_VERSION')

        if base is None:
            base = "nvcr.io"

        if version is None:
            version = "v2"

        nohttps = self._get_setting('SREGISTRY_NVIDIA_NOHTTPS')
        if nohttps is None:
            nohttps = "https://"
        else:
            nohttps = "http://"

        # <protocol>://<base>/<version>
        self.base = "%s%s/%s" %(nohttps, base.strip('/'), version)

    def _update_secrets(self):
        '''update secrets will take a secrets credential file
           either located at .sregistry or the environment variable
           SREGISTRY_CLIENT_SECRETS and update the current client
           secrets as well as the associated API base. For the case of
           using Docker Hub, if we find a .docker secrets file, we update
           from there.
        '''
        # If the user has defined secrets, use them
        token = self._required_get_and_update('SREGISTRY_NVIDIA_TOKEN')
        username = self._get_and_update_setting('SREGISTRY_NVIDIA_USERNAME')

        if username is None:
            username = "$oauthtoken"

        # Option 1: the user exports username and password
        if token is not None:
            auth = basic_auth_header(username, token)
            self.headers.update(auth)

    def __str__(self):
        return type(self)

# Functions exposed to the client
Client.pull = pull
Client._pull = _pull

# Api functions for image layers and manifests (hidden)
Client._create_metadata_tar = create_metadata_tar
Client._download_layers = download_layers
Client._extract_runscript = extract_runscript
Client._extract_labels = extract_labels
Client._extract_env = extract_env
Client._get_config = get_config
Client._get_digests = get_digests
Client._get_download_cache = get_download_cache
Client._get_layer = get_layer
Client._get_layerLink = get_layerLink
Client._get_manifest = get_manifest
Client._get_manifests = get_manifests
Client._get_size = get_size
Client._update_token = update_token
Client._get_manifest_selfLink = get_manifest_selfLink
Client._get_environment_tar = get_environment_tar


'''

pull.py: pull function for singularity registry

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from spython.main import Client as Singularity
from sregistry.logger import bot
from sregistry.defaults import SREGISTRY_STORAGE
from sregistry.utils import ( parse_image_name, remove_uri )
import globus_sdk

import requests
import shutil
import sys
import os

def pull(self, images, file_name=None, save=True, **kwargs):
    '''pull an image from a Globus endpoint. The user must have the default
       local endpoint set up. For example:

       6881ae2e-db26-11e5-9772-22000b9da45e:.singularity/shub/sherlock_vep.simg

    Parameters
    ==========
    images: refers to the globus endpoint id and image path.
    file_name: the user's requested name for the file. It can
               optionally be None if the user wants a default.
    save: if True, you should save the container to the database
          using self.add(). For globus this is the only option, and
          we don't have control over when this happens.

    Returns
    =======
    finished: a single container path, or list of paths
    '''

    # Ensure we have a transfer client
    if not hasattr(self, 'transfer_client'):
        self._init_transfer_client()

    if not isinstance(images,list):
        images = [images]

    bot.debug('Execution of PULL for %s images' %len(images))

    finished = []
    for image in images:

        # Split the name into endpoint and rest

        endpoint, remote = self._parse_endpoint_name(image)
        source = self.transfer_client.get_endpoint(endpoint)

        name = os.path.basename(remote)
        q = parse_image_name(name, default_collection=source['name'])

        # The user must have a personal endpoint

        endpoints = self._get_endpoints()

        if len(endpoints['my-endpoints']) == 0:
            bot.error('You must have a personal endpoint to transfer the container')
            sys.exit(1)

        # Take the first endpoint that is active

        dest = None
        for eid,contender in endpoints['my-endpoints'].items():
           if contender['gcp_connected'] is True:
               dest = contender
               break

        # Exit if none are active, required!

        if dest is None:
            bot.error('No activated local endpoints online! Start to transfer')
            sys.exit(1)

        # We need to know the full path of the endpoint

        base = self._get_endpoint_path(dest['id'])
        storage_folder = '%s/%s' %(base, q['collection'])
        self._create_endpoint_folder(dest['id'], storage_folder)

        label = "Singularity Registry Pull"
        tdata = globus_sdk.TransferData(self.transfer_client,
                                        source['id'],
                                        dest['id'],
                                        label=label,
                                        sync_level="checksum")

        image = os.path.join(base, q['storage'])
        tdata.add_item(remote, image)
        bot.info('Requesting transfer to %s' %q['storage'])
        transfer_result = self.transfer_client.submit_transfer(tdata)
        bot.info(transfer_result['message'])
        finished.append(transfer_result)

    if len(finished) == 1:
        finished = finished[0]
    return finished


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.utils import (
    mkdir_p,
    read_json,
    write_json
)

import json
import os
import sys

def get_credential_cache():
    '''if the user has specified settings to provide a cache for credentials
       files, initialize it. The root for the folder is created if it doesn't
       exist. The path for the specific client is returned, and it's
       not assumed to be either a folder or a file (this is up to the
       developer of the client).
    '''
    from sregistry.defaults import ( CREDENTIAL_CACHE, SREGISTRY_CLIENT )

    client_credential_cache = None

    # Check 1: user can disable a credential cache on the client level
    if CREDENTIAL_CACHE is not None:
        env = 'SREGISTRY_DISABLE_CREDENTIAL_%s' %SREGISTRY_CLIENT.upper()
        if os.environ.get(env) is not None:
            bot.debug('[%s] cache disabled' %SREGISTRY_CLIENT)
            CREDENTIAL_CACHE = None

    # Check 2: user can disable a credential cache on the client level
    if CREDENTIAL_CACHE is not None:
        if not os.path.exists(CREDENTIAL_CACHE):
            mkdir_p(CREDENTIAL_CACHE)
        client_credential_cache = '%s/%s' %(CREDENTIAL_CACHE, SREGISTRY_CLIENT)
    if client_credential_cache is not None:
        bot.debug('credentials cache')
    return client_credential_cache

def _default_client_secrets():
    '''return default client secrets, including singularity hub base
    '''
    client_secrets = { 'hub': { 'base': "https://singularity-hub.org/api" } }
    return client_secrets

def update_client_secrets(backend, updates, secrets=None, save=True):
    '''update client secrets will update the data structure for a particular
       authentication. This should only be used for a (quasi permanent) token
       or similar. The secrets file, if found, is updated and saved by default.
    '''
    if secrets is None:
        secrets = read_client_secrets()
    if backend not in secrets:
        secrets[backend] = {}
    secrets[backend].update(updates)

    # The update typically includes a save
    if save is True:
        secrets_file = get_secrets_file()
        if secrets_file is not None:
            write_json(secrets,secrets_file)

    return secrets

def read_client_secrets():
    '''for private or protected registries, a client secrets file is required
       to be located at .sregistry. If no secrets are found, we use default
       of Singularity Hub, and return a dummy secrets.
    '''
    client_secrets = _default_client_secrets()

    # If token file not provided, check environment
    secrets = get_secrets_file()

    # If exists, load
    if secrets is not None:
        client_secrets = read_json(secrets)

    # Otherwise, initialize
    else:
        from sregistry.defaults import SREGISTRY_CLIENT_SECRETS
        write_json(client_secrets, SREGISTRY_CLIENT_SECRETS)

    return client_secrets

def get_secrets_file():
    '''Sniff the environment and standard location for client
       secrets file. Otherwise, return None
    '''
    from sregistry.defaults import SREGISTRY_CLIENT_SECRETS
    if os.path.exists(SREGISTRY_CLIENT_SECRETS):
        return SREGISTRY_CLIENT_SECRETS


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from sregistry.main import ApiConnection
import google
import json
import sys
import os

from retrying import retry
from google.cloud import storage
from googleapiclient.discovery import build as discovery_build
from oauth2client.client import GoogleCredentials

from .build import (
    build,
    load_build_config,
    setup_build,
    run_build,
    load_templates,
    get_templates,
    list_builders,
    list_templates,
    get_ipaddress,
    get_instances
)
from .delete import ( delete, destroy )
from .logs import ( logs, list_logs, print_log )
from .pull import pull
from .push import ( push, upload )
from .query import ( container_query, list_containers, search, search_all )

class Client(ApiConnection):

    def __init__(self, secrets=None, base=None, init=True, **kwargs):

        self._update_secrets()
        self._update_headers()

        # Do we need storage/compute client now?
        if init is True:
            self._init_client()

        super(ApiConnection, self).__init__(**kwargs)

    def _speak(self):
        '''add the bucket name to be printed to the user at appropriate times
        '''
        bot.info('[bucket][%s]' %self._bucket_name)

    def _update_secrets(self):
        '''The user is required to have an application secrets file in his
           or her environment. The information isn't saved to the secrets
           file, but the client exists with error if the variable isn't found.
        '''
        env = 'GOOGLE_APPLICATION_CREDENTIALS'
        self._secrets = self._get_and_update_setting(env)
        if self._secrets is None:
            bot.error('You must export %s to use Google Storage client' %env)
            sys.exit(1)

    def _init_client(self):
        '''init client will check if the user has defined a bucket that
           differs from the default, use the application credentials to
           get the bucket, and then instantiate the client.
        '''

        # Get storage and compute services
        self._get_services()

        env = 'SREGISTRY_GOOGLE_STORAGE_BUCKET'
        self._bucket_name = self._get_and_update_setting(env)

        # If the user didn't set in environment, use default
        if self._bucket_name is None:
            self._bucket_name = 'sregistry-%s' %os.environ['USER']

        self._get_bucket()

    def _get_services(self, version='v1'):
        '''get version 1 of the google compute and storage service

        Parameters
        ==========
        version: version to use (default is v1)
        '''
        self._bucket_service = storage.Client()
        creds = GoogleCredentials.get_application_default()
        self._storage_service = discovery_build('storage', version, credentials=creds)
        self._compute_service = discovery_build('compute', version, credentials=creds)

    def _get_bucket(self):
        '''get a bucket based on a bucket name. If it doesn't exist, create it.
        '''

        # Case 1: The bucket already exists
        try:
            self._bucket = self._bucket_service.get_bucket(self._bucket_name)

        # Case 2: The bucket needs to be created
        except google.cloud.exceptions.NotFound:
            self._bucket = self._bucket_service.create_bucket(self._bucket_name)

        # Case 3: The bucket name is already taken
        except:
            bot.error('Cannot get or create %s' %self._bucket_name)
            sys.exit(1)

        return self._bucket

    def _get_project(self, project=None):
        '''get project returns the active project, and exists if not found.

           Parameters
           ==========
           project: a project to default to, if not found in the environment
           zone: a default zone, will be us-west1-a by default

        '''
        return self._required_get_and_update('SREGISTRY_GOOGLE_PROJECT', project)

    def _get_zone(self, zone='us-west1-a'):
        '''get zone returns the zone set in the environment, or the default

           Parameters
           ==========
           zone: a default zone, will be us-west1-a by default

        '''
        return self._get_and_update_setting('SREGISTRY_GOOGLE_ZONE', zone)

Client.pull = pull
Client.push = push
Client._upload = upload
Client.delete = delete
Client.destroy = destroy

# Build functions
Client.build = build
Client._setup_build = setup_build
Client._run_build = run_build
Client._get_instances = get_instances
Client._get_ipaddress = get_ipaddress
Client._get_templates = get_templates
Client._load_build_config = load_build_config
Client._load_templates = load_templates
Client.list_builders = list_builders
Client.list_templates = list_templates
Client.logs = logs
Client._list_logs = list_logs
Client._print_log = print_log

Client.search = search
Client._search_all = search_all
Client._container_query = container_query
Client._list_containers = list_containers


'''

Copyright (C) 2017-2019 Vanessa Sochat.

This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

'''

from sregistry.logger import bot
from retrying import retry
import json
import sys
import os

@retry(wait_exponential_multiplier=1000,
       wait_exponential_max=10000,
       stop_max_attempt_number=3)

def delete_object(service, bucket_name, object_name):
    '''delete object will delete a file from a bucket

       Parameters
       ==========
       storage_service: the service obtained with get_storage_service
       bucket_name: the name of the bucket
       object_name: the "name" parameter of the object.

    '''
    try:
        operation = service.objects().delete(bucket=bucket_name,
                                             object=object_name).execute()
    except HttpError as e:
        pass
        operation = e
    return operation

def delete(self, name):
    '''delete an image from Google Storage.

       Parameters
       ==========
       name: the name of the file (or image) to delete

    '''

    bot.debug("DELETE %s" % name)

    for file_object in files:
        if isinstance(file_object, dict):
            if "kind" in file_object:
                if file_object['kind'] == "storage#object":
                    object_name = "/".join(file_object['id'].split('/')[:-1])
                    object_name = re.sub('%s/' %self._bucket['name'],'', object_name,1)

                    delete_object(service=self._bucket_service,
                                  bucket_name=bucket['name'],
                                  object_name=object_name)

@retry(wait_exponential_multiplier=1000,
       wait_exponential_max=10000,
       stop_max_attempt_number=3)

def destroy(self, name):
    '''destroy an instance, meaning take down the instance and stop the build.

       Parameters
       ==========
       name: the name of the instance to stop building.
    '''

    instances = self._get_instances()
    project = self._get_project()
    zone = self._get_zone()
    found = False

    if 'items' in instances:
        for instance in instances['items']:
            if instance['name'] == name:
                found = True
                break

    if found:
        bot.info('Killing instance %s' %name)
        return self._compute_service.instances().delete(project=project,
                                                        zone=zone,
                                                        instance=name).execute()