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

"""Router action implementations"""

import copy
import json
import logging

from cliff import columns as cliff_columns
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
from osc_lib.utils import tags as _tag

from openstackclient.i18n import _
from openstackclient.identity import common as identity_common
from openstackclient.network import common

LOG = logging.getLogger(__name__)


class AdminStateColumn(cliff_columns.FormattableColumn):
    def human_readable(self):
        return 'UP' if self._value else 'DOWN'


class RouterInfoColumn(cliff_columns.FormattableColumn):
    def human_readable(self):
        try:
            return json.dumps(self._value)
        except (TypeError, KeyError):
            return ''


class RoutesColumn(cliff_columns.FormattableColumn):
    def human_readable(self):
        # Map the route keys to match --route option.
        for route in self._value or []:
            if 'nexthop' in route:
                route['gateway'] = route.pop('nexthop')
        return utils.format_list_of_dicts(self._value)


_formatters = {
    'admin_state_up': AdminStateColumn,
    'is_admin_state_up': AdminStateColumn,
    'external_gateway_info': RouterInfoColumn,
    'availability_zones': format_columns.ListColumn,
    'availability_zone_hints': format_columns.ListColumn,
    'routes': RoutesColumn,
    'tags': format_columns.ListColumn,
}


def _get_columns(item):
    column_map = {
        'is_ha': 'ha',
        'is_distributed': 'distributed',
        'is_admin_state_up': 'admin_state_up',
    }
    if hasattr(item, 'interfaces_info'):
        column_map['interfaces_info'] = 'interfaces_info'
    invisible_columns = ['location']
    if item.is_ha is None:
        invisible_columns.append('is_ha')
        column_map.pop('is_ha')
    if item.is_distributed is None:
        invisible_columns.append('is_distributed')
        column_map.pop('is_distributed')
    return utils.get_osc_show_columns_for_sdk_resource(
        item, column_map, invisible_columns
    )


def _get_attrs(client_manager, parsed_args):
    attrs = {}
    if parsed_args.name is not None:
        attrs['name'] = parsed_args.name
    if parsed_args.enable:
        attrs['admin_state_up'] = True
    if parsed_args.disable:
        attrs['admin_state_up'] = False
    if parsed_args.centralized:
        attrs['distributed'] = False
    if parsed_args.distributed:
        attrs['distributed'] = True
    if (
        'availability_zone_hints' in parsed_args
        and parsed_args.availability_zone_hints is not None
    ):
        attrs['availability_zone_hints'] = parsed_args.availability_zone_hints
    if parsed_args.description is not None:
        attrs['description'] = parsed_args.description
    # "router set" command doesn't support setting project.
    if 'project' in parsed_args and parsed_args.project is not None:
        identity_client = client_manager.identity
        project_id = identity_common.find_project(
            identity_client,
            parsed_args.project,
            parsed_args.project_domain,
        ).id
        attrs['project_id'] = project_id
    if parsed_args.external_gateway:
        gateway_info = {}
        n_client = client_manager.network
        network = n_client.find_network(
            parsed_args.external_gateway, ignore_missing=False
        )
        gateway_info['network_id'] = network.id
        if parsed_args.disable_snat:
            gateway_info['enable_snat'] = False
        if parsed_args.enable_snat:
            gateway_info['enable_snat'] = True
        if parsed_args.fixed_ip:
            ips = []
            for ip_spec in parsed_args.fixed_ip:
                if ip_spec.get('subnet', False):
                    subnet_name_id = ip_spec.pop('subnet')
                    if subnet_name_id:
                        subnet = n_client.find_subnet(
                            subnet_name_id, ignore_missing=False
                        )
                        ip_spec['subnet_id'] = subnet.id
                if ip_spec.get('ip-address', False):
                    ip_spec['ip_address'] = ip_spec.pop('ip-address')
                ips.append(ip_spec)
            gateway_info['external_fixed_ips'] = ips
        attrs['external_gateway_info'] = gateway_info
    # "router set" command doesn't support setting flavor_id.
    if 'flavor_id' in parsed_args and parsed_args.flavor_id is not None:
        attrs['flavor_id'] = parsed_args.flavor_id

    return attrs


