#    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 copy
from http import client as http_client

from ironic_lib import metrics_utils
from oslo_utils import uuidutils
import pecan

from ironic import api
from ironic.api.controllers import link
from ironic.api.controllers.v1 import collection
from ironic.api.controllers.v1 import notification_utils as notify
from ironic.api.controllers.v1 import port
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api import method
from ironic.common import args
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import states as ir_states
from ironic import objects

METRICS = metrics_utils.get_metrics_logger(__name__)

_DEFAULT_RETURN_FIELDS = ['uuid', 'address', 'name']

PORTGROUP_SCHEMA = {
    'type': 'object',
    'properties': {
        'address': {'type': ['string', 'null']},
        'extra': {'type': ['object', 'null']},
        'mode': {'type': ['string', 'null']},
        'name': {'type': ['string', 'null']},
        'node_uuid': {'type': 'string'},
        'properties': {'type': ['object', 'null']},
        'standalone_ports_supported': {'type': ['string', 'boolean', 'null']},
        'uuid': {'type': ['string', 'null']},
    },
    'required': ['node_uuid'],
    'additionalProperties': False,
}

PORTGROUP_PATCH_SCHEMA = copy.deepcopy(PORTGROUP_SCHEMA)
# patching /extra/vif_port_id has the side-effect of modifying
# internal_info values, so include it in the patch schema
PORTGROUP_PATCH_SCHEMA['properties']['internal_info'] = {
    'type': ['null', 'object']}

PORTGROUP_VALIDATOR_EXTRA = args.dict_valid(
    address=args.mac_address,
    node_uuid=args.uuid,
    standalone_ports_supported=args.boolean,
    uuid=args.uuid
)
PORTGROUP_VALIDATOR = args.and_valid(
    args.schema(PORTGROUP_SCHEMA),
    PORTGROUP_VALIDATOR_EXTRA
)

PORTGROUP_PATCH_VALIDATOR = args.and_valid(
    args.schema(PORTGROUP_PATCH_SCHEMA),
    PORTGROUP_VALIDATOR_EXTRA
)

PATCH_ALLOWED_FIELDS = [
    'address',
    'extra',
    'mode',
    'name',
    'node_uuid',
    'properties',
    'standalone_ports_supported'
]


def convert_with_links(rpc_portgroup, fields=None, sanitize=True):
    """Add links to the portgroup."""
    portgroup = api_utils.object_to_dict(
        rpc_portgroup,
        link_resource='portgroups',
        fields=(
            'address',
            'extra',
            'internal_info',
            'mode',
            'name',
            'properties',
            'standalone_ports_supported'
        )
    )
    api_utils.populate_node_uuid(rpc_portgroup, portgroup)
    url = api.request.public_url
    portgroup['ports'] = [
        link.make_link('self', url, 'portgroups',
                       rpc_portgroup.uuid + "/ports"),
        link.make_link('bookmark', url, 'portgroups',
                       rpc_portgroup.uuid + "/ports", bookmark=True)
    ]

    if fields is not None:
        api_utils.check_for_invalid_fields(fields, portgroup)

    if not sanitize:
        return portgroup

    api_utils.sanitize_dict(portgroup, fields)

    return portgroup


def list_convert_with_links(rpc_portgroups, limit, url=None, fields=None,
                            **kwargs):
    return collection.list_convert_with_links(
        items=[convert_with_links(p, fields=fields, sanitize=False)
               for p in rpc_portgroups],
        item_name='portgroups',
        limit=limit,
        url=url,
        fields=fields,
        sanitize_func=api_utils.sanitize_dict,
        **kwargs
    )


