# Copyright 2010 Jacob Kaplan-Moss
#
# Copyright (c) 2011-2014 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import print_function

import argparse
import copy
import os
import sys
import time

from cinderclient import exceptions
from cinderclient.openstack.common import strutils
from cinderclient import utils
from cinderclient.v1 import availability_zones


def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
                     poll_period=5, show_progress=True):
    """Blocks while an action occurs. Periodically shows progress."""
    def print_progress(progress):
        if show_progress:
            msg = ('\rInstance %(action)s... %(progress)s%% complete'
                   % dict(action=action, progress=progress))
        else:
            msg = '\rInstance %(action)s...' % dict(action=action)

        sys.stdout.write(msg)
        sys.stdout.flush()

    print()
    while True:
        obj = poll_fn(obj_id)
        status = obj.status.lower()
        progress = getattr(obj, 'progress', None) or 0
        if status in final_ok_states:
            print_progress(100)
            print("\nFinished")
            break
        elif status == "error":
            print("\nError %(action)s instance" % {'action': action})
            break
        else:
            print_progress(progress)
            time.sleep(poll_period)


def _find_volume_snapshot(cs, snapshot):
    """Gets a volume snapshot by name or ID."""
    return utils.find_resource(cs.volume_snapshots, snapshot)


def _find_backup(cs, backup):
    """Gets a backup by name or ID."""
    return utils.find_resource(cs.backups, backup)


def _find_transfer(cs, transfer):
    """Gets a transfer by name or ID."""
    return utils.find_resource(cs.transfers, transfer)


def _find_qos_specs(cs, qos_specs):
    """Gets a qos specs by ID."""
    return utils.find_resource(cs.qos_specs, qos_specs)


def _print_volume(volume):
    utils.print_dict(volume._info)


def _print_volume_snapshot(snapshot):
    utils.print_dict(snapshot._info)


def _print_volume_image(image):
    utils.print_dict(image[1]['os-volume_upload_image'])


def _translate_keys(collection, convert):
    for item in collection:
        keys = item.__dict__
        for from_key, to_key in convert:
            if from_key in keys and to_key not in keys:
                setattr(item, to_key, item._info[from_key])


def _translate_volume_keys(collection):
    convert = [('displayName', 'display_name'), ('volumeType', 'volume_type'),
               ('os-vol-tenant-attr:tenant_id', 'tenant_id')]
    _translate_keys(collection, convert)


def _translate_volume_snapshot_keys(collection):
    convert = [('displayName', 'display_name'), ('volumeId', 'volume_id')]
    _translate_keys(collection, convert)


def _translate_availability_zone_keys(collection):
    convert = [('zoneName', 'name'), ('zoneState', 'status')]
    _translate_keys(collection, convert)


def _extract_metadata(args):
    metadata = {}
    for metadatum in args.metadata:
        # unset doesn't require a val, so we have the if/else
        if '=' in metadatum:
            (key, value) = metadatum.split('=', 1)
        else:
            key = metadatum
            value = None

        metadata[key] = value
    return metadata


@utils.arg(
    '--all-tenants',
    dest='all_tenants',
    metavar='<0|1>',
    nargs='?',
    type=int,
    const=1,
    default=0,
    help='Shows details for all tenants. Admin only.')
@utils.arg(
    '--all_tenants',
    nargs='?',
    type=int,
    const=1,
    help=argparse.SUPPRESS)
@utils.arg(
    '--display-name',
    metavar='<display-name>',
    default=None,
    help='Filters list by a volume display name. Default=None.')
@utils.arg(
    '--status',
    metavar='<status>',
    default=None,
    help='Filters list by a status. Default=None.')
@utils.arg(
    '--metadata',
    type=str,
    nargs='*',
    metavar='<key=value>',
    help='Filters list by metadata key and value pair. '
    'Default=None.',
    default=None)
@utils.arg('--availability-zone',
           metavar='<availability-zone>',
           help='Filters list by availability zone.')