class AddPortToRouter(command.Command):
    _description = _("Add a port to a router")

    def get_parser(self, prog_name):
        parser = super(AddPortToRouter, self).get_parser(prog_name)
        parser.add_argument(
            'router',
            metavar='<router>',
            help=_("Router to which port will be added (name or ID)"),
        )
        parser.add_argument(
            'port', metavar='<port>', help=_("Port to be added (name or ID)")
        )
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.network
        port = client.find_port(parsed_args.port, ignore_missing=False)
        client.add_interface_to_router(
            client.find_router(parsed_args.router, ignore_missing=False),
            port_id=port.id,
        )


class AddSubnetToRouter(command.Command):
    _description = _("Add a subnet to a router")

    def get_parser(self, prog_name):
        parser = super(AddSubnetToRouter, self).get_parser(prog_name)
        parser.add_argument(
            'router',
            metavar='<router>',
            help=_("Router to which subnet will be added (name or ID)"),
        )
        parser.add_argument(
            'subnet',
            metavar='<subnet>',
            help=_("Subnet to be added (name or ID)"),
        )
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.network
        subnet = client.find_subnet(parsed_args.subnet, ignore_missing=False)
        client.add_interface_to_router(
            client.find_router(parsed_args.router, ignore_missing=False),
            subnet_id=subnet.id,
        )


class AddExtraRoutesToRouter(command.ShowOne):
    _description = _("Add extra static routes to a router's routing table.")

    def get_parser(self, prog_name):
        parser = super(AddExtraRoutesToRouter, self).get_parser(prog_name)
        parser.add_argument(
            'router',
            metavar='<router>',
            help=_(
                "Router to which extra static routes "
                "will be added (name or ID)."
            ),
        )
        parser.add_argument(
            '--route',
            metavar='destination=<subnet>,gateway=<ip-address>',
            action=parseractions.MultiKeyValueAction,
            dest='routes',
            default=[],
            required_keys=['destination', 'gateway'],
            help=_(
                "Add extra static route to the router. "
                "destination: destination subnet (in CIDR notation), "
                "gateway: nexthop IP address. "
                "Repeat option to add multiple routes. "
                "Trying to add a route that's already present "
                "(exactly, including destination and nexthop) "
                "in the routing table is allowed and is considered "
                "a successful operation."
            ),
        )
        return parser

    def take_action(self, parsed_args):
        if parsed_args.routes is not None:
            for route in parsed_args.routes:
                route['nexthop'] = route.pop('gateway')
        client = self.app.client_manager.network
        router_obj = client.add_extra_routes_to_router(
            client.find_router(parsed_args.router, ignore_missing=False),
            body={'router': {'routes': parsed_args.routes}},
        )
        display_columns, columns = _get_columns(router_obj)
        data = utils.get_item_properties(
            router_obj, columns, formatters=_formatters
        )
        return (display_columns, data)


class RemoveExtraRoutesFromRouter(command.ShowOne):
    _description = _(
        "Remove extra static routes from a router's routing table."
    )

    def get_parser(self, prog_name):
        parser = super(RemoveExtraRoutesFromRouter, self).get_parser(prog_name)
        parser.add_argument(
            'router',
            metavar='<router>',
            help=_(
                "Router from which extra static routes "
                "will be removed (name or ID)."
            ),
        )
        parser.add_argument(
            '--route',
            metavar='destination=<subnet>,gateway=<ip-address>',
            action=parseractions.MultiKeyValueAction,
            dest='routes',
            default=[],
            required_keys=['destination', 'gateway'],
            help=_(
                "Remove extra static route from the router. "
                "destination: destination subnet (in CIDR notation), "
                "gateway: nexthop IP address. "
                "Repeat option to remove multiple routes. "
                "Trying to remove a route that's already missing "
                "(fully, including destination and nexthop) "
                "from the routing table is allowed and is considered "
                "a successful operation."
            ),
        )
        return parser

    def take_action(self, parsed_args):
        if parsed_args.routes is not None:
            for route in parsed_args.routes:
                route['nexthop'] = route.pop('gateway')
        client = self.app.client_manager.network
        router_obj = client.remove_extra_routes_from_router(
            client.find_router(parsed_args.router, ignore_missing=False),
            body={'router': {'routes': parsed_args.routes}},
        )
        display_columns, columns = _get_columns(router_obj)
        data = utils.get_item_properties(
            router_obj, columns, formatters=_formatters
        )
        return (display_columns, data)


