#
#   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.
#

"""Volume V3 Volume action implementations"""

import argparse
from collections.abc import Iterable, Sequence
import functools
import logging
from typing import Any

from cliff import columns as cliff_columns
from openstack.block_storage.v3 import volume as _volume
from openstack import exceptions as sdk_exceptions
from openstack import utils as sdk_utils
from osc_lib.cli import format_columns
from osc_lib.cli import parseractions
from osc_lib import exceptions
from osc_lib import utils

from openstackclient.api import volume_v3
from openstackclient import command
from openstackclient.common import pagination
from openstackclient.i18n import _
from openstackclient.identity import common as identity_common


LOG = logging.getLogger(__name__)


class KeyValueHintAction(argparse.Action):
    """Uses KeyValueAction or KeyValueAppendAction based on the given key"""

    APPEND_KEYS = ('same_host', 'different_host')

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        self._key_value_action = parseractions.KeyValueAction(*args, **kwargs)
        self._key_value_append_action = parseractions.KeyValueAppendAction(
            *args, **kwargs
        )
        super().__init__(*args, **kwargs)

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,
        values: Any,
        option_string: str | None = None,
    ) -> None:
        if values.startswith(self.APPEND_KEYS):
            self._key_value_append_action(
                parser, namespace, values, option_string=option_string
            )
        else:
            self._key_value_action(
                parser, namespace, values, option_string=option_string
            )


class AttachmentsColumn(cliff_columns.FormattableColumn[list[Any]]):
    """Formattable column for attachments column.

    Unlike the parent FormattableColumn class, the initializer of the
    class takes server_cache as the second argument.
    osc_lib.utils.get_item_properties instantiate cliff FormattableColumn
    object with a single parameter "column value", so you need to pass
    a partially initialized class like
    ``functools.partial(AttachmentsColumn, server_cache)``.
    """

    def __init__(
        self, value: list[Any], server_cache: dict[str, Any] | None = None
    ) -> None:
        super().__init__(value)
        self._server_cache = server_cache or {}

    def human_readable(self) -> str:
        """Return a formatted string of a volume's attached instances

        :rtype: a string of formatted instances
        """

        msg = ''
        for attachment in self._value:
            server = attachment['server_id']
            if server in self._server_cache.keys():
                server = self._server_cache[server].name
            device = attachment['device']
            msg += f'Attached to {server} on {device} '
        return msg


def _format_volume(volume: _volume.Volume) -> dict[str, object]:
    # Some columns returned by openstacksdk should not be shown because they're
    # either irrelevant or duplicates
    ignored_columns = {
        # computed columns
        'location',
        # create-only columns
        'OS-SCH-HNT:scheduler_hints',
        'imageRef',
        # removed columns
        'os-volume-replication:driver_data',
        'os-volume-replication:extended_status',
        # unnecessary columns
        'links',
    }
    optional_columns = {
        # only present if part of a consistency group
        'consistencygroup_id',
        # only present if the volume is encrypted
        'encryption_key_id',
        # only present if there are image properties associated
        'volume_image_metadata',
    }

    info = volume.to_dict(original_names=True)
    data = {}
    for key, value in info.items():
        if key in ignored_columns:
            continue

        if key in optional_columns:
            if info[key] is None:
                continue

        data[key] = value

    data.update(
        {
            'properties': format_columns.DictColumn(data.pop('metadata')),
            'type': data.pop('volume_type'),
        }
    )

    return data