@utils.service_type('volume')
def do_list(cs, args):
    """Lists all volumes."""
    all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants))
    search_opts = {
        'all_tenants': all_tenants,
        'display_name': args.display_name,
        'availability-zone': args.availability_zone,
        'status': args.status,
        'metadata': _extract_metadata(args) if args.metadata else None,
    }
    volumes = cs.volumes.list(search_opts=search_opts)
    _translate_volume_keys(volumes)

    # Create a list of servers to which the volume is attached
    for vol in volumes:
        servers = [s.get('server_id') for s in vol.attachments]
        setattr(vol, 'attached_to', ','.join(map(str, servers)))
    if all_tenants:
        key_list = ['ID', 'Tenant ID', 'Status', 'Display Name',
                    'Size', 'Volume Type', 'Bootable', 'Attached to']
    else:
        key_list = ['ID', 'Status', 'Display Name',
                    'Size', 'Volume Type', 'Bootable', 'Attached to']
    utils.print_list(volumes, key_list)


@utils.arg('volume', metavar='<volume>', help='Volume name or ID.')
@utils.service_type('volume')
def do_show(cs, args):
    """Shows volume details."""
    volume = utils.find_volume(cs, args.volume)
    _print_volume(volume)


@utils.arg('size',
           metavar='<size>',
           type=int,
           help='Volume size, in GBs.')
@utils.arg(
    '--snapshot-id',
    metavar='<snapshot-id>',
    default=None,
    help='Creates volume from snapshot ID. '
    'Default=None.')
@utils.arg(
    '--snapshot_id',
    help=argparse.SUPPRESS)
@utils.arg(
    '--source-volid',
    metavar='<source-volid>',
    default=None,
    help='Creates volume from volume ID. '
    'Default=None.')
@utils.arg(
    '--source_volid',
    help=argparse.SUPPRESS)
@utils.arg(
    '--image-id',
    metavar='<image-id>',
    default=None,
    help='Creates volume from image ID. '
    'Default=None.')
@utils.arg(
    '--image_id',
    help=argparse.SUPPRESS)
@utils.arg(
    '--display-name',
    metavar='<display-name>',
    default=None,
    help='Volume name. '
    'Default=None.')
@utils.arg(
    '--display_name',
    help=argparse.SUPPRESS)
@utils.arg(
    '--display-description',
    metavar='<display-description>',
    default=None,
    help='Volume description. '
    'Default=None.')
@utils.arg(
    '--display_description',
    help=argparse.SUPPRESS)
@utils.arg(
    '--volume-type',
    metavar='<volume-type>',
    default=None,
    help='Volume type. '
    'Default=None.')
@utils.arg(
    '--volume_type',
    help=argparse.SUPPRESS)
@utils.arg(
    '--availability-zone',
    metavar='<availability-zone>',
    default=None,
    help='Availability zone for volume. '
    'Default=None.')
@utils.arg(
    '--availability_zone',
    help=argparse.SUPPRESS)
@utils.arg('--metadata',
           type=str,
           nargs='*',
           metavar='<key=value>',
           help='Metadata key and value pairs. '
           'Default=None.',
           default=None)
@utils.arg(
    '--shareable',
    metavar="<'T'|'F'>",
    help=('Allow volume to be attached more than once '
          '(Optional, Default=False)'),
    default=False)
@utils.service_type('volume')
def do_create(cs, args):
    """Creates a volume."""

    volume_metadata = None
    if args.metadata is not None:
        volume_metadata = _extract_metadata(args)

    volume = cs.volumes.create(args.size,
                               args.snapshot_id,
                               args.source_volid,
                               args.display_name,
                               args.display_description,
                               args.volume_type,
                               availability_zone=args.availability_zone,
                               imageRef=args.image_id,
                               metadata=volume_metadata,
                               shareable=strutils.bool_from_string(args.shareable))
    _print_volume(volume)


@utils.arg('volume', metavar='<volume>', nargs='+',
           help='Name or ID of volume to delete. '
           'Separate multiple volumes with a space.')
@utils.service_type('volume')
def do_delete(cs, args):
    """Removes one or more volumes."""
    failure_count = 0
    for volume in args.volume:
        try:
            utils.find_volume(cs, volume).delete()
        except Exception as e:
            failure_count += 1
            print("Delete for volume %s failed: %s" % (volume, e))
    if failure_count == len(args.volume):
        raise exceptions.CommandError("Unable to delete any of the specified "
                                      "volumes.")


@utils.arg('volume', metavar='<volume>', nargs='+',
           help='Name or ID of volume to delete. '
           'Separate multiple volumes with a space.')
