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

from osc_lib.cli import format_columns
from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils as osc_utils

from manilaclient import api_versions
from manilaclient.common._i18n import _
from manilaclient.common import cliutils
from manilaclient.osc import utils

LOG = logging.getLogger(__name__)


class CreateShareReplica(command.ShowOne):
    """Create a share replica."""

    _description = _("Create a replica of the given share")

    def get_parser(self, prog_name):
        parser = super().get_parser(prog_name)
        parser.add_argument(
            "share",
            metavar="<share>",
            help=_("Name or ID of the share to replicate."),
        )
        parser.add_argument(
            '--availability-zone',
            metavar='<availability-zone>',
            default=None,
            help=_(
                'Availability zone in which the replica should be created.'
            ),
        )
        parser.add_argument(
            '--wait',
            action='store_true',
            default=False,
            help=_('Wait for replica creation'),
        )
        parser.add_argument(
            "--scheduler-hint",
            metavar="<key=value>",
            default={},
            action=parseractions.KeyValueAction,
            help=_(
                "Scheduler hint for the share replica as key=value pairs, "
                "Supported key is only_host. Available for microversion "
                ">= 2.67."
            ),
        )
        parser.add_argument(
            '--share-network',
            metavar='<share-network-name-or-id>',
            default=None,
            help=_(
                'Optional network info ID or name. Available for '
                'microversion >= 2.72'
            ),
        )
        parser.add_argument(
            "--property",
            metavar="<key=value>",
            default={},
            action=parseractions.KeyValueAction,
            help=_(
                "Set a property to this replica "
                "(repeat option to set multiple properties). "
                "Available only for microversion >= 2.95"
            ),
        )
        return parser

    def take_action(self, parsed_args):
        share_client = self.app.client_manager.share

        share = osc_utils.find_resource(share_client.shares, parsed_args.share)
        scheduler_hints = {}
        if parsed_args.scheduler_hint:
            if share_client.api_version < api_versions.APIVersion("2.67"):
                raise exceptions.CommandError(
                    _(
                        "arg '--scheduler_hint' is available only starting "
                        "with API microversion '2.67'."
                    )
                )

            hints = utils.extract_key_value_options(parsed_args.scheduler_hint)
            if 'only_host' not in hints.keys() or len(hints) > 1:
                raise exceptions.CommandError(
                    "The only valid key supported with the --scheduler-hint "
                    "argument is 'only_host'."
                )
            scheduler_hints['only_host'] = hints.get('only_host')

        body = {
            'share': share,
            'availability_zone': parsed_args.availability_zone,
        }

        metadata = {}
        if parsed_args.property:
            if share_client.api_version < api_versions.APIVersion("2.95"):
                raise exceptions.CommandError(
                    _(
                        "arg '--property' is available only starting "
                        "with API microversion '2.95'."
                    )
                )
            metadata = parsed_args.property

        if metadata:
            body['metadata'] = metadata

        if scheduler_hints:
            body['scheduler_hints'] = scheduler_hints

        share_network_id = None
        if parsed_args.share_network:
            if share_client.api_version < api_versions.APIVersion("2.72"):
                raise exceptions.CommandError(
                    "'share-network' option is available only starting "
                    "with '2.72' API microversion."
                )
            share_network_id = osc_utils.find_resource(
                share_client.share_networks, parsed_args.share_network
            ).id
            body['share_network'] = share_network_id

        share_replica = share_client.share_replicas.create(**body)
        if parsed_args.wait:
            if not osc_utils.wait_for_status(
                status_f=share_client.share_replicas.get,
                res_id=share_replica.id,
                success_status=['available'],
            ):
                LOG.error(_("ERROR: Share replica is in error state."))

            share_replica = osc_utils.find_resource(
                share_client.share_replicas, share_replica.id
            )

        return self.dict2columns(share_replica._info)