class CreateVolume(command.ShowOne):
    _description = _("Create new volume")

    @staticmethod
    def _check_size_arg(args: argparse.Namespace) -> None:
        """Check whether --size option is required or not.

        Require size parameter in case if any of the following is not
        specified:

        * snapshot
        * source volume
        * backup
        * remote source (volume to be managed)
        """

        if (
            args.snapshot or args.source or args.backup or args.remote_source
        ) is None and args.size is None:
            msg = _(
                "--size is a required option if none of --snapshot, "
                "--backup, --source, or --remote-source are provided."
            )
            raise exceptions.CommandError(msg)

    def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
        parser = super().get_parser(prog_name)
        parser.add_argument(
            "name",
            metavar="<name>",
            nargs="?",
            help=_("Volume name"),
        )
        parser.add_argument(
            "--size",
            metavar="<size>",
            type=int,
            help=_(
                "Volume size in GB (required unless --snapshot or "
                "--source specified)"
            ),
        )
        parser.add_argument(
            "--type",
            metavar="<volume-type>",
            help=_("Set the type of volume"),
        )
        source_group = parser.add_mutually_exclusive_group()
        source_group.add_argument(
            "--image",
            metavar="<image>",
            help=_("Use <image> as source of volume (name or ID)"),
        )
        source_group.add_argument(
            "--snapshot",
            metavar="<snapshot>",
            help=_("Use <snapshot> as source of volume (name or ID)"),
        )
        source_group.add_argument(
            "--source",
            metavar="<volume>",
            help=_("Volume to clone (name or ID)"),
        )
        source_group.add_argument(
            "--source-replicated",
            metavar="<replicated-volume>",
            help=argparse.SUPPRESS,
        )
        source_group.add_argument(
            "--backup",
            metavar="<backup>",
            help=_(
                "Restore backup to a volume (name or ID) "
                "(supported by --os-volume-api-version 3.47 or later)"
            ),
        )
        source_group.add_argument(
            "--remote-source",
            metavar="<key=value>",
            action=parseractions.KeyValueAction,
            help=_(
                "The attribute(s) of the existing remote volume "
                "(admin required) (repeat option to specify multiple "
                "attributes, e.g.: '--remote-source source-name=test_name "
                "--remote-source source-id=test_id')"
            ),
        )
        parser.add_argument(
            "--description",
            metavar="<description>",
            help=_("Volume description"),
        )
        parser.add_argument(
            "--availability-zone",
            metavar="<availability-zone>",
            help=_("Create volume in <availability-zone>"),
        )
        parser.add_argument(
            "--consistency-group",
            metavar="consistency-group>",
            help=_("Consistency group where the new volume belongs to"),
        )
        parser.add_argument(
            "--property",
            metavar="<key=value>",
            action=parseractions.KeyValueAction,
            dest="properties",
            help=_(
                "Set a property to this volume "
                "(repeat option to set multiple properties)"
            ),
        )
        parser.add_argument(
            "--hint",
            metavar="<key=value>",
            action=KeyValueHintAction,
            help=_(
                "Arbitrary scheduler hint key-value pairs to help creating "
                "a volume. Repeat the option to set multiple hints. "
                "'same_host' and 'different_host' get values appended when "
                "repeated, all other keys take the last given value"
            ),
        )
        bootable_group = parser.add_mutually_exclusive_group()
        bootable_group.add_argument(
            "--bootable",
            action="store_true",
            dest="bootable",
            default=None,
            help=_("Mark volume as bootable"),
        )
        bootable_group.add_argument(
            "--non-bootable",
            action="store_false",
            dest="bootable",
            default=None,
            help=_("Mark volume as non-bootable (default)"),
        )
        readonly_group = parser.add_mutually_exclusive_group()
        readonly_group.add_argument(
            "--read-only",
            action="store_true",
            dest="read_only",
            default=None,
            help=_("Set volume to read-only access mode"),
        )
        readonly_group.add_argument(
            "--read-write",
            action="store_false",
            dest="read_only",
            default=None,
            help=_("Set volume to read-write access mode (default)"),
        )
        parser.add_argument(
            "--host",
            metavar="<host>",
            help=_(
                "Cinder host on which the existing volume resides; "
                "takes the form: host@backend-name#pool. This is only "
                "used along with the --remote-source option."
            ),
        )
        parser.add_argument(
            "--cluster",
            metavar="<cluster>",
            help=_(
                "Cinder cluster on which the existing volume resides; "
                "takes the form: cluster@backend-name#pool. This is only "
                "used along with the --remote-source option. "
                "(supported by --os-volume-api-version 3.16 or above)"
            ),
        )
        return parser

    def take_action(
        self, parsed_args: argparse.Namespace
    ) -> tuple[Sequence[str], Iterable[Any]]:
        self._check_size_arg(parsed_args)
        # size is validated in the above call to
        # _check_size_arg where we check that size
        # should be passed if we are not creating a
        # volume from snapshot, backup or source volume
        size = parsed_args.size

        volume_client = sdk_utils.ensure_service_version(
            self.app.client_manager.volume, '3'
        )
        image_client = self.app.client_manager.image

        if (
            parsed_args.host or parsed_args.cluster
        ) and not parsed_args.remote_source:
            msg = _(
                "The --host and --cluster options are only supported "
                "with --remote-source parameter."
            )
            raise exceptions.CommandError(msg)

        if parsed_args.backup and not sdk_utils.supports_microversion(
            volume_client, '3.47'
        ):
            msg = _(
                "--os-volume-api-version 3.47 or greater is required "
                "to create a volume from backup."
            )
            raise exceptions.CommandError(msg)

        if parsed_args.remote_source:
            if (
                parsed_args.size
                or parsed_args.consistency_group
                or parsed_args.hint
                or parsed_args.read_only is not None
            ):
                msg = _(
                    "The --size, --consistency-group, --hint, --read-only "
                    "and --read-write options are not supported with the "
                    "--remote-source parameter."
                )
                raise exceptions.CommandError(msg)
            if parsed_args.cluster:
                if not sdk_utils.supports_microversion(volume_client, '3.16'):
                    msg = _(
                        "--os-volume-api-version 3.16 or greater is required "
                        "to support the cluster parameter."
                    )
                    raise exceptions.CommandError(msg)
            if parsed_args.cluster and parsed_args.host:
                msg = _(
                    "Only one of --host or --cluster needs to be specified "
                    "to manage a volume."
                )
                raise exceptions.CommandError(msg)
            if not parsed_args.cluster and not parsed_args.host:
                msg = _(
                    "One of --host or --cluster needs to be specified to "
                    "manage a volume."
                )
                raise exceptions.CommandError(msg)
            volume = volume_client.manage_volume(
                host=parsed_args.host,
                cluster=parsed_args.cluster,
                ref=parsed_args.remote_source,
                name=parsed_args.name,
                description=parsed_args.description,
                volume_type=parsed_args.type,
                availability_zone=parsed_args.availability_zone,
                metadata=parsed_args.properties,
                bootable=parsed_args.bootable,
            )
            data = _format_volume(volume)
            col_headers, col_data = zip(*sorted(data.items()))
            return col_headers, col_data

        source_volume = None
        if parsed_args.source:
            source_volume_obj = volume_client.find_volume(
                parsed_args.source, ignore_missing=False
            )
            source_volume = source_volume_obj.id
            size = max(size or 0, source_volume_obj.size)

        consistency_group = None
        if parsed_args.consistency_group:
            consistency_group = volume_v3.find_consistency_group(
                volume_client, parsed_args.consistency_group
            )['id']

        image = None
        if parsed_args.image:
            image = image_client.find_image(
                parsed_args.image, ignore_missing=False
            ).id

        snapshot = None
        if parsed_args.snapshot:
            snapshot_obj = volume_client.find_snapshot(
                parsed_args.snapshot, ignore_missing=False
            )
            snapshot = snapshot_obj.id
            # Cinder requires a value for size when creating a volume
            # even if creating from a snapshot. Cinder will create the
            # volume with at least the same size as the snapshot anyway,
            # so since we have the object here, just override the size
            # value if it's either not given or is smaller than the
            # snapshot size.
            size = max(size or 0, snapshot_obj.size)

        backup = None
        if parsed_args.backup:
            backup_obj = volume_client.find_backup(
                parsed_args.backup, ignore_missing=False
            )
            backup = backup_obj.id
            # As above
            size = max(size or 0, backup_obj.size)

        volume = volume_client.create_volume(
            size=size,
            snapshot_id=snapshot,
            name=parsed_args.name,
            description=parsed_args.description,
            volume_type=parsed_args.type,
            availability_zone=parsed_args.availability_zone,
            metadata=parsed_args.properties,
            image_id=image,
            source_volume_id=source_volume,
            consistency_group_id=consistency_group,
            scheduler_hints=parsed_args.hint,
            backup_id=backup,
        )

        if parsed_args.bootable is not None:
            try:
                if utils.wait_for_status(
                    volume_client.get_volume,
                    volume.id,
                    success_status=['available'],
                    error_status=['error'],
                    sleep_time=1,
                ):
                    volume_client.set_volume_bootable_status(
                        volume, parsed_args.bootable
                    )
                else:
                    msg = _(
                        "Volume status is not available for setting boot state"
                    )
                    raise exceptions.CommandError(msg)
            except Exception as e:
                LOG.error(_("Failed to set volume bootable property: %s"), e)

        if parsed_args.read_only is not None:
            try:
                if utils.wait_for_status(
                    volume_client.get_volume,
                    volume.id,
                    success_status=['available'],
                    error_status=['error'],
                    sleep_time=1,
                ):
                    volume_client.set_volume_readonly(
                        volume, parsed_args.read_only
                    )
                else:
                    msg = _(
                        "Volume status is not available for setting it"
                        "read only."
                    )
                    raise exceptions.CommandError(msg)
            except Exception as e:
                LOG.error(
                    _("Failed to set volume read-only access mode flag: %s"),
                    e,
                )

        data = _format_volume(volume)
        col_headers, col_data = zip(*sorted(data.items()))
        return col_headers, col_data


