# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright 2016 Continuum Analytics, Inc.
#
# May be copied and distributed freely only as part of an Anaconda or
# Miniconda installation.
# -----------------------------------------------------------------------------
import json
import os.path
import sys
from collections import defaultdict

import click

from anaconda_enterprise.cli import deconstruct
from anaconda_enterprise.cli.api import create_client
from anaconda_enterprise.cli.internal import yaml

from . import upload

ERROR_ = click.style('ERROR', fg='red')
WARNING_ = click.style('WARNING', fg='yellow')
INFO_ = click.style('INFO', fg='green')

META = 'meta.json'


@click.group(name='admin', hidden=True)
def main():
    """This command group includes several commands reserved primarily
       for use within the AE5 repository subsystem. For this reason,
       this group is hidden by default from the help output.
    """


def validate_extra_attributes(ctx, param, value):
    try:
        content = yaml.load(value)
        if not isinstance(content, dict):
            raise click.BadParameter('extra attributes needs to be an object')
        return content
    except ValueError as exc:
        raise click.BadParameter(exc.args[0])


@main.command()
@click.argument('installer', type=click.Path(exists=True), nargs=-1)
@click.option('--extra-attributes',
              default='{}',
              callback=validate_extra_attributes)
@click.pass_obj  # object in context is the Configuration Manager
def upload_installer(config_manager, installer, extra_attributes):
    """Upload an installer/parcel for distribution
    """
    client = create_client(config_manager)

    upload_count = 0
    for pkg in installer:
        pkg_name = os.path.basename(pkg)
        size = os.path.getsize(pkg)

        try:
            click.echo('Uploading {0} ({1} bytes)'.format(pkg_name, size),
                       nl=False)
            meta = deconstruct.extract(pkg)

            spec_id = extra_attributes.pop("env_spec_id", None)
            meta['extra-attributes'] = extra_attributes
            upload_info = client.stage_installer(meta, env_spec_id=spec_id)

            with open(pkg, 'rb') as fd:
                client.upload_file(upload_info, fd)

            upload_count += 1
            click.echo('.')
        except Exception as exc:
            click.echo('\n{0}: {1}'.format(ERROR_, exc))

    if upload_count == len(installer):
        click.echo('Complete. Uploaded {0} of {1}'.format(
            upload_count, len(installer)))
    else:
        click.echo('Failed. Uploaded {0} of {1}'.format(
            upload_count, len(installer)))
        sys.exit(1)


@main.command()
@click.argument('url')
@click.pass_obj  # object in context is the Configuration Manager
def fetch_resource(config_manager, url):
    """Perform arbitrary calls to the AE5 API
    """
    client = create_client(config_manager)
    print(json.dumps(client.fetch_resource(url), indent=2))


@main.command()
@click.argument('env_spec_id')
@click.argument('lock', type=click.Path(exists=True), nargs=1)
@click.option('--extra-attributes',
              default='{}',
              callback=validate_extra_attributes)
@click.pass_obj  # object in context is the Configuration Manager
def store_env_spec_lock(config_manager, env_spec_id, lock, extra_attributes):
    """Upload a resolved environment spec
    """
    client = create_client(config_manager)
    with open(lock) as fd:
        lock = json.load(fd)
    client.store_env_spec_lock(env_spec_id, lock, extra_attributes)