# TODO(yanxing'an): Use the SDK resource mapped attribute names once the
# OSC minimum requirements include SDK 1.0.
class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs):
    _description = _("Create a new router")

    def get_parser(self, prog_name):
        parser = super(CreateRouter, self).get_parser(prog_name)
        parser.add_argument(
            'name', metavar='<name>', help=_("New router name")
        )
        admin_group = parser.add_mutually_exclusive_group()
        admin_group.add_argument(
            '--enable',
            action='store_true',
            default=True,
            help=_("Enable router (default)"),
        )
        admin_group.add_argument(
            '--disable', action='store_true', help=_("Disable router")
        )
        distribute_group = parser.add_mutually_exclusive_group()
        distribute_group.add_argument(
            '--distributed',
            action='store_true',
            help=_("Create a distributed router"),
        )
        distribute_group.add_argument(
            '--centralized',
            action='store_true',
            help=_("Create a centralized router"),
        )
        ha_group = parser.add_mutually_exclusive_group()
        ha_group.add_argument(
            '--ha',
            action='store_true',
            help=_("Create a highly available router"),
        )
        ha_group.add_argument(
            '--no-ha', action='store_true', help=_("Create a legacy router")
        )
        parser.add_argument(
            '--description',
            metavar='<description>',
            help=_("Set router description"),
        )
        parser.add_argument(
            '--project',
            metavar='<project>',
            help=_("Owner's project (name or ID)"),
        )
        identity_common.add_project_domain_option_to_parser(parser)
        parser.add_argument(
            '--availability-zone-hint',
            metavar='<availability-zone>',
            action='append',
            dest='availability_zone_hints',
            help=_(
                "Availability Zone in which to create this router "
                "(Router Availability Zone extension required, "
                "repeat option to set multiple availability zones)"
            ),
        )
        _tag.add_tag_option_to_parser_for_create(parser, _('router'))
        parser.add_argument(
            '--external-gateway',
            metavar="<network>",
            help=_("External Network used as router's gateway (name or ID)"),
        )
        parser.add_argument(
            '--fixed-ip',
            metavar='subnet=<subnet>,ip-address=<ip-address>',
            action=parseractions.MultiKeyValueAction,
            optional_keys=['subnet', 'ip-address'],
            help=_(
                "Desired IP and/or subnet (name or ID) "
                "on external gateway: "
                "subnet=<subnet>,ip-address=<ip-address> "
                "(repeat option to set multiple fixed IP addresses)"
            ),
        )
        snat_group = parser.add_mutually_exclusive_group()
        snat_group.add_argument(
            '--enable-snat',
            action='store_true',
            help=_("Enable Source NAT on external gateway"),
        )
        snat_group.add_argument(
            '--disable-snat',
            action='store_true',
            help=_("Disable Source NAT on external gateway"),
        )
        ndp_proxy_group = parser.add_mutually_exclusive_group()
        ndp_proxy_group.add_argument(
            '--enable-ndp-proxy',
            dest='enable_ndp_proxy',
            default=None,
            action='store_true',
            help=_("Enable IPv6 NDP proxy on external gateway"),
        )
        ndp_proxy_group.add_argument(
            '--disable-ndp-proxy',
            dest='enable_ndp_proxy',
            default=None,
            action='store_false',
            help=_("Disable IPv6 NDP proxy on external gateway"),
        )
        parser.add_argument(
            '--flavor-id',
            metavar='<flavor-id>',
            help=_("Associate the router to a flavor by ID"),
        )

        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.network

        attrs = _get_attrs(self.app.client_manager, parsed_args)
        if parsed_args.ha:
            attrs['ha'] = True
        if parsed_args.no_ha:
            attrs['ha'] = False
        attrs.update(
            self._parse_extra_properties(parsed_args.extra_properties)
        )

        if parsed_args.enable_ndp_proxy and not parsed_args.external_gateway:
            msg = _(
                "You must specify '--external-gateway' in order "
                "to enable router's NDP proxy"
            )
            raise exceptions.CommandError(msg)

        if parsed_args.enable_ndp_proxy is not None:
            attrs['enable_ndp_proxy'] = parsed_args.enable_ndp_proxy

        obj = client.create_router(**attrs)
        # tags cannot be set when created, so tags need to be set later.
        _tag.update_tags_for_set(client, obj, parsed_args)

        if (
            parsed_args.disable_snat
            or parsed_args.enable_snat
            or parsed_args.fixed_ip
        ) and not parsed_args.external_gateway:
            msg = _(
                "You must specify '--external-gateway' in order "
                "to specify SNAT or fixed-ip values"
            )
            raise exceptions.CommandError(msg)

        display_columns, columns = _get_columns(obj)
        data = utils.get_item_properties(obj, columns, formatters=_formatters)

        return (display_columns, data)


