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

from six.moves import http_client

from ironic_tempest_plugin.services.baremetal import base


class BaremetalClient(base.BaremetalClient):
    """Base Tempest REST client for Ironic API v1."""
    version = '1'
    uri_prefix = 'v1'

    @base.handle_errors
    def list_nodes(self, **kwargs):
        """List all existing nodes."""
        return self._list_request('nodes', **kwargs)

    @base.handle_errors
    def list_nodes_detail(self, **kwargs):
        """Detailed list of all existing nodes."""
        return self._list_request('/nodes/detail', **kwargs)

    @base.handle_errors
    def list_chassis(self):
        """List all existing chassis."""
        return self._list_request('chassis')

    @base.handle_errors
    def list_chassis_nodes(self, chassis_uuid):
        """List all nodes associated with a chassis."""
        return self._list_request('/chassis/%s/nodes' % chassis_uuid)

    @base.handle_errors
    def list_ports(self, **kwargs):
        """List all existing ports."""
        return self._list_request('ports', **kwargs)

    @base.handle_errors
    def list_portgroups(self, **kwargs):
        """List all existing port groups."""
        return self._list_request('portgroups', **kwargs)

    @base.handle_errors
    def list_volume_connectors(self, **kwargs):
        """List all existing volume connectors."""
        return self._list_request('volume/connectors', **kwargs)

    @base.handle_errors
    def list_volume_targets(self, **kwargs):
        """List all existing volume targets."""
        return self._list_request('volume/targets', **kwargs)

    @base.handle_errors
    def list_node_ports(self, uuid):
        """List all ports associated with the node."""
        return self._list_request('/nodes/%s/ports' % uuid)

    @base.handle_errors
    def list_nodestates(self, uuid):
        """List all existing states."""
        return self._list_request('/nodes/%s/states' % uuid)

    @base.handle_errors
    def list_ports_detail(self, **kwargs):
        """Details list all existing ports."""
        return self._list_request('/ports/detail', **kwargs)

    @base.handle_errors
    def list_drivers(self):
        """List all existing drivers."""
        return self._list_request('drivers')

    @base.handle_errors
    def show_node(self, uuid):
        """Gets a specific node.

        :param uuid: Unique identifier of the node in UUID format.
        :return: Serialized node as a dictionary.

        """
        return self._show_request('nodes', uuid)

    @base.handle_errors
    def show_node_by_instance_uuid(self, instance_uuid):
        """Gets a node associated with given instance uuid.

        :param instance_uuid: Unique identifier of the instance in UUID format.
        :return: Serialized node as a dictionary.

        """
        uri = '/nodes/detail?instance_uuid=%s' % instance_uuid

        return self._show_request('nodes',
                                  uuid=None,
                                  uri=uri)

    @base.handle_errors
    def show_chassis(self, uuid):
        """Gets a specific chassis.

        :param uuid: Unique identifier of the chassis in UUID format.
        :return: Serialized chassis as a dictionary.

        """
        return self._show_request('chassis', uuid)

    @base.handle_errors
    def show_port(self, uuid):
        """Gets a specific port.

        :param uuid: Unique identifier of the port in UUID format.
        :return: Serialized port as a dictionary.

        """
        return self._show_request('ports', uuid)

    @base.handle_errors
    def show_portgroup(self, portgroup_ident):
        """Gets a specific port group.

        :param portgroup_ident: Name or UUID of the port group.
        :return: Serialized port group as a dictionary.
        """
        return self._show_request('portgroups', portgroup_ident)

    @base.handle_errors
    def show_volume_connector(self, volume_connector_ident):
        """Gets a specific volume connector.

        :param volume_connector_ident: UUID of the volume connector.
        :return: Serialized volume connector as a dictionary.
        """
        return self._show_request('volume/connectors', volume_connector_ident)

    @base.handle_errors
    def show_volume_target(self, volume_target_ident):
        """Gets a specific volume target.

        :param volume_target_ident: UUID of the volume target.
        :return: Serialized volume target as a dictionary.
        """
        return self._show_request('volume/targets', volume_target_ident)

    @base.handle_errors
    def show_port_by_address(self, address):
        """Gets a specific port by address.

        :param address: MAC address of the port.
        :return: Serialized port as a dictionary.

        """
        uri = '/ports/detail?address=%s' % address

        return self._show_request('ports', uuid=None, uri=uri)

    def show_driver(self, driver_name):
        """Gets a specific driver.

        :param driver_name: Name of driver.
        :return: Serialized driver as a dictionary.
        """
        return self._show_request('drivers', driver_name)

    @base.handle_errors
    def create_node(self, chassis_id=None, **kwargs):
        """Create a baremetal node with the specified parameters.

        :param chassis_id: The unique identifier of the chassis.
        :param cpu_arch: CPU architecture of the node. Default: x86_64.
        :param cpus: Number of CPUs. Default: 8.
        :param local_gb: Disk size. Default: 1024.
        :param memory_mb: Available RAM. Default: 4096.
        :param driver: Driver name. Default: "fake"
        :return: A tuple with the server response and the created node.

        """
        node = {}
        if kwargs.get('resource_class'):
            node['resource_class'] = kwargs['resource_class']

        node.update(
            {'chassis_uuid': chassis_id,
             'properties': {'cpu_arch': kwargs.get('cpu_arch', 'x86_64'),
                            'cpus': kwargs.get('cpus', 8),
                            'local_gb': kwargs.get('local_gb', 1024),
                            'memory_mb': kwargs.get('memory_mb', 4096)},
             'driver': kwargs.get('driver', 'fake')}
        )

        return self._create_request('nodes', node)

    @base.handle_errors
    def create_chassis(self, **kwargs):
        """Create a chassis with the specified parameters.

        :param description: The description of the chassis.
            Default: test-chassis
        :return: A tuple with the server response and the created chassis.

        """
        chassis = {'description': kwargs.get('description', 'test-chassis')}

        if 'uuid' in kwargs:
            chassis.update({'uuid': kwargs.get('uuid')})

        return self._create_request('chassis', chassis)

    @base.handle_errors
    def create_port(self, node_id, **kwargs):
        """Create a port with the specified parameters.

        :param node_id: The ID of the node which owns the port.
        :param address: MAC address of the port.
        :param extra: Meta data of the port. Default: {'foo': 'bar'}.
        :param uuid: UUID of the port.
        :param portgroup_uuid: The UUID of a portgroup of which this port is a
            member.
        :param physical_network: The physical network to which the port is
            attached.
        :return: A tuple with the server response and the created port.

        """
        port = {'extra': kwargs.get('extra', {'foo': 'bar'}),
                'uuid': kwargs['uuid']}

        if node_id is not None:
            port['node_uuid'] = node_id

        for key in ('address', 'physical_network', 'portgroup_uuid'):
            if kwargs.get(key) is not None:
                port[key] = kwargs[key]

        return self._create_request('ports', port)

    @base.handle_errors
    def create_portgroup(self, node_uuid, **kwargs):
        """Create a port group with the specified parameters.

        :param node_uuid: The UUID of the node which owns the port group.
        :param kwargs:
            address: MAC address of the port group. Optional.
            extra: Meta data of the port group. Default: {'foo': 'bar'}.
            name: Name of the port group. Optional.
            uuid: UUID of the port group. Optional.
        :return: A tuple with the server response and the created port group.
        """
        portgroup = {'extra': kwargs.get('extra', {'foo': 'bar'})}

        portgroup['node_uuid'] = node_uuid

        if kwargs.get('address'):
            portgroup['address'] = kwargs['address']

        if kwargs.get('name'):
            portgroup['name'] = kwargs['name']

        return self._create_request('portgroups', portgroup)

    @base.handle_errors
    def create_volume_connector(self, node_uuid, **kwargs):
        """Create a volume connector with the specified parameters.

        :param node_uuid: The UUID of the node which owns the volume connector.
        :param kwargs:
            type: type of the volume connector.
            connector_id: connector_id of the volume connector.
            uuid: UUID of the volume connector. Optional.
            extra: meta data of the volume connector; a dictionary. Optional.
        :return: A tuple with the server response and the created volume
            connector.
        """
        volume_connector = {'node_uuid': node_uuid}

        for arg in ('type', 'connector_id', 'uuid', 'extra'):
            if arg in kwargs:
                volume_connector[arg] = kwargs[arg]

        return self._create_request('volume/connectors', volume_connector)

    @base.handle_errors
    def create_volume_target(self, node_uuid, **kwargs):
        """Create a volume target with the specified parameters.

        :param node_uuid: The UUID of the node which owns the volume target.
        :param kwargs:
            volume_type: type of the volume target.
            volume_id: volume_id of the volume target.
            boot_index: boot index of the volume target.
            uuid: UUID of the volume target. Optional.
            extra: meta data of the volume target; a dictionary. Optional.
            properties: properties related to the type of the volume target;
                a dictionary. Optional.
        :return: A tuple with the server response and the created volume
            target.
        """
        volume_target = {'node_uuid': node_uuid}

        for arg in ('volume_type', 'volume_id', 'boot_index', 'uuid', 'extra',
                    'properties'):
            if arg in kwargs:
                volume_target[arg] = kwargs[arg]

        return self._create_request('volume/targets', volume_target)

    @base.handle_errors
    def delete_node(self, uuid):
        """Deletes a node having the specified UUID.

        :param uuid: The unique identifier of the node.
        :return: A tuple with the server response and the response body.

        """
        return self._delete_request('nodes', uuid)

    @base.handle_errors
    def delete_chassis(self, uuid):
        """Deletes a chassis having the specified UUID.

        :param uuid: The unique identifier of the chassis.
        :return: A tuple with the server response and the response body.

        """
        return self._delete_request('chassis', uuid)

    @base.handle_errors
    def delete_port(self, uuid):
        """Deletes a port having the specified UUID.

        :param uuid: The unique identifier of the port.
        :return: A tuple with the server response and the response body.

        """
        return self._delete_request('ports', uuid)

    @base.handle_errors
    def delete_portgroup(self, portgroup_ident):
        """Deletes a port group having the specified UUID or name.

        :param portgroup_ident: Name or UUID of the port group.
        :return: A tuple with the server response and the response body.
        """
        return self._delete_request('portgroups', portgroup_ident)

    @base.handle_errors
    def delete_volume_connector(self, volume_connector_ident):
        """Deletes a volume connector having the specified UUID.

        :param volume_connector_ident: UUID of the volume connector.
        :return: A tuple with the server response and the response body.
        """
        return self._delete_request('volume/connectors',
                                    volume_connector_ident)

    @base.handle_errors
    def delete_volume_target(self, volume_target_ident):
        """Deletes a volume target having the specified UUID.

        :param volume_target_ident: UUID of the volume target.
        :return: A tuple with the server response and the response body.
        """
        return self._delete_request('volume/targets', volume_target_ident)

    @base.handle_errors
    def update_node(self, uuid, patch=None, **kwargs):
        """Update the specified node.

        :param uuid: The unique identifier of the node.
        :param patch: A JSON path that sets values of the specified attributes
                      to the new ones.
        :param **kwargs: Attributes and new values for them, used only when
                         patch param is not set.
        :return: A tuple with the server response and the updated node.

        """
        node_attributes = ('properties/cpu_arch',
                           'properties/cpus',
                           'properties/local_gb',
                           'properties/memory_mb',
                           'driver',
                           'deploy_interface',
                           'instance_uuid',
                           'resource_class')
        if not patch:
            patch = self._make_patch(node_attributes, **kwargs)

        return self._patch_request('nodes', uuid, patch)

    @base.handle_errors
    def update_chassis(self, uuid, **kwargs):
        """Update the specified chassis.

        :param uuid: The unique identifier of the chassis.
        :return: A tuple with the server response and the updated chassis.

        """
        chassis_attributes = ('description',)
        patch = self._make_patch(chassis_attributes, **kwargs)

        return self._patch_request('chassis', uuid, patch)

    @base.handle_errors
    def update_port(self, uuid, patch):
        """Update the specified port.

        :param uuid: The unique identifier of the port.
        :param patch: List of dicts representing json patches.
        :return: A tuple with the server response and the updated port.

        """

        return self._patch_request('ports', uuid, patch)

    @base.handle_errors
    def update_volume_connector(self, uuid, patch):
        """Update the specified volume connector.

        :param uuid: The unique identifier of the volume connector.
        :param patch: List of dicts representing json patches. Each dict
            has keys 'path', 'op' and 'value'; to update a field.
        :return: A tuple with the server response and the updated volume
            connector.
        """

        return self._patch_request('volume/connectors', uuid, patch)

    @base.handle_errors
    def update_volume_target(self, uuid, patch):
        """Update the specified volume target.

        :param uuid: The unique identifier of the volume target.
        :param patch: List of dicts representing json patches. Each dict
            has keys 'path', 'op' and 'value'; to update a field.
        :return: A tuple with the server response and the updated volume
            target.
        """

        return self._patch_request('volume/targets', uuid, patch)

    @base.handle_errors
    def set_node_power_state(self, node_uuid, state):
        """Set power state of the specified node.

        :param node_uuid: The unique identifier of the node.
        :param state: desired state to set (on/off/reboot).

        """
        target = {'target': state}
        return self._put_request('nodes/%s/states/power' % node_uuid,
                                 target)

    @base.handle_errors
    def set_node_provision_state(self, node_uuid, state, configdrive=None,
                                 clean_steps=None):
        """Set provision state of the specified node.

        :param node_uuid: The unique identifier of the node.
        :param state: desired state to set
                (active/rebuild/deleted/inspect/manage/provide).
        :param configdrive: A gzipped, base64-encoded
            configuration drive string.
        :param clean_steps: A list with clean steps to execute.
        """
        data = {'target': state}
        # NOTE (vsaienk0): Add both here if specified, do not check anything.
        # API will return an error in case of invalid parameters.
        if configdrive is not None:
            data['configdrive'] = configdrive
        if clean_steps is not None:
            data['clean_steps'] = clean_steps
        return self._put_request('nodes/%s/states/provision' % node_uuid,
                                 data)

    @base.handle_errors
    def set_node_raid_config(self, node_uuid, target_raid_config):
        """Set raid config of the specified node.

        :param node_uuid: The unique identifier of the node.
        :param target_raid_config: desired RAID configuration of the node.
        """
        return self._put_request('nodes/%s/states/raid' % node_uuid,
                                 target_raid_config)

    @base.handle_errors
    def validate_driver_interface(self, node_uuid):
        """Get all driver interfaces of a specific node.

        :param node_uuid: Unique identifier of the node in UUID format.

        """

        uri = '{pref}/{res}/{uuid}/{postf}'.format(pref=self.uri_prefix,
                                                   res='nodes',
                                                   uuid=node_uuid,
                                                   postf='validate')

        return self._show_request('nodes', node_uuid, uri=uri)

    @base.handle_errors
    def set_node_boot_device(self, node_uuid, boot_device, persistent=False):
        """Set the boot device of the specified node.

        :param node_uuid: The unique identifier of the node.
        :param boot_device: The boot device name.
        :param persistent: Boolean value. True if the boot device will
                           persist to all future boots, False if not.
                           Default: False.

        """
        request = {'boot_device': boot_device, 'persistent': persistent}
        resp, body = self._put_request('nodes/%s/management/boot_device' %
                                       node_uuid, request)
        self.expected_success(http_client.NO_CONTENT, resp.status)
        return body

    @base.handle_errors
    def get_node_boot_device(self, node_uuid):
        """Get the current boot device of the specified node.

        :param node_uuid: The unique identifier of the node.

        """
        path = 'nodes/%s/management/boot_device' % node_uuid
        resp, body = self._list_request(path)
        self.expected_success(http_client.OK, resp.status)
        return body

    @base.handle_errors
    def get_node_supported_boot_devices(self, node_uuid):
        """Get the supported boot devices of the specified node.

        :param node_uuid: The unique identifier of the node.

        """
        path = 'nodes/%s/management/boot_device/supported' % node_uuid
        resp, body = self._list_request(path)
        self.expected_success(http_client.OK, resp.status)
        return body

    @base.handle_errors
    def get_console(self, node_uuid):
        """Get connection information about the console.

        :param node_uuid: Unique identifier of the node in UUID format.

        """

        resp, body = self._show_request('nodes/states/console', node_uuid)
        self.expected_success(http_client.OK, resp.status)
        return resp, body

    @base.handle_errors
    def set_console_mode(self, node_uuid, enabled):
        """Start and stop the node console.

        :param node_uuid: Unique identifier of the node in UUID format.
        :param enabled: Boolean value; whether to enable or disable the
                        console.

        """

        enabled = {'enabled': enabled}
        resp, body = self._put_request('nodes/%s/states/console' % node_uuid,
                                       enabled)
        self.expected_success(http_client.ACCEPTED, resp.status)
        return resp, body

    @base.handle_errors
    def vif_list(self, node_uuid, api_version=None):
        """Get list of attached VIFs.

        :param node_uuid: Unique identifier of the node in UUID format.
        :param api_version: Ironic API version to use.
        """
        extra_headers = False
        headers = None
        if api_version is not None:
            extra_headers = True
            headers = {'x-openstack-ironic-api-version': api_version}
        return self._list_request('nodes/%s/vifs' % node_uuid,
                                  headers=headers,
                                  extra_headers=extra_headers)

    @base.handle_errors
    def vif_attach(self, node_uuid, vif_id):
        """Attach a VIF to a node

        :param node_uuid: Unique identifier of the node in UUID format.
        :param vif_id: An ID representing the VIF
        """
        vif = {'id': vif_id}
        resp = self._create_request_no_response_body(
            'nodes/%s/vifs' % node_uuid, vif)

        return resp

    @base.handle_errors
    def vif_detach(self, node_uuid, vif_id):
        """Detach a VIF from a node

        :param node_uuid: Unique identifier of the node in UUID format.
        :param vif_id: An ID representing the VIF
        """
        resp, body = self._delete_request('nodes/%s/vifs' % node_uuid, vif_id)
        self.expected_success(http_client.NO_CONTENT, resp.status)
        return resp, body

    @base.handle_errors
    def get_driver_properties(self, driver_name):
        """Get properties information about driver.

        :param driver_name: Name of driver.
        :return: tuple of response and serialized properties as a dictionary.

        """
        uri = 'drivers/%s/properties' % driver_name
        resp, body = self.get(uri)
        self.expected_success(200, resp.status)
        return resp, self.deserialize(body)

    @base.handle_errors
    def get_driver_logical_disk_properties(self, driver_name):
        """Get driver logical disk properties.

        :param driver_name: Name of driver.
        :return: tuple of response and serialized logical disk properties as
                 a dictionary.

        """
        uri = 'drivers/%s/raid/logical_disk_properties' % driver_name
        resp, body = self.get(uri)
        self.expected_success(200, resp.status)
        return resp, self.deserialize(body)