class DeleteShareReplica(command.Command):
    """Delete one or more share replicas."""

    _description = _("Delete one or more share replicas")

    def get_parser(self, prog_name):
        parser = super().get_parser(prog_name)
        parser.add_argument(
            "replica",
            metavar="<replica>",
            nargs="+",
            help=_("Name or ID of the replica(s) to delete"),
        )
        parser.add_argument(
            "--force",
            action='store_true',
            default=False,
            help=_(
                "Attempt to force delete a replica on its backend. "
                "Using this option will purge the replica from Manila "
                "even if it is not cleaned up on the backend. "
            ),
        )
        parser.add_argument(
            "--wait",
            action='store_true',
            default=False,
            help=_("Wait for share replica deletion"),
        )
        return parser

    def take_action(self, parsed_args):
        share_client = self.app.client_manager.share
        result = 0

        for replica in parsed_args.replica:
            try:
                replica_obj = osc_utils.find_resource(
                    share_client.share_replicas, replica
                )

                share_client.share_replicas.delete(
                    replica_obj, force=parsed_args.force
                )

                if parsed_args.wait:
                    if not osc_utils.wait_for_delete(
                        manager=share_client.share_replicas,
                        res_id=replica_obj.id,
                    ):
                        result += 1

            except Exception as e:
                result += 1
                LOG.error(
                    _(
                        "Failed to delete a share replica with "
                        "name or ID '%(replica)s': %(e)s"
                    ),
                    {'replica': replica, 'e': e},
                )

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


class ListShareReplica(command.Lister):
    """List share replicas."""

    _description = _("List share replicas")

    def get_parser(self, prog_name):
        parser = super().get_parser(prog_name)
        parser.add_argument(
            "--share",
            metavar="<share>",
            default=None,
            help=_("Name or ID of the share to list replicas for."),
        )
        parser.add_argument(
            '--property',
            metavar='<key=value>',
            action=parseractions.KeyValueAction,
            help=_(
                'Filter replicas having a given metadata key=value '
                'property. (repeat option to filter by multiple '
                'properties)'
            ),
        )
        return parser

    def take_action(self, parsed_args):
        share_client = self.app.client_manager.share

        share = None
        if parsed_args.share:
            share = osc_utils.find_resource(
                share_client.shares, parsed_args.share
            )

        properties = parsed_args.property or {}
        if properties:
            if share_client.api_version < api_versions.APIVersion("2.95"):
                raise exceptions.CommandError(
                    "Property based filtering is only available "
                    "with manila API version >= 2.95"
                )

        search_opts = {}
        if properties:
            meta_str = ",".join(f"{k}:{v}" for k, v in properties.items())
            search_opts['metadata'] = meta_str

        replicas = share_client.share_replicas.list(
            share=share, search_opts=search_opts
        )

        if properties:
            replicas = [
                r
                for r in replicas
                if all(
                    r._info.get('metadata', {}).get(k) == v
                    for k, v in properties.items()
                )
            ]

        columns = [
            'id',
            'status',
            'replica_state',
            'share_id',
            'host',
            'availability_zone',
            'updated_at',
        ]

        if share_client.api_version >= api_versions.APIVersion("2.95"):
            for replica in replicas:
                replica._info.update(
                    {
                        'properties': format_columns.DictColumn(
                            replica._info.pop('metadata', {})
                        ),
                    },
                )

            columns += ['properties']

        column_headers = utils.format_column_headers(columns)
        data = (
            osc_utils.get_dict_properties(replica._info, columns)
            for replica in replicas
        )

        return (column_headers, data)