class DeleteRouter(command.Command):
    _description = _("Delete router(s)")

    def get_parser(self, prog_name):
        parser = super(DeleteRouter, self).get_parser(prog_name)
        parser.add_argument(
            'router',
            metavar="<router>",
            nargs="+",
            help=_("Router(s) to delete (name or ID)"),
        )
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.network
        result = 0

        for router in parsed_args.router:
            try:
                obj = client.find_router(router, ignore_missing=False)
                client.delete_router(obj)
            except Exception as e:
                result += 1
                LOG.error(
                    _(
                        "Failed to delete router with "
                        "name or ID '%(router)s': %(e)s"
                    ),
                    {'router': router, 'e': e},
                )

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


# TODO(yanxing'an): Use the SDK resource mapped attribute names once the
# OSC minimum requirements include SDK 1.0.
class ListRouter(command.Lister):
    _description = _("List routers")

    def get_parser(self, prog_name):
        parser = super(ListRouter, self).get_parser(prog_name)
        parser.add_argument(
            '--name',
            metavar='<name>',
            help=_("List routers according to their name"),
        )
        admin_state_group = parser.add_mutually_exclusive_group()
        admin_state_group.add_argument(
            '--enable', action='store_true', help=_("List enabled routers")
        )
        admin_state_group.add_argument(
            '--disable', action='store_true', help=_("List disabled routers")
        )
        parser.add_argument(
            '--long',
            action='store_true',
            default=False,
            help=_("List additional fields in output"),
        )
        parser.add_argument(
            '--project',
            metavar='<project>',
            help=_("List routers according to their project (name or ID)"),
        )
        identity_common.add_project_domain_option_to_parser(parser)
        parser.add_argument(
            '--agent',
            metavar='<agent-id>',
            help=_("List routers hosted by an agent (ID only)"),
        )
        _tag.add_tag_filtering_option_to_parser(parser, _('routers'))

        return parser

    def take_action(self, parsed_args):
        identity_client = self.app.client_manager.identity
        client = self.app.client_manager.network

        columns = (
            'id',
            'name',
            'status',
            'is_admin_state_up',
            'project_id',
        )
        column_headers = (
            'ID',
            'Name',
            'Status',
            'State',
            'Project',
        )

        args = {}

        if parsed_args.name is not None:
            args['name'] = parsed_args.name

        if parsed_args.enable:
            args['admin_state_up'] = True
            args['is_admin_state_up'] = True
        elif parsed_args.disable:
            args['admin_state_up'] = False
            args['is_admin_state_up'] = False

        if parsed_args.project:
            project_id = identity_common.find_project(
                identity_client,
                parsed_args.project,
                parsed_args.project_domain,
            ).id
            args['project_id'] = project_id

        _tag.get_tag_filtering_args(parsed_args, args)

        if parsed_args.agent is not None:
            agent = client.get_agent(parsed_args.agent)
            data = client.agent_hosted_routers(agent)
            # NOTE: Networking API does not support filtering by parameters,
            # so we need filtering in the client side.
            data = [d for d in data if self._filter_match(d, args)]
        else:
            data = client.routers(**args)

        # check if "HA" and "Distributed" columns should be displayed also
        data = list(data)
        for d in data:
            if (
                d.is_distributed is not None
                and 'is_distributed' not in columns
            ):
                columns = columns + ('is_distributed',)
                column_headers = column_headers + ('Distributed',)
            if d.is_ha is not None and 'is_ha' not in columns:
                columns = columns + ('is_ha',)
                column_headers = column_headers + ('HA',)
        if parsed_args.long:
            columns = columns + (
                'routes',
                'external_gateway_info',
            )
            column_headers = column_headers + (
                'Routes',
                'External gateway info',
            )
            # availability zone will be available only when
            # router_availability_zone extension is enabled
            if client.find_extension("router_availability_zone"):
                columns = columns + ('availability_zones',)
                column_headers = column_headers + ('Availability zones',)
            columns = columns + ('tags',)
            column_headers = column_headers + ('Tags',)

        return (
            column_headers,
            (
                utils.get_item_properties(
                    s,
                    columns,
                    formatters=_formatters,
                )
                for s in data
            ),
        )

    @staticmethod
    def _filter_match(data, conditions):
        for key, value in conditions.items():
            try:
                if getattr(data, key) != value:
                    return False
            except AttributeError:
                # Some filter attributes like tenant_id or admin_state_up
                # are backward compatibility in older OpenStack SDK support.
                # They does not exist in the latest release.
                # In this case we just skip checking such filter condition.
                continue
        return True