@utils.service_type('volume')
def do_force_delete(cs, args):
    """Attempts force-delete of volume, regardless of state."""
    failure_count = 0
    for volume in args.volume:
        try:
            utils.find_volume(cs, volume).force_delete()
        except Exception as e:
            failure_count += 1
            print("Delete for volume %s failed: %s" % (volume, e))
    if failure_count == len(args.volume):
        raise exceptions.CommandError("Unable to force delete any of the "
                                      "specified volumes.")


@utils.arg('volume', metavar='<volume>', nargs='+',
           help='Name or ID of volume to modify. '
           'Separate multiple volumes with a space.')
@utils.arg('--state', metavar='<state>', default='available',
           help=('The state to assign to the volume. Valid values are '
                 '"available," "error," "creating," "deleting," or '
                 '"error_deleting." '
                 'Default is "available."'))
@utils.service_type('volume')
def do_reset_state(cs, args):
    """Explicitly updates the volume state."""
    failure_flag = False

    for volume in args.volume:
        try:
            utils.find_volume(cs, volume).reset_state(args.state)
        except Exception as e:
            failure_flag = True
            msg = "Reset state for volume %s failed: %s" % (volume, e)
            print(msg)

    if failure_flag:
        msg = "Unable to reset the state for the specified volume(s)."
        raise exceptions.CommandError(msg)


@utils.arg('volume', metavar='<volume>',
           help='Name or ID of volume to rename.')
@utils.arg('display_name', nargs='?', metavar='<display-name>',
           help='New display name for volume.')
@utils.arg('--display-description', metavar='<display-description>',
           help='Volume description. Default=None.',
           default=None)
@utils.service_type('volume')
def do_rename(cs, args):
    """Renames a volume."""
    kwargs = {}
    if args.display_name is not None:
        kwargs['display_name'] = args.display_name
    if args.display_description is not None:
        kwargs['display_description'] = args.display_description

    if not any(kwargs):
        msg = 'Must supply either display-name or display-description.'
        raise exceptions.ClientException(code=1, message=msg)

    utils.find_volume(cs, args.volume).update(**kwargs)


@utils.arg('volume',
           metavar='<volume>',
           help='Name or ID of volume for which to update metadata.')
@utils.arg('action',
           metavar='<action>',
           choices=['set', 'unset'],
           help="The action. Valid values are 'set' or 'unset.'")
@utils.arg('metadata',
           metavar='<key=value>',
           nargs='+',
           default=[],
           help='The metadata key and pair to set or unset. '
           'For unset, specify only the key. '
           'Default=[].')
@utils.service_type('volume')
def do_metadata(cs, args):
    """Sets or deletes volume metadata."""
    volume = utils.find_volume(cs, args.volume)
    metadata = _extract_metadata(args)

    if args.action == 'set':
        cs.volumes.set_metadata(volume, metadata)
    elif args.action == 'unset':
        # NOTE(zul): Make sure py2/py3 sorting is the same
        cs.volumes.delete_metadata(volume, sorted(metadata.keys(),
                                   reverse=True))


@utils.arg(
    '--all-tenants',
    dest='all_tenants',
    metavar='<0|1>',
    nargs='?',
    type=int,
    const=1,
    default=0,
    help='Shows details for all tenants. Admin only.')
@utils.arg(
    '--all_tenants',
    nargs='?',
    type=int,
    const=1,
    help=argparse.SUPPRESS)
@utils.arg(
    '--display-name',
    metavar='<display-name>',
    default=None,
    help='Filters list by a display name. Default=None.')
@utils.arg(
    '--status',
    metavar='<status>',
    default=None,
    help='Filters list by a status. Default=None.')
@utils.arg(
    '--volume-id',
    metavar='<volume-id>',
    default=None,
    help='Filters list by a volume ID. Default=None.')
@utils.service_type('volume')
def do_snapshot_list(cs, args):
    """Lists all snapshots."""
    all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants))
    search_opts = {
        'all_tenants': all_tenants,
        'display_name': args.display_name,
        'status': args.status,
        'volume_id': args.volume_id,
    }

    snapshots = cs.volume_snapshots.list(search_opts=search_opts)
    _translate_volume_snapshot_keys(snapshots)
    utils.print_list(snapshots,
                     ['ID', 'Volume ID', 'Status', 'Display Name', 'Size'])