class ShowShareReplica(command.ShowOne):
    """Show share replica."""

    _description = _("Show details about a replica")

    def get_parser(self, prog_name):
        parser = super().get_parser(prog_name)
        parser.add_argument(
            "replica",
            metavar="<replica>",
            help=_(
                "ID of the share replica. Available only for "
                "microversion >= 2.47. "
            ),
        )
        return parser

    def take_action(self, parsed_args):
        share_client = self.app.client_manager.share

        replica = share_client.share_replicas.get(parsed_args.replica)

        replica_export_locations = (
            share_client.share_replica_export_locations.list(
                share_replica=replica
            )
        )

        replica._info['export_locations'] = []
        for element_location in replica_export_locations:
            element_location._info.pop('links', None)
            replica._info['export_locations'].append(element_location._info)

        if parsed_args.formatter == 'table':
            replica._info['export_locations'] = (
                cliutils.convert_dict_list_to_string(
                    replica._info['export_locations']
                )
            )
        # Special mapping for columns to make the output easier to read:
        # 'metadata' --> 'properties'
        metadata = replica._info.pop('metadata', {})
        replica._info.update(
            {
                'properties': (
                    format_columns.DictColumn(metadata)
                    if parsed_args.formatter == 'table'
                    else metadata
                ),
            },
        )

        replica._info.pop('links', None)

        return self.dict2columns(replica._info)


class SetShareReplica(command.Command):
    """Set share replica"""

    _description = _(
        "Explicitly set share replica status and/or replica-state "
        "and/or property"
    )

    def get_parser(self, prog_name):
        parser = super().get_parser(prog_name)
        parser.add_argument(
            "replica",
            metavar="<replica>",
            help=_("ID of the share replica to modify."),
        )
        parser.add_argument(
            "--replica-state",
            metavar="<replica-state>",
            choices=['in_sync', 'out_of_sync', 'active', 'error'],
            help=_(
                "Indicate which replica_state to assign the replica. "
                "Options include in_sync, out_of_sync, active and error."
            ),
        )
        parser.add_argument(
            "--status",
            metavar="<status>",
            choices=[
                'available',
                'error',
                'creating',
                'deleting',
                'error_deleting',
            ],
            help=_(
                "Indicate which status to assign the replica. Options "
                "include available, error, creating, deleting and "
                "error_deleting."
            ),
        )
        parser.add_argument(
            "--property",
            metavar="<key=value>",
            default={},
            action=parseractions.KeyValueAction,
            help=_(
                "Set a property to this replica "
                "(repeat option to set multiple properties). "
                "Available only for microversion >= 2.95"
            ),
        )
        return parser

    def take_action(self, parsed_args):
        share_client = self.app.client_manager.share
        result = 0

        replica = osc_utils.find_resource(
            share_client.share_replicas, parsed_args.replica
        )

        if parsed_args.replica_state:
            try:
                share_client.share_replicas.reset_replica_state(
                    replica, parsed_args.replica_state
                )
            except Exception as e:
                result += 1
                LOG.error(
                    _(
                        "Failed to set replica_state "
                        "'%(replica_state)s': %(exception)s"
                    ),
                    {
                        'replica_state': parsed_args.replica_state,
                        'exception': e,
                    },
                )

        if parsed_args.status:
            try:
                share_client.share_replicas.reset_state(
                    replica, parsed_args.status
                )
            except Exception as e:
                result += 1
                LOG.error(
                    _("Failed to set status '%(status)s': %(exception)s"),
                    {'status': parsed_args.status, 'exception': e},
                )

        if parsed_args.property:
            if share_client.api_version < api_versions.APIVersion("2.95"):
                raise exceptions.CommandError(
                    _(
                        "Setting properties in share replicas is available "
                        "only starting with API microversion '2.95'."
                    )
                )
            try:
                replica.set_metadata(parsed_args.property)
            except Exception as e:
                LOG.error(
                    _(
                        "Failed to set share replica properties "
                        "'%(properties)s': %(exception)s"
                    ),
                    {'properties': parsed_args.property, 'exception': e},
                )
                result += 1

        if (
            not parsed_args.replica_state
            and not parsed_args.status
            and not parsed_args.property
        ):
            raise exceptions.CommandError(
                _(
                    "Nothing to set. Please define "
                    "either '--replica_state' or '--status' or '--property'."
                )
            )
        if result > 0:
            raise exceptions.CommandError(
                _("One or more of the set operations failed")
            )