class DeleteVolume(command.Command):
    _description = _("Delete volume(s)")

    def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
        parser = super().get_parser(prog_name)
        parser.add_argument(
            "volumes",
            metavar="<volume>",
            nargs="+",
            help=_("Volume(s) to delete (name or ID)"),
        )
        group = parser.add_mutually_exclusive_group()
        group.add_argument(
            "--force",
            action="store_true",
            help=_(
                "Attempt forced removal of volume(s), regardless of state "
                "(defaults to False)"
            ),
        )
        group.add_argument(
            "--cascade",
            action="store_true",
            help=_(
                "Remove any snapshots along with volume(s) (defaults to False)"
            ),
        )
        group.add_argument(
            # now called "cascade", accept old arg for compatibility
            "--purge",
            action="store_true",
            help=argparse.SUPPRESS,
            dest='cascade',
        )
        parser.add_argument(
            '--remote',
            action='store_true',
            help=_("Specify this parameter to unmanage a volume."),
        )
        return parser

    def take_action(self, parsed_args: argparse.Namespace) -> None:
        volume_client = sdk_utils.ensure_service_version(
            self.app.client_manager.volume, '3'
        )
        result = 0

        if parsed_args.remote and (parsed_args.force or parsed_args.cascade):
            msg = _(
                "The --force and --cascade options are not "
                "supported with the --remote parameter."
            )
            raise exceptions.CommandError(msg)

        for volume in parsed_args.volumes:
            try:
                volume_obj = volume_client.find_volume(
                    volume, ignore_missing=False
                )
                if parsed_args.remote:
                    volume_client.unmanage_volume(volume_obj.id)
                else:
                    volume_client.delete_volume(
                        volume_obj.id,
                        force=parsed_args.force,
                        cascade=parsed_args.cascade,
                    )
            except Exception as e:
                result += 1
                LOG.error(
                    _(
                        "Failed to delete volume with "
                        "name or ID '%(volume)s': %(e)s"
                    ),
                    {'volume': volume, 'e': e},
                )

        if result > 0:
            total = len(parsed_args.volumes)
            msg = _("%(result)s of %(total)s volumes failed to delete.") % {
                'result': result,
                'total': total,
            }
            raise exceptions.CommandError(msg)