class PortgroupsController(pecan.rest.RestController):
    """REST controller for portgroups."""

    _custom_actions = {
        'detail': ['GET'],
    }

    invalid_sort_key_list = ['extra', 'internal_info', 'properties']

    _subcontroller_map = {
        'ports': port.PortsController,
    }

    @pecan.expose()
    def _lookup(self, ident, *remainder):
        if not api_utils.allow_portgroups():
            pecan.abort(http_client.NOT_FOUND)
        try:
            ident = args.uuid_or_name('portgroup', ident)
        except exception.InvalidParameterValue as e:
            pecan.abort(http_client.BAD_REQUEST, e.args[0])
        if not remainder:
            return
        subcontroller = self._subcontroller_map.get(remainder[0])
        if subcontroller:
            if api_utils.allow_portgroups_subcontrollers():
                return subcontroller(
                    portgroup_ident=ident,
                    node_ident=self.parent_node_ident), remainder[1:]
            pecan.abort(http_client.NOT_FOUND)

    def __init__(self, node_ident=None):
        super(PortgroupsController, self).__init__()
        self.parent_node_ident = node_ident

    def _get_portgroups_collection(self, node_ident, address,
                                   marker, limit, sort_key, sort_dir,
                                   resource_url=None, fields=None,
                                   detail=None):
        """Return portgroups collection.

        :param node_ident: UUID or name of a node.
        :param address: MAC address of a portgroup.
        :param marker: Pagination marker for large data sets.
        :param limit: Maximum number of resources to return in a single result.
        :param sort_key: Column to sort results by. Default: id.
        :param sort_dir: Direction to sort. "asc" or "desc". Default: asc.
        :param resource_url: Optional, URL to the portgroup resource.
        :param fields: Optional, a list with a specified set of fields
                       of the resource to be returned.
        """
        limit = api_utils.validate_limit(limit)
        sort_dir = api_utils.validate_sort_dir(sort_dir)

        marker_obj = None
        if marker:
            marker_obj = objects.Portgroup.get_by_uuid(api.request.context,
                                                       marker)

        if sort_key in self.invalid_sort_key_list:
            raise exception.InvalidParameterValue(
                _("The sort_key value %(key)s is an invalid field for "
                  "sorting") % {'key': sort_key})

        node_ident = self.parent_node_ident or node_ident

        if node_ident:
            # FIXME: Since all we need is the node ID, we can
            #        make this more efficient by only querying
            #        for that column. This will get cleaned up
            #        as we move to the object interface.
            node = api_utils.get_rpc_node(node_ident)
            portgroups = objects.Portgroup.list_by_node_id(
                api.request.context, node.id, limit,
                marker_obj, sort_key=sort_key, sort_dir=sort_dir)
        elif address:
            portgroups = self._get_portgroups_by_address(address)
        else:
            portgroups = objects.Portgroup.list(api.request.context, limit,
                                                marker_obj, sort_key=sort_key,
                                                sort_dir=sort_dir)
        parameters = {}
        if detail is not None:
            parameters['detail'] = detail

        return list_convert_with_links(portgroups, limit,
                                       url=resource_url,
                                       fields=fields,
                                       sort_key=sort_key,
                                       sort_dir=sort_dir,
                                       **parameters)

    def _get_portgroups_by_address(self, address):
        """Retrieve a portgroup by its address.

        :param address: MAC address of a portgroup, to get the portgroup
                        which has this MAC address.
        :returns: a list with the portgroup, or an empty list if no portgroup
                  is found.

        """
        try:
            portgroup = objects.Portgroup.get_by_address(api.request.context,
                                                         address)
            return [portgroup]
        except exception.PortgroupNotFound:
            return []

    @METRICS.timer('PortgroupsController.get_all')
    @method.expose()
    @args.validate(node=args.uuid_or_name, address=args.mac_address,
                   marker=args.uuid, limit=args.integer, sort_key=args.string,
                   sort_dir=args.string, fields=args.string_list,
                   detail=args.boolean)
    def get_all(self, node=None, address=None, marker=None,
                limit=None, sort_key='id', sort_dir='asc', fields=None,
                detail=None):
        """Retrieve a list of portgroups.

        :param node: UUID or name of a node, to get only portgroups for that
                     node.
        :param address: MAC address of a portgroup, to get the portgroup which
                        has this MAC address.
        :param marker: pagination marker for large data sets.
        :param limit: maximum number of resources to return in a single result.
                      This value cannot be larger than the value of max_limit
                      in the [api] section of the ironic configuration, or only
                      max_limit resources will be returned.
        :param sort_key: column to sort results by. Default: id.
        :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
        :param fields: Optional, a list with a specified set of fields
                       of the resource to be returned.
        """
        if not api_utils.allow_portgroups():
            raise exception.NotFound()

        api_utils.check_policy('baremetal:portgroup:get')

        api_utils.check_allowed_portgroup_fields(fields)
        api_utils.check_allowed_portgroup_fields([sort_key])

        fields = api_utils.get_request_return_fields(fields, detail,
                                                     _DEFAULT_RETURN_FIELDS)

        return self._get_portgroups_collection(node, address,
                                               marker, limit,
                                               sort_key, sort_dir,
                                               fields=fields,
                                               detail=detail)

    @METRICS.timer('PortgroupsController.detail')
    @method.expose()
    @args.validate(node=args.uuid_or_name, address=args.mac_address,
                   marker=args.uuid, limit=args.integer, sort_key=args.string,
                   sort_dir=args.string)
    def detail(self, node=None, address=None, marker=None,
               limit=None, sort_key='id', sort_dir='asc'):
        """Retrieve a list of portgroups with detail.

        :param node: UUID or name of a node, to get only portgroups for that
                     node.
        :param address: MAC address of a portgroup, to get the portgroup which
                        has this MAC address.
        :param marker: pagination marker for large data sets.
        :param limit: maximum number of resources to return in a single result.
                      This value cannot be larger than the value of max_limit
                      in the [api] section of the ironic configuration, or only
                      max_limit resources will be returned.
        :param sort_key: column to sort results by. Default: id.
        :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
        """
        if not api_utils.allow_portgroups():
            raise exception.NotFound()

        api_utils.check_policy('baremetal:portgroup:get')
        api_utils.check_allowed_portgroup_fields([sort_key])

        # NOTE: /detail should only work against collections
        parent = api.request.path.split('/')[:-1][-1]
        if parent != "portgroups":
            raise exception.HTTPNotFound()

        resource_url = '/'.join(['portgroups', 'detail'])
        return self._get_portgroups_collection(
            node, address, marker, limit, sort_key, sort_dir,
            resource_url=resource_url)

    @METRICS.timer('PortgroupsController.get_one')
    @method.expose()
    @args.validate(portgroup_ident=args.uuid_or_name, fields=args.string_list)
    def get_one(self, portgroup_ident, fields=None):
        """Retrieve information about the given portgroup.

        :param portgroup_ident: UUID or logical name of a portgroup.
        :param fields: Optional, a list with a specified set of fields
                       of the resource to be returned.
        """
        if not api_utils.allow_portgroups():
            raise exception.NotFound()

        api_utils.check_policy('baremetal:portgroup:get')

        if self.parent_node_ident:
            raise exception.OperationNotPermitted()

        api_utils.check_allowed_portgroup_fields(fields)

        rpc_portgroup = api_utils.get_rpc_portgroup_with_suffix(
            portgroup_ident)
        return convert_with_links(rpc_portgroup, fields=fields)

    @METRICS.timer('PortgroupsController.post')
    @method.expose(status_code=http_client.CREATED)
    @method.body('portgroup')
    @args.validate(portgroup=PORTGROUP_VALIDATOR)
    def post(self, portgroup):
        """Create a new portgroup.

        :param portgroup: a portgroup within the request body.
        """
        if not api_utils.allow_portgroups():
            raise exception.NotFound()

        context = api.request.context
        api_utils.check_policy('baremetal:portgroup:create')

        if self.parent_node_ident:
            raise exception.OperationNotPermitted()

        if (not api_utils.allow_portgroup_mode_properties()
                and (portgroup.get('mode') or portgroup.get('properties'))):
            raise exception.NotAcceptable()

        if (portgroup.get('name')
                and not api_utils.is_valid_logical_name(portgroup['name'])):
            error_msg = _("Cannot create portgroup with invalid name "
                          "'%(name)s'") % {'name': portgroup['name']}
            raise exception.ClientSideError(
                error_msg, status_code=http_client.BAD_REQUEST)

        api_utils.handle_post_port_like_extra_vif(portgroup)

        # NOTE(yuriyz): UUID is mandatory for notifications payload
        if not portgroup.get('uuid'):
            portgroup['uuid'] = uuidutils.generate_uuid()

        node = api_utils.replace_node_uuid_with_id(portgroup)

        new_portgroup = objects.Portgroup(context, **portgroup)

        notify.emit_start_notification(context, new_portgroup, 'create',
                                       node_uuid=node.uuid)
        with notify.handle_error_notification(context, new_portgroup, 'create',
                                              node_uuid=node.uuid):
            new_portgroup.create()
        notify.emit_end_notification(context, new_portgroup, 'create',
                                     node_uuid=node.uuid)

        # Set the HTTP Location Header
        api.response.location = link.build_url('portgroups',
                                               new_portgroup.uuid)
        return convert_with_links(new_portgroup)

    @METRICS.timer('PortgroupsController.patch')
    @method.expose()
    @method.body('patch')
    @args.validate(portgroup_ident=args.uuid_or_name, patch=args.patch)
    def patch(self, portgroup_ident, patch):
        """Update an existing portgroup.

        :param portgroup_ident: UUID or logical name of a portgroup.
        :param patch: a json PATCH document to apply to this portgroup.
        """
        if not api_utils.allow_portgroups():
            raise exception.NotFound()

        context = api.request.context
        api_utils.check_policy('baremetal:portgroup:update')

        if self.parent_node_ident:
            raise exception.OperationNotPermitted()

        if (not api_utils.allow_portgroup_mode_properties()
                and (api_utils.is_path_updated(patch, '/mode')
                     or api_utils.is_path_updated(patch, '/properties'))):
            raise exception.NotAcceptable()

        api_utils.patch_validate_allowed_fields(patch, PATCH_ALLOWED_FIELDS)

        rpc_portgroup = api_utils.get_rpc_portgroup_with_suffix(
            portgroup_ident)

        names = api_utils.get_patch_values(patch, '/name')
        for name in names:
            if (name and not api_utils.is_valid_logical_name(name)):
                error_msg = _("Portgroup %(portgroup)s: Cannot change name to"
                              " invalid name '%(name)s'") % {'portgroup':
                                                             portgroup_ident,
                                                             'name': name}
                raise exception.ClientSideError(
                    error_msg, status_code=http_client.BAD_REQUEST)

        portgroup_dict = rpc_portgroup.as_dict()

        # NOTE:
        # 1) Remove node_id because it's an internal value and
        #    not present in the API object
        # 2) Add node_uuid
        rpc_node = api_utils.replace_node_id_with_uuid(portgroup_dict)

        portgroup_dict = api_utils.apply_jsonpatch(portgroup_dict, patch)

        if 'mode' not in portgroup_dict:
            msg = _("'mode' is a mandatory attribute and can not be removed")
            raise exception.ClientSideError(msg)

        try:
            if portgroup_dict['node_uuid'] != rpc_node.uuid:
                rpc_node = objects.Node.get(api.request.context,
                                            portgroup_dict['node_uuid'])

        except exception.NodeNotFound as e:
            # Change error code because 404 (NotFound) is inappropriate
            # response for a POST request to patch a Portgroup
            e.code = http_client.BAD_REQUEST  # BadRequest
            raise

        api_utils.handle_patch_port_like_extra_vif(
            rpc_portgroup, portgroup_dict.get('internal_info'), patch)

        api_utils.patched_validate_with_schema(
            portgroup_dict, PORTGROUP_PATCH_SCHEMA, PORTGROUP_PATCH_VALIDATOR)

        api_utils.patch_update_changed_fields(
            portgroup_dict, rpc_portgroup, fields=objects.Portgroup.fields,
            schema=PORTGROUP_PATCH_SCHEMA, id_map={'node_id': rpc_node.id}
        )

        if (rpc_node.provision_state == ir_states.INSPECTING
                and api_utils.allow_inspect_wait_state()):
            msg = _('Cannot update portgroup "%(portgroup)s" on node '
                    '"%(node)s" while it is in state "%(state)s".') % {
                'portgroup': rpc_portgroup.uuid, 'node': rpc_node.uuid,
                'state': ir_states.INSPECTING}
            raise exception.ClientSideError(msg,
                                            status_code=http_client.CONFLICT)

        notify.emit_start_notification(context, rpc_portgroup, 'update',
                                       node_uuid=rpc_node.uuid)
        with notify.handle_error_notification(context, rpc_portgroup, 'update',
                                              node_uuid=rpc_node.uuid):
            topic = api.request.rpcapi.get_topic_for(rpc_node)
            new_portgroup = api.request.rpcapi.update_portgroup(
                context, rpc_portgroup, topic)

        api_portgroup = convert_with_links(new_portgroup)
        notify.emit_end_notification(context, new_portgroup, 'update',
                                     node_uuid=rpc_node.uuid)

        return api_portgroup

    @METRICS.timer('PortgroupsController.delete')
    @method.expose(status_code=http_client.NO_CONTENT)
    @args.validate(portgroup_ident=args.uuid_or_name)
    def delete(self, portgroup_ident):
        """Delete a portgroup.

        :param portgroup_ident: UUID or logical name of a portgroup.
        """
        if not api_utils.allow_portgroups():
            raise exception.NotFound()

        context = api.request.context
        api_utils.check_policy('baremetal:portgroup:delete')

        if self.parent_node_ident:
            raise exception.OperationNotPermitted()

        rpc_portgroup = api_utils.get_rpc_portgroup_with_suffix(
            portgroup_ident)
        rpc_node = objects.Node.get_by_id(api.request.context,
                                          rpc_portgroup.node_id)

        notify.emit_start_notification(context, rpc_portgroup, 'delete',
                                       node_uuid=rpc_node.uuid)
        with notify.handle_error_notification(context, rpc_portgroup, 'delete',
                                              node_uuid=rpc_node.uuid):
            topic = api.request.rpcapi.get_topic_for(rpc_node)
            api.request.rpcapi.destroy_portgroup(context, rpc_portgroup,
                                                 topic)
        notify.emit_end_notification(context, rpc_portgroup, 'delete',
                                     node_uuid=rpc_node.uuid)