@utils.arg('snapshot', metavar='<snapshot>',
           help='Name or ID of snapshot.')
@utils.service_type('volume')
def do_snapshot_show(cs, args):
    """Shows snapshot details."""
    snapshot = _find_volume_snapshot(cs, args.snapshot)
    _print_volume_snapshot(snapshot)


@utils.arg('volume',
           metavar='<volume>',
           help='Name or ID of volume to snapshot.')
@utils.arg('--force',
           metavar='<True|False>',
           help='Allows or disallows snapshot of '
           'a volume when the volume is attached to an instance. '
           'If set to True, ignores the current status of the '
           'volume when attempting to snapshot it rather '
           'than forcing it to be available. '
           'Default=False.',
           default=False)
@utils.arg(
    '--display-name',
    metavar='<display-name>',
    default=None,
    help='The snapshot name. Default=None.')
@utils.arg(
    '--display_name',
    help=argparse.SUPPRESS)
@utils.arg(
    '--display-description',
    metavar='<display-description>',
    default=None,
    help='The snapshot description. Default=None.')
@utils.arg(
    '--display_description',
    help=argparse.SUPPRESS)
@utils.service_type('volume')
def do_snapshot_create(cs, args):
    """Creates a snapshot."""
    volume = utils.find_volume(cs, args.volume)
    snapshot = cs.volume_snapshots.create(volume.id,
                                          args.force,
                                          args.display_name,
                                          args.display_description)
    _print_volume_snapshot(snapshot)


@utils.arg('snapshot',
           metavar='<snapshot>', nargs='+',
           help='Name or ID of the snapshot(s) to delete.')
@utils.service_type('volume')
def do_snapshot_delete(cs, args):
    """Remove one or more snapshots."""
    failure_count = 0
    for snapshot in args.snapshot:
        try:
            _find_volume_snapshot(cs, snapshot).delete()
        except Exception as e:
            failure_count += 1
            print("Delete for snapshot %s failed: %s" % (snapshot, e))
    if failure_count == len(args.snapshot):
        raise exceptions.CommandError("Unable to delete any of the specified "
                                      "snapshots.")


@utils.arg('snapshot', metavar='<snapshot>',
           help='Name or ID of snapshot.')
@utils.arg('display_name', nargs='?', metavar='<display-name>',
           help='New display name for snapshot.')
@utils.arg('--display-description', metavar='<display-description>',
           help='Snapshot description. Default=None.',
           default=None)
@utils.service_type('volume')
def do_snapshot_rename(cs, args):
    """Renames a snapshot."""
    kwargs = {}
    if args.display_name is not None:
        kwargs['display_name'] = args.display_name
    if args.display_description is not None:
        kwargs['display_description'] = args.display_description

    if not any(kwargs):
        msg = 'Must supply either display-name or display-description.'
        raise exceptions.ClientException(code=1, message=msg)

    _find_volume_snapshot(cs, args.snapshot).update(**kwargs)


@utils.arg('snapshot', metavar='<snapshot>', nargs='+',
           help='Name or ID of snapshot to modify.')
@utils.arg('--state', metavar='<state>',
           default='available',
           help=('The state to assign to the snapshot. Valid values are '
                 '"available," "error," "creating," "deleting," or '
                 '"error_deleting." '
                 'Default is "available."'))
@utils.service_type('volume')
def do_snapshot_reset_state(cs, args):
    """Explicitly updates the snapshot state."""
    failure_count = 0

    single = (len(args.snapshot) == 1)

    for snapshot in args.snapshot:
        try:
            _find_volume_snapshot(cs, snapshot).reset_state(args.state)
        except Exception as e:
            failure_count += 1
            msg = "Reset state for snapshot %s failed: %s" % (snapshot, e)
            if not single:
                print(msg)

    if failure_count == len(args.snapshot):
        if not single:
            msg = ("Unable to reset the state for any of the the specified "
                   "snapshots.")
        raise exceptions.CommandError(msg)


def _print_volume_type_list(vtypes):
    utils.print_list(vtypes, ['ID', 'Name'])


@utils.service_type('volume')
def do_type_list(cs, args):
    """Lists available 'volume types'."""
    vtypes = cs.volume_types.list()
    _print_volume_type_list(vtypes)


@utils.service_type('volume')