class ListVolume(command.Lister):
    _description = _("List volumes")

    def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
        parser = super().get_parser(prog_name)
        parser.add_argument(
            '--project',
            metavar='<project>',
            help=_('Filter results by project (name or ID) (admin only)'),
        )
        identity_common.add_project_domain_option_to_parser(parser)
        parser.add_argument(
            '--user',
            metavar='<user>',
            help=_('Filter results by user (name or ID) (admin only)'),
        )
        identity_common.add_user_domain_option_to_parser(parser)
        parser.add_argument(
            '--name',
            metavar='<name>',
            help=_('Filter results by volume name'),
        )
        parser.add_argument(
            '--status',
            metavar='<status>',
            help=_('Filter results by status'),
        )
        parser.add_argument(
            '--property',
            metavar='<key=value>',
            action=parseractions.KeyValueAction,
            dest='properties',
            help=_(
                'Filter by a property on the volume list '
                '(repeat option to filter by multiple properties) '
            ),
        )
        parser.add_argument(
            '--all-projects',
            action='store_true',
            default=False,
            help=_('Include all projects (admin only)'),
        )
        parser.add_argument(
            '--long',
            action='store_true',
            default=False,
            help=_('List additional fields in output'),
        )
        pagination.add_marker_pagination_option_to_parser(
            parser, include_max_items=False
        )
        return parser

    def take_action(
        self, parsed_args: argparse.Namespace
    ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]:
        volume_client = sdk_utils.ensure_service_version(
            self.app.client_manager.volume, '3'
        )
        identity_client = sdk_utils.ensure_service_version(
            self.app.client_manager.sdk_connection.identity, '3'
        )

        columns: tuple[str, ...] = ('id', 'name', 'status', 'size')
        column_headers: tuple[str, ...] = ('ID', 'Name', 'Status', 'Size')
        if parsed_args.long:
            columns += ('volume_type', 'is_bootable')
            column_headers += ('Type', 'Bootable')
        columns += ('attachments',)
        column_headers += ('Attached to',)
        if parsed_args.long:
            columns += ('metadata',)
            column_headers += ('Properties',)

        kwargs = {}

        if parsed_args.name:
            kwargs['name'] = parsed_args.name

        if parsed_args.status:
            kwargs['status'] = parsed_args.status

        if parsed_args.limit:
            kwargs['limit'] = parsed_args.limit

        if parsed_args.marker:
            kwargs['marker'] = parsed_args.marker

        if parsed_args.project:
            project_id = identity_common.find_project_id_sdk(
                identity_client,
                parsed_args.project,
                parsed_args.project_domain,
            )
            kwargs['project_id'] = project_id

        user_id = None
        if parsed_args.user:
            user_id = identity_common.find_user_id_sdk(
                identity_client, parsed_args.user, parsed_args.user_domain
            )
            kwargs['user_id'] = user_id

        if parsed_args.properties:
            # yes, cinder really does expect a stringified dict...
            kwargs['properties'] = str(parsed_args.properties)

        # set value of 'all_tenants' when using project option
        all_projects = bool(parsed_args.project) or parsed_args.all_projects
        kwargs['all_projects'] = all_projects

        data = list(volume_client.volumes(**kwargs))

        do_server_list = False

        for vol in data:
            if vol.status == 'in-use':
                do_server_list = True
                break

        # Cache the server list
        server_cache = {}
        if do_server_list:
            try:
                compute_client = self.app.client_manager.compute
                for s in compute_client.servers():
                    server_cache[s.id] = s
            except sdk_exceptions.SDKException:
                # Just forget it if there's any trouble
                pass

        AttachmentsColumnWithCache = functools.partial(
            AttachmentsColumn, server_cache=server_cache
        )

        return (
            column_headers,
            (
                utils.get_item_properties(
                    s,
                    columns,
                    formatters={
                        'metadata': format_columns.DictColumn,
                        'attachments': AttachmentsColumnWithCache,
                    },
                )
                for s in data
            ),
        )