@main.command(name="import")
@click.argument("dump", type=click.Path(exists=True), nargs=-1)
@click.option("--dry-run", default=False)
@click.pass_context
def import_(ctx, dump, dry_run):
    """Import an Anaconda Repository v4 tree

    Keywords:

    ``dump`` -- A list of directories that are the result of using
    ``anaconda-server-admin export``

    ``dry_run`` -- If set to True prints a YAML to stdout which
    includes channels, file to channel mapping and the user's
    channels.

    Functionality:

    For every user in a repo4 it will create a new channel per label
    the user has, using the username as a prefix.

    An example:

    Say the user ``anaconda-user-1`` has the following packages:

    * ``moto-0.4.31-2.tar.bz2`` with label main
    * ``pysocks-1.6.6-py35_0.tar.bz2`` with label test

    For this user, the script will create the following channels:

    * ``anaconda-user-1`` with file ``moto-0.4.31-2.tar.bz2``
    * ``anaconda-user-1/test`` with file ``pysocks-1.6.6-py35_0.tar.bz2``

    The label "main" is dropped in preference of just keeping the
    username as channel name.

    For every organization we have to add the groups to the channel
    names.

    Say ``anaconda-organization`` has a group called "Devs" and there
    are packages there such as ``xz-5.2.2-1.tar.bz2`` which has label
    test. This script will create the following channels:

    * ``anaconda-organization`` -- This will have everything from the Owner group

    * ``anaconda-organization/Devs`` -- This will have everything that's
      available for the Dev group

    * ``anaconda-organization/Devs/test`` -- This will have everything
      that's in the Dev group with label test.

    So for an organization, group, and label a new channel is
    created. Just that the label "main" and group "Owner" are dropped
    in preference of making names shorter.
    """

    # here we keep channels and files
    _import_data = []

    # The following two vars are going to be used to join information regarding
    # which channels should be shared with the users.
    _user_acl_data = {}
    _chan_prefix_and_labels = defaultdict(set)

    # In v4 each file in a package can have a set of labels. Those labels should be changed
    # to channels. For a user that's *not* an organization, that is pretty simple.
    # We create a channel for each pair (user, label) e.g: user/main, user/test ("/" is valid in a
    # channel name). The complicated bit comes with organizations that have groups of users.

    # For an org. we should create a channel for each triplet (organization, group, label) and
    # the files that go in there depend on the on the group and label they have.

    for path_ in dump:

        meta_path = os.path.join(path_, META)
        if not os.path.exists(meta_path):
            click.echo("{0}: {1}".format(
                WARNING_, "This directory does not contain user info"))
            continue

        # Open the meta.json that has the user information
        with open(meta_path) as user_meta:
            user_info = json.load(user_meta)

        username = user_info["username"]

        # This holds the data we're going to import.
        account = {'username': username, 'files': [], 'channels': []}

        # Let's grab the group information of the user
        if user_info["user_type"] == "user":
            # We have to give permission to this user to the channels
            # which are prefixed with the following:
            extra_channels_for_user = [
                "{username}/{group}".format(username=g["owner"]["username"],
                                            group=g["name"])
                for g in user_info["groups"]
            ]
            # ^ The labels for the "extra channels" above are in the
            # organization account information (the meta.json for that user).
            # We use this data to match which users are allowed to which channels.

            _user_acl_data[username] = extra_channels_for_user

        # Fetch all the packages for this user from the meta.json
        for package in user_info["packages"]:

            package_info = {}

            path_to_packages = os.path.join(path_,
                                            package["package_output_dir"])
            with open(os.path.join(path_to_packages, META)) as package_meta:
                package_info = json.load(package_meta)

            channel_prefixes = [username]
            if user_info["user_type"] == 'org':
                channel_prefixes += [
                    "{username}/{group}".format(
                        username=g["owner"]["username"], group=g["name"])
                    for g in package_info["groups"]
                ]

            for file_ in package_info['files']:
                filepath = file_["dump_filename"]
                dist_type = file_["distribution_type"]

                # Ignore those that are not conda packages
                if dist_type != "conda":
                    click.echo(
                        "{0}: {1} is a {2} and will not be uploaded.".format(
                            WARNING_, filepath, dist_type))
                    continue

                # Ignore those that don't have labels.
                if not file_["labels"]:
                    click.echo(
                        "{0}: {1} has no labels and will not be uploaded".
                        format(WARNING_, filepath))
                    continue

                file_attrs = {}
                file_attrs["path"] = "{}".format(
                    os.path.join(path_to_packages, filepath))
                file_attrs["basename"] = file_[
                    "basename"]  # used to copy later on

                file_channels = []
                for p in channel_prefixes:
                    chan_names = [
                        chan_name(p, label) for label in file_["labels"]
                    ]
                    # we need this join the user's groups with the channels created
                    # for an organization.
                    if user_info["user_type"] == 'org':
                        _chan_prefix_and_labels[p].update(chan_names)
                    file_channels.extend(chan_names)

                file_attrs["channels"] = file_channels

                account["files"].append(file_attrs)

                for chan in file_channels:
                    if chan not in account["channels"]:
                        account["channels"].append(chan)
        _import_data.append(account)

    # Join user/channel data on the "prefix"
    _join_user_chans = {}
    for key, prefixes in _user_acl_data.items():
        users_chan = _join_user_chans.setdefault(key, [])
        for p in prefixes:
            for chan in _chan_prefix_and_labels[p]:
                users_chan.append(chan)

    if dry_run:
        yaml.dump({
            "accounts": _import_data,
            "sharing": _join_user_chans
        }, sys.stdout)
        return

    # for each account create the channels, and upload each file
    # to the corresponding channel, and share with the user or groups.
    client = create_client(ctx.obj)

    for account in _import_data:
        files = account['files']
        username = account['username']

        if not files:
            continue

        # create the channels before uploading
        channels = account['channels']
        for chan in channels:
            if client.get_channel(chan) is None:
                click.echo("{0}: Creating {1}".format(INFO_, chan))
                chan_obj = client.create_channel(chan)
                click.echo("{0}: Sharing {1} with user {2} as rw".format(
                    INFO_, chan, username))
                client.share_channel(chan_obj, username, 'users', 'rw')

        for file_ in account['files']:
            # Upload to a channel
            chan = file_["channels"][0]
            click.echo("{0}: uploading {1}".format(INFO_, file_['path']))
            try:
                upload._upload(ctx.obj,
                               channel=chan,
                               package=[file_['path']],
                               ignore_name=True)
            except Exception as e:
                click.echo("\n{0}: {1} from file: {2}".format(
                    ERROR_, e, file_["path"]))
                continue

            # Copy to the rest of the channels
            src_chan = chan
            src_subdir, src_filename = file_["basename"].rsplit("/", 1)
            for chan in file_['channels'][1:]:
                click.echo("{0}: copying {1} to {2}".format(
                    INFO_, file_['path'], chan))
                dest_chan = client.get_channel(chan)
                client.copy_pkg_file(src_chan, src_subdir, src_filename,
                                     dest_chan["id"])

    for username, channels in _join_user_chans.items():
        if not channels:
            continue
        for chan in channels:
            chan_obj = client.get_channel(chan)
            click.echo("{0}: Sharing {1} with user {2} as r".format(
                INFO_, chan, username))
            client.share_channel(chan_obj, username, 'users', 'r')


def chan_name(prefix, label):
    """Generate a channel name from a prefix and label

    Keywords:

    * prefix -- a str, which is the username or username/group
    * label -- a str, which is the label of a package in Repository v4.
    """
    owners_prefix = "/Owners"

    if owners_prefix in prefix:
        channel_name = prefix[:len(owners_prefix)]
    else:
        channel_name = prefix

    if label != 'main':
        channel_name += "/" + label
    return channel_name