class RemovePortFromRouter(command.Command):
    _description = _("Remove a port from a router")

    def get_parser(self, prog_name):
        parser = super(RemovePortFromRouter, self).get_parser(prog_name)
        parser.add_argument(
            'router',
            metavar='<router>',
            help=_("Router from which port will be removed (name or ID)"),
        )
        parser.add_argument(
            'port',
            metavar='<port>',
            help=_("Port to be removed and deleted (name or ID)"),
        )
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.network
        port = client.find_port(parsed_args.port, ignore_missing=False)
        client.remove_interface_from_router(
            client.find_router(parsed_args.router, ignore_missing=False),
            port_id=port.id,
        )


class RemoveSubnetFromRouter(command.Command):
    _description = _("Remove a subnet from a router")

    def get_parser(self, prog_name):
        parser = super(RemoveSubnetFromRouter, self).get_parser(prog_name)
        parser.add_argument(
            'router',
            metavar='<router>',
            help=_(
                "Router from which the subnet will be removed (name or ID)"
            ),
        )
        parser.add_argument(
            'subnet',
            metavar='<subnet>',
            help=_("Subnet to be removed (name or ID)"),
        )
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.network
        subnet = client.find_subnet(parsed_args.subnet, ignore_missing=False)
        client.remove_interface_from_router(
            client.find_router(parsed_args.router, ignore_missing=False),
            subnet_id=subnet.id,
        )