class MigrateVolume(command.Command):
    _description = _("Migrate volume to a new host")

    def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
        parser = super().get_parser(prog_name)
        parser.add_argument(
            'volume',
            metavar="<volume>",
            help=_("Volume to migrate (name or ID)"),
        )
        destination_group = parser.add_mutually_exclusive_group(required=True)
        destination_group.add_argument(
            '--host',
            metavar="<host>",
            help=_(
                "Destination host (takes the form: host@backend-name#pool)"
            ),
        )
        destination_group.add_argument(
            '--cluster',
            metavar="<cluster>",
            help=_(
                "Destination cluster to migrate the volume to "
                "(requires --os-volume-api-version 3.16 or higher)"
            ),
        )
        parser.add_argument(
            '--force-host-copy',
            action="store_true",
            help=_(
                "Enable generic host-based force-migration, "
                "which bypasses driver optimizations"
            ),
        )
        parser.add_argument(
            '--lock-volume',
            action="store_true",
            help=_(
                "If specified, the volume state will be locked "
                "and will not allow a migration to be aborted "
                "(possibly by another operation)"
            ),
        )
        return parser

    def take_action(self, parsed_args: argparse.Namespace) -> None:
        volume_client = sdk_utils.ensure_service_version(
            self.app.client_manager.volume, '3'
        )
        volume = volume_client.find_volume(
            parsed_args.volume, ignore_missing=False
        )

        if parsed_args.cluster and not sdk_utils.supports_microversion(
            volume_client, '3.16'
        ):
            msg = _(
                "--os-volume-api-version 3.16 or greater is required to "
                "support the volume migration with cluster"
            )
            raise exceptions.CommandError(msg)

        volume_client.migrate_volume(
            volume.id,
            host=parsed_args.host,
            force_host_copy=parsed_args.force_host_copy,
            lock_volume=parsed_args.lock_volume,
            cluster=parsed_args.cluster,
        )