class UnsetShareReplica(command.Command):
    """Unset a share replica property."""

    _description = _("Unset a share replica property")

    def get_parser(self, prog_name):
        parser = super().get_parser(prog_name)
        parser.add_argument(
            "replica",
            metavar="<replica>",
            help=_("ID of the share replica to unset property from"),
        )
        parser.add_argument(
            '--property',
            metavar='<key>',
            action='append',
            help=_(
                'Remove a property from replica '
                '(repeat option to remove multiple properties)'
            ),
        )
        return parser

    def take_action(self, parsed_args):
        share_client = self.app.client_manager.share

        replica = osc_utils.find_resource(
            share_client.share_replicas, parsed_args.replica
        )

        if parsed_args.property:
            if share_client.api_version < api_versions.APIVersion("2.95"):
                raise exceptions.CommandError(
                    _(
                        "arg '--property' is available only starting "
                        "with API microversion '2.95'."
                    )
                )
            result = 0
            for key in parsed_args.property:
                try:
                    replica.delete_metadata([key])
                except Exception as e:
                    result += 1
                    LOG.error(
                        _(
                            "Failed to unset replica property "
                            "'%(key)s': %(exception)s"
                        ),
                        {'key': key, 'exception': e},
                    )
            if result > 0:
                raise exceptions.CommandError(
                    _("One or more of the unset operations failed")
                )
        else:
            raise exceptions.CommandError(
                "Please specify '--property <key>' to unset a property."
            )


class PromoteShareReplica(command.Command):
    """Promote share replica"""

    _description = _("Promote specified replica to 'active' replica_state.")

    def get_parser(self, prog_name):
        parser = super().get_parser(prog_name)
        parser.add_argument(
            "replica", metavar="<replica>", help=_("ID of the share replica.")
        )
        parser.add_argument(
            '--quiesce-wait-time',
            metavar='<quiesce-wait-time>',
            default=None,
            help=_(
                'Quiesce wait time in seconds. Available for '
                'microversion >= 2.75'
            ),
        )
        parser.add_argument(
            '--wait',
            action='store_true',
            default=False,
            help=_('Wait for share replica promotion'),
        )
        return parser

    def take_action(self, parsed_args):
        share_client = self.app.client_manager.share

        replica = osc_utils.find_resource(
            share_client.share_replicas, parsed_args.replica
        )

        args = [
            replica,
        ]
        if parsed_args.quiesce_wait_time:
            if share_client.api_version < api_versions.APIVersion("2.75"):
                raise exceptions.CommandError(
                    "'quiesce-wait-time' option is available only starting "
                    "with '2.75' API microversion."
                )
            args += [parsed_args.quiesce_wait_time]

        try:
            share_client.share_replicas.promote(*args)
            if parsed_args.wait:
                if not osc_utils.wait_for_status(
                    status_f=share_client.share_replicas.get,
                    res_id=replica.id,
                    success_status=['active'],
                    status_field='replica_state',
                ):
                    LOG.error(_("ERROR: Share replica is in error state."))

        except Exception as e:
            msg = "Failed to promote replica to 'active': %(e)s"
            raise exceptions.CommandError(msg % {'e': e})


class ResyncShareReplica(command.Command):
    """Resync share replica"""

    _description = _(
        "Attempt to update the share replica with its 'active' mirror."
    )

    def get_parser(self, prog_name):
        parser = super().get_parser(prog_name)
        parser.add_argument(
            "replica",
            metavar="<replica>",
            help=_("ID of the share replica to resync."),
        )
        return parser

    def take_action(self, parsed_args):
        share_client = self.app.client_manager.share

        replica = osc_utils.find_resource(
            share_client.share_replicas, parsed_args.replica
        )

        try:
            share_client.share_replicas.resync(replica)
        except Exception as e:
            msg = "Failed to resync share replica: %(e)s"
            raise exceptions.CommandError(msg % {'e': e})