# TODO(yanxing'an): Use the SDK resource mapped attribute names once the
# OSC minimum requirements include SDK 1.0.
class SetRouter(common.NeutronCommandWithExtraArgs):
    _description = _("Set router properties")

    def get_parser(self, prog_name):
        parser = super(SetRouter, self).get_parser(prog_name)
        parser.add_argument(
            'router',
            metavar="<router>",
            help=_("Router to modify (name or ID)"),
        )
        parser.add_argument(
            '--name', metavar='<name>', help=_("Set router name")
        )
        parser.add_argument(
            '--description',
            metavar='<description>',
            help=_('Set router description'),
        )
        admin_group = parser.add_mutually_exclusive_group()
        admin_group.add_argument(
            '--enable',
            action='store_true',
            default=None,
            help=_("Enable router"),
        )
        admin_group.add_argument(
            '--disable', action='store_true', help=_("Disable router")
        )
        distribute_group = parser.add_mutually_exclusive_group()
        distribute_group.add_argument(
            '--distributed',
            action='store_true',
            help=_("Set router to distributed mode (disabled router only)"),
        )
        distribute_group.add_argument(
            '--centralized',
            action='store_true',
            help=_("Set router to centralized mode (disabled router only)"),
        )
        parser.add_argument(
            '--route',
            metavar='destination=<subnet>,gateway=<ip-address>',
            action=parseractions.MultiKeyValueAction,
            dest='routes',
            default=None,
            required_keys=['destination', 'gateway'],
            help=_(
                "Add routes to the router "
                "destination: destination subnet (in CIDR notation) "
                "gateway: nexthop IP address "
                "(repeat option to add multiple routes). "
                "This is deprecated in favor of 'router add/remove route' "
                "since it is prone to race conditions between concurrent "
                "clients when not used together with --no-route to "
                "overwrite the current value of 'routes'."
            ),
        )
        parser.add_argument(
            '--no-route',
            action='store_true',
            help=_(
                "Clear routes associated with the router. "
                "Specify both --route and --no-route to overwrite "
                "current value of routes."
            ),
        )
        routes_ha = parser.add_mutually_exclusive_group()
        routes_ha.add_argument(
            '--ha',
            action='store_true',
            help=_(
                "Set the router as highly available " "(disabled router only)"
            ),
        )
        routes_ha.add_argument(
            '--no-ha',
            action='store_true',
            help=_(
                "Clear high availability attribute of the router "
                "(disabled router only)"
            ),
        )
        parser.add_argument(
            '--external-gateway',
            metavar="<network>",
            help=_("External Network used as router's gateway (name or ID)"),
        )
        parser.add_argument(
            '--fixed-ip',
            metavar='subnet=<subnet>,ip-address=<ip-address>',
            action=parseractions.MultiKeyValueAction,
            optional_keys=['subnet', 'ip-address'],
            help=_(
                "Desired IP and/or subnet (name or ID) "
                "on external gateway: "
                "subnet=<subnet>,ip-address=<ip-address> "
                "(repeat option to set multiple fixed IP addresses)"
            ),
        )
        snat_group = parser.add_mutually_exclusive_group()
        snat_group.add_argument(
            '--enable-snat',
            action='store_true',
            help=_("Enable Source NAT on external gateway"),
        )
        snat_group.add_argument(
            '--disable-snat',
            action='store_true',
            help=_("Disable Source NAT on external gateway"),
        )
        ndp_proxy_group = parser.add_mutually_exclusive_group()
        ndp_proxy_group.add_argument(
            '--enable-ndp-proxy',
            dest='enable_ndp_proxy',
            default=None,
            action='store_true',
            help=_("Enable IPv6 NDP proxy on external gateway"),
        )
        ndp_proxy_group.add_argument(
            '--disable-ndp-proxy',
            dest='enable_ndp_proxy',
            default=None,
            action='store_false',
            help=_("Disable IPv6 NDP proxy on external gateway"),
        )
        qos_policy_group = parser.add_mutually_exclusive_group()
        qos_policy_group.add_argument(
            '--qos-policy',
            metavar='<qos-policy>',
            help=_("Attach QoS policy to router gateway IPs"),
        )
        qos_policy_group.add_argument(
            '--no-qos-policy',
            action='store_true',
            help=_("Remove QoS policy from router gateway IPs"),
        )
        _tag.add_tag_option_to_parser_for_set(parser, _('router'))
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.network
        obj = client.find_router(parsed_args.router, ignore_missing=False)

        # Get the common attributes.
        attrs = _get_attrs(self.app.client_manager, parsed_args)

        # Get the route attributes.
        if parsed_args.ha:
            attrs['ha'] = True
        elif parsed_args.no_ha:
            attrs['ha'] = False

        if parsed_args.routes is not None:
            for route in parsed_args.routes:
                route['nexthop'] = route.pop('gateway')
            attrs['routes'] = parsed_args.routes
            if not parsed_args.no_route:
                # Map the route keys and append to the current routes.
                # The REST API will handle route validation and duplicates.
                attrs['routes'] += obj.routes
        elif parsed_args.no_route:
            attrs['routes'] = []
        if (
            parsed_args.disable_snat
            or parsed_args.enable_snat
            or parsed_args.fixed_ip
        ) and not parsed_args.external_gateway:
            msg = _(
                "You must specify '--external-gateway' in order "
                "to update the SNAT or fixed-ip values"
            )
            raise exceptions.CommandError(msg)

        if (
            parsed_args.qos_policy or parsed_args.no_qos_policy
        ) and not parsed_args.external_gateway:
            try:
                original_net_id = obj.external_gateway_info['network_id']
            except (KeyError, TypeError):
                msg = _(
                    "You must specify '--external-gateway' or the router "
                    "must already have an external network in order to "
                    "set router gateway IP QoS"
                )
                raise exceptions.CommandError(msg)
            else:
                if not attrs.get('external_gateway_info'):
                    attrs['external_gateway_info'] = {}
                attrs['external_gateway_info']['network_id'] = original_net_id
        if parsed_args.qos_policy:
            check_qos_id = client.find_qos_policy(
                parsed_args.qos_policy, ignore_missing=False
            ).id
            attrs['external_gateway_info']['qos_policy_id'] = check_qos_id

        if 'no_qos_policy' in parsed_args and parsed_args.no_qos_policy:
            attrs['external_gateway_info']['qos_policy_id'] = None

        attrs.update(
            self._parse_extra_properties(parsed_args.extra_properties)
        )

        if parsed_args.enable_ndp_proxy is not None:
            attrs['enable_ndp_proxy'] = parsed_args.enable_ndp_proxy

        if attrs:
            client.update_router(obj, **attrs)
        # tags is a subresource and it needs to be updated separately.
        _tag.update_tags_for_set(client, obj, parsed_args)