class SetVolume(command.Command):
    _description = _("Set volume properties")

    def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
        parser = super().get_parser(prog_name)
        parser.add_argument(
            'volume',
            metavar='<volume>',
            help=_('Volume to modify (name or ID)'),
        )
        parser.add_argument(
            '--name',
            metavar='<name>',
            help=_('New volume name'),
        )
        parser.add_argument(
            '--size',
            metavar='<size>',
            type=int,
            help=_('Extend volume size in GB'),
        )
        parser.add_argument(
            '--description',
            metavar='<description>',
            help=_('New volume description'),
        )
        parser.add_argument(
            "--no-property",
            dest="no_property",
            action="store_true",
            help=_(
                "Remove all properties from <volume> "
                "(specify both --no-property and --property to "
                "remove the current properties before setting "
                "new properties.)"
            ),
        )
        parser.add_argument(
            '--property',
            metavar='<key=value>',
            action=parseractions.KeyValueAction,
            dest='properties',
            help=_(
                'Set a property on this volume '
                '(repeat option to set multiple properties)'
            ),
        )
        parser.add_argument(
            '--image-property',
            metavar='<key=value>',
            action=parseractions.KeyValueAction,
            dest='image_properties',
            help=_(
                'Set an image property on this volume '
                '(repeat option to set multiple image properties)'
            ),
        )
        parser.add_argument(
            "--state",
            metavar="<state>",
            choices=[
                'available',
                'error',
                'creating',
                'deleting',
                'in-use',
                'attaching',
                'detaching',
                'error_deleting',
                'maintenance',
            ],
            help=_(
                'New volume state ("available", "error", "creating", '
                '"deleting", "in-use", "attaching", "detaching", '
                '"error_deleting" or "maintenance") (admin only) '
                '(This option simply changes the state of the volume '
                'in the database with no regard to actual status, '
                'exercise caution when using)'
            ),
        )
        attached_group = parser.add_mutually_exclusive_group()
        attached_group.add_argument(
            "--attached",
            action="store_true",
            help=_(
                'Set volume attachment status to "attached" '
                '(admin only) '
                '(This option simply changes the state of the volume '
                'in the database with no regard to actual status, '
                'exercise caution when using)'
            ),
        )
        attached_group.add_argument(
            "--detached",
            action="store_true",
            help=_(
                'Set volume attachment status to "detached" '
                '(admin only) '
                '(This option simply changes the state of the volume '
                'in the database with no regard to actual status, '
                'exercise caution when using)'
            ),
        )
        parser.add_argument(
            '--type',
            metavar='<volume-type>',
            help=_('New volume type (name or ID)'),
        )
        parser.add_argument(
            '--retype-policy',
            metavar='<retype-policy>',
            choices=['never', 'on-demand'],
            help=argparse.SUPPRESS,
        )
        parser.add_argument(
            '--migration-policy',
            metavar='<migration-policy>',
            choices=['never', 'on-demand'],
            help=_(
                'Migration policy while re-typing volume '
                '("never" or "on-demand", default is "never" ) '
                '(available only when --type option is specified)'
            ),
        )
        bootable_group = parser.add_mutually_exclusive_group()
        bootable_group.add_argument(
            "--bootable",
            action="store_true",
            dest="bootable",
            default=None,
            help=_("Mark volume as bootable"),
        )
        bootable_group.add_argument(
            "--non-bootable",
            action="store_false",
            dest="bootable",
            default=None,
            help=_("Mark volume as non-bootable"),
        )
        readonly_group = parser.add_mutually_exclusive_group()
        readonly_group.add_argument(
            "--read-only",
            action="store_true",
            dest="read_only",
            default=None,
            help=_("Set volume to read-only access mode"),
        )
        readonly_group.add_argument(
            "--read-write",
            action="store_false",
            dest="read_only",
            default=None,
            help=_("Set volume to read-write access mode"),
        )
        return parser

    def take_action(self, parsed_args: argparse.Namespace) -> None:
        volume_client = sdk_utils.ensure_service_version(
            self.app.client_manager.volume, '3'
        )
        volume = volume_client.find_volume(
            parsed_args.volume, ignore_missing=False
        )

        result = 0
        if parsed_args.retype_policy:
            msg = _(
                "The '--retype-policy' option has been deprecated in favor "
                "of '--migration-policy' option. The '--retype-policy' option "
                "will be removed in a future release. Please use "
                "'--migration-policy' instead."
            )
            self.log.warning(msg)

        if parsed_args.size:
            try:
                if parsed_args.size <= volume.size:
                    msg = (
                        _("New size must be greater than %s GB") % volume.size
                    )
                    raise exceptions.CommandError(msg)
                if volume.status not in ('available', 'in-use'):
                    msg = (
                        _(
                            "Volume is in %s state, it must be available "
                            "or in-use before size can be extended."
                        )
                        % volume.status
                    )
                    raise exceptions.CommandError(msg)
                if (
                    volume.status == 'in-use'
                    and not sdk_utils.supports_microversion(
                        volume_client, '3.42'
                    )
                ):
                    msg = _(
                        "--os-volume-api-version 3.42 or greater is "
                        "required to extend in-use volumes."
                    )
                    raise exceptions.CommandError(msg)
                volume_client.extend_volume(volume, parsed_args.size)
            except Exception as e:
                LOG.error(_("Failed to set volume size: %s"), e)
                result += 1

        if parsed_args.no_property:
            try:
                volume_client.delete_volume_metadata(volume)
            except Exception as e:
                LOG.error(_("Failed to clean volume properties: %s"), e)
                result += 1

        if parsed_args.properties:
            try:
                volume_client.set_volume_metadata(
                    volume, **parsed_args.properties
                )
            except Exception as e:
                LOG.error(_("Failed to set volume properties: %s"), e)
                result += 1

        if parsed_args.image_properties:
            try:
                volume_client.set_volume_image_metadata(
                    volume, **parsed_args.image_properties
                )
            except Exception as e:
                LOG.error(_("Failed to set image properties: %s"), e)
                result += 1

        if parsed_args.state:
            try:
                volume_client.reset_volume_status(
                    volume, status=parsed_args.state
                )
            except Exception as e:
                LOG.error(_("Failed to set volume state: %s"), e)
                result += 1

        if parsed_args.attached:
            try:
                volume_client.reset_volume_status(
                    volume, attach_status="attached"
                )
            except Exception as e:
                LOG.error(_("Failed to set volume attach-status: %s"), e)
                result += 1

        if parsed_args.detached:
            try:
                volume_client.reset_volume_status(
                    volume, attach_status="detached"
                )
            except Exception as e:
                LOG.error(_("Failed to set volume attach-status: %s"), e)
                result += 1

        if parsed_args.bootable is not None:
            try:
                volume_client.set_volume_bootable_status(
                    volume, parsed_args.bootable
                )
            except Exception as e:
                LOG.error(_("Failed to set volume bootable property: %s"), e)
                result += 1

        if parsed_args.read_only is not None:
            try:
                volume_client.set_volume_readonly(
                    volume, parsed_args.read_only
                )
            except Exception as e:
                LOG.error(
                    _("Failed to set volume read-only access mode flag: %s"),
                    e,
                )
                result += 1

        policy = parsed_args.migration_policy or parsed_args.retype_policy
        if parsed_args.type:
            # get the migration policy
            migration_policy = 'never'
            if policy:
                migration_policy = policy
            try:
                # find the volume type
                volume_type = volume_client.find_type(
                    parsed_args.type, ignore_missing=False
                )
                # reset to the new volume type
                volume_client.retype_volume(
                    volume, volume_type, migration_policy
                )
            except Exception as e:
                LOG.error(_("Failed to set volume type: %s"), e)
                result += 1
        elif policy:
            # If the "--migration-policy" is specified without "--type"
            LOG.warning(
                _("'%s' option will not work without '--type' option"),
                (
                    '--migration-policy'
                    if parsed_args.migration_policy
                    else '--retype-policy'
                ),
            )

        kwargs = {}
        if parsed_args.name:
            kwargs['name'] = parsed_args.name
        if parsed_args.description:
            kwargs['description'] = parsed_args.description
        if kwargs:
            try:
                volume_client.update_volume(volume, **kwargs)
            except Exception as e:
                LOG.error(
                    _(
                        "Failed to update volume display name "
                        "or display description: %s"
                    ),
                    e,
                )
                result += 1

        if result > 0:
            raise exceptions.CommandError(
                _("One or more of the set operations failed")
            )


class ShowVolume(command.ShowOne):
    _description = _("Display volume details")

    def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
        parser = super().get_parser(prog_name)
        parser.add_argument(
            'volume',
            metavar="<volume>",
            help=_("Volume to display (name or ID)"),
        )
        return parser

    def take_action(
        self, parsed_args: argparse.Namespace
    ) -> tuple[Sequence[str], Iterable[Any]]:
        volume_client = sdk_utils.ensure_service_version(
            self.app.client_manager.volume, '3'
        )
        volume = volume_client.find_volume(
            parsed_args.volume, ignore_missing=False
        )

        data = _format_volume(volume)
        col_headers, col_data = zip(*sorted(data.items()))
        return col_headers, col_data


class UnsetVolume(command.Command):
    _description = _("Unset volume properties")

    def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
        parser = super().get_parser(prog_name)
        parser.add_argument(
            'volume',
            metavar='<volume>',
            help=_('Volume to modify (name or ID)'),
        )
        parser.add_argument(
            '--property',
            metavar='<key>',
            action='append',
            dest='properties',
            help=_(
                'Remove a property from volume '
                '(repeat option to remove multiple properties)'
            ),
        )
        parser.add_argument(
            '--image-property',
            metavar='<key>',
            action='append',
            dest='image_properties',
            help=_(
                'Remove an image property from volume '
                '(repeat option to remove multiple image properties)'
            ),
        )
        return parser

    def take_action(self, parsed_args: argparse.Namespace) -> None:
        volume_client = sdk_utils.ensure_service_version(
            self.app.client_manager.volume, '3'
        )
        volume = volume_client.find_volume(
            parsed_args.volume, ignore_missing=False
        )

        result = 0
        if parsed_args.properties:
            try:
                volume_client.delete_volume_metadata(
                    volume.id, keys=parsed_args.properties
                )
            except Exception as e:
                LOG.error(_("Failed to unset volume properties: %s"), e)
                result += 1

        if parsed_args.image_properties:
            try:
                volume_client.delete_volume_image_metadata(
                    volume.id, keys=parsed_args.image_properties
                )
            except Exception as e:
                LOG.error(_("Failed to unset image properties: %s"), e)
                result += 1

        if result > 0:
            raise exceptions.CommandError(
                _("One or more of the unset operations failed")
            )