class ShowRouter(command.ShowOne):
    _description = _("Display router details")

    def get_parser(self, prog_name):
        parser = super(ShowRouter, self).get_parser(prog_name)
        parser.add_argument(
            'router',
            metavar="<router>",
            help=_("Router to display (name or ID)"),
        )
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.network
        obj = client.find_router(parsed_args.router, ignore_missing=False)
        interfaces_info = []
        filters = {}
        filters['device_id'] = obj.id
        for port in client.ports(**filters):
            if port.device_owner != "network:router_gateway":
                for ip_spec in port.fixed_ips:
                    int_info = {
                        'port_id': port.id,
                        'ip_address': ip_spec.get('ip_address'),
                        'subnet_id': ip_spec.get('subnet_id'),
                    }
                    interfaces_info.append(int_info)

        setattr(obj, 'interfaces_info', interfaces_info)
        display_columns, columns = _get_columns(obj)
        _formatters['interfaces_info'] = RouterInfoColumn
        data = utils.get_item_properties(obj, columns, formatters=_formatters)

        return (display_columns, data)


class UnsetRouter(common.NeutronUnsetCommandWithExtraArgs):
    _description = _("Unset router properties")

    def get_parser(self, prog_name):
        parser = super(UnsetRouter, self).get_parser(prog_name)
        parser.add_argument(
            '--route',
            metavar='destination=<subnet>,gateway=<ip-address>',
            action=parseractions.MultiKeyValueAction,
            dest='routes',
            default=None,
            required_keys=['destination', 'gateway'],
            help=_(
                "Routes to be removed from the router "
                "destination: destination subnet (in CIDR notation) "
                "gateway: nexthop IP address "
                "(repeat option to unset multiple routes)"
            ),
        )
        parser.add_argument(
            '--external-gateway',
            action='store_true',
            default=False,
            help=_("Remove external gateway information from the router"),
        )
        parser.add_argument(
            '--qos-policy',
            action='store_true',
            default=False,
            help=_("Remove QoS policy from router gateway IPs"),
        )
        parser.add_argument(
            'router',
            metavar="<router>",
            help=_("Router to modify (name or ID)"),
        )
        _tag.add_tag_option_to_parser_for_unset(parser, _('router'))
        return parser

    def take_action(self, parsed_args):
        client = self.app.client_manager.network
        obj = client.find_router(parsed_args.router, ignore_missing=False)
        tmp_routes = copy.deepcopy(obj.routes)
        tmp_external_gateway_info = copy.deepcopy(obj.external_gateway_info)
        attrs = {}
        if parsed_args.routes:
            try:
                for route in parsed_args.routes:
                    route['nexthop'] = route.pop('gateway')
                    tmp_routes.remove(route)
            except ValueError:
                msg = _("Router does not contain route %s") % route
                raise exceptions.CommandError(msg)
            attrs['routes'] = tmp_routes
        if parsed_args.qos_policy:
            try:
                if (
                    tmp_external_gateway_info['network_id']
                    and tmp_external_gateway_info['qos_policy_id']
                ):
                    pass
            except (KeyError, TypeError):
                msg = _("Router does not have external network or qos policy")
                raise exceptions.CommandError(msg)
            else:
                attrs['external_gateway_info'] = {
                    'network_id': tmp_external_gateway_info['network_id'],
                    'qos_policy_id': None,
                }

        if parsed_args.external_gateway:
            attrs['external_gateway_info'] = {}

        attrs.update(
            self._parse_extra_properties(parsed_args.extra_properties)
        )

        if attrs:
            client.update_router(obj, **attrs)
        # tags is a subresource and it needs to be updated separately.
        _tag.update_tags_for_unset(client, obj, parsed_args)