class VolumeSummary(command.ShowOne):
    _description = _("Show a summary of all volumes in this deployment.")

    def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
        parser = super().get_parser(prog_name)
        parser.add_argument(
            '--all-projects',
            action='store_true',
            default=False,
            help=_('Include all projects (admin only)'),
        )
        return parser

    def take_action(
        self, parsed_args: argparse.Namespace
    ) -> tuple[Sequence[str], Iterable[Any]]:
        volume_client = sdk_utils.ensure_service_version(
            self.app.client_manager.volume, '3'
        )

        if not sdk_utils.supports_microversion(volume_client, '3.12'):
            msg = _(
                "--os-volume-api-version 3.12 or greater is required to "
                "support the 'volume summary' command"
            )
            raise exceptions.CommandError(msg)

        columns = [
            'total_count',
            'total_size',
        ]
        column_headers = [
            'Total Count',
            'Total Size',
        ]
        if sdk_utils.supports_microversion(volume_client, '3.36'):
            columns.append('metadata')
            column_headers.append('Metadata')

        # set value of 'all_tenants' when using project option
        all_projects = parsed_args.all_projects

        vol_summary = volume_client.summary(all_projects)

        return (
            column_headers,
            utils.get_item_properties(
                vol_summary,
                columns,
                formatters={'metadata': format_columns.DictColumn},
            ),
        )


class VolumeRevertToSnapshot(command.Command):
    _description = _("Revert a volume to a snapshot.")

    def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
        parser = super().get_parser(prog_name)
        parser.add_argument(
            'snapshot',
            metavar="<snapshot>",
            help=_(
                'Name or ID of the snapshot to restore. The snapshot must '
                'be the most recent one known to cinder.'
            ),
        )
        return parser

    def take_action(self, parsed_args: argparse.Namespace) -> None:
        volume_client = sdk_utils.ensure_service_version(
            self.app.client_manager.volume, '3'
        )

        if not sdk_utils.supports_microversion(volume_client, '3.40'):
            msg = _(
                "--os-volume-api-version 3.40 or greater is required to "
                "support the 'volume revert snapshot' command"
            )
            raise exceptions.CommandError(msg)

        snapshot = volume_client.find_snapshot(
            parsed_args.snapshot,
            ignore_missing=False,
        )
        volume = volume_client.find_volume(
            snapshot.volume_id,
            ignore_missing=False,
        )

        volume_client.revert_volume_to_snapshot(volume, snapshot)
