# Copyright 2014 Huawei Technologies Co. Ltd
#
# 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.

"""Compass api client library.

   .. moduleauthor:: Xiaodong Wang <xiaodongwang@huawei.com>
"""
import json
import logging
import requests


class Client(object):
    """wrapper for compass restful api.

    .. note::
       Every api client method returns (status as int, resp as dict).
       If the api succeeds, the status is 2xx, the resp includes
       {'status': 'OK'} and other keys depend on method.
       If the api fails, the status is 4xx, the resp includes {
       'status': '...', 'message': '...'}
    """

    def __init__(self, url, headers=None, proxies=None, stream=None):
        """Restful api client initialization.

        :param url: url to the compass web service.
        :type url: str.
        :param headers: http header sent in each restful request.
        :type headers: dict of header name (str) to heade value (str).
        :param proxies: the proxy address for each protocol.
        :type proxies: dict of protocol (str) to proxy url (str).
        :param stream: wether the restful response should be streamed.
        :type stream: bool.
        """
        self.url_ = url
        self.session_ = requests.Session()
        if headers:
            self.session_.headers = headers

        if proxies is not None:
            self.session_.proxies = proxies

        if stream is not None:
            self.session_.stream = stream

    def __del__(self):
        self.session_.close()

    @classmethod
    def _get_response(cls, resp):
        """decapsulate the resp to status code and python formatted data."""
        resp_obj = {}
        try:
            resp_obj = resp.json()
        except Exception as error:
            logging.error('failed to load object from %s: %s',
                          resp.url, resp.content)
            logging.exception(error)
            resp_obj['status'] = 'Json Parsing Failure'
            resp_obj['message'] = resp.content

        return resp.status_code, resp_obj

    def _get(self, relative_url, params=None):
        """encapsulate get method."""
        url = '%s%s' % (self.url_, relative_url)
        if params:
            resp = self.session_.get(url, params=params)
        else:
            resp = self.session_.get(url)

        return self._get_response(resp)

    def _post(self, relative_url, data=None):
        """encapsulate post method."""
        url = '%s%s' % (self.url_, relative_url)
        if data:
            resp = self.session_.post(url, json.dumps(data))
        else:
            resp = self.session_.post(url)

        return self._get_response(resp)

    def _put(self, relative_url, data=None):
        """encapsulate put method."""
        url = '%s%s' % (self.url_, relative_url)
        if data:
            resp = self.session_.put(url, json.dumps(data))
        else:
            resp = self.session_.put(url)

        return self._get_response(resp)

    def _delete(self, relative_url):
        """encapsulate delete method."""
        url = '%s%s' % (self.url_, relative_url)
        return self._get_response(self.session_.delete(url))

    def get_switches(self, switch_ips=None, switch_networks=None, limit=None):
        """List details for switches.

        .. note::
           The switches can be filtered by switch_ips, siwtch_networks and
           limit. These params can be None or missing. If the param is None
           or missing, that filter will be ignored.

        :param switch_ips: Filter switch(es) with IP(s).
        :type switch_ips: list of str. Each is as 'xxx.xxx.xxx.xxx'.
        :param switch_networks: Filter switche(es) with network(s).
        :type switch_networks: list of str. Each is as 'xxx.xxx.xxx.xxx/xx'.
        :param limit: int, The maximum number of switches to return.
        :type limit: int. 0 means unlimited.
        """
        params = {}
        if switch_ips:
            params['switchIp'] = switch_ips

        if switch_networks:
            params['switchIpNetwork'] = switch_networks

        if limit:
            params['limit'] = limit
        return self._get('/switches', params=params)

    def get_switch(self, switch_id):
        """Lists details for a specified switch.

        :param switch_id: switch id.
        :type switch_id: int.
        """
        return self._get('/switches/%s' % switch_id)

    def add_switch(self, switch_ip, version=None, community=None,
                   username=None, password=None, raw_data=None):
        """Create a switch with specified details.

        .. note::
           It will trigger switch polling if successful. During
           the polling, MAC address of the devices connected to the
           switch will be learned by SNMP or SSH.

        :param switch_ip: the switch IP address.
        :type switch_ip: str, as xxx.xxx.xxx.xxx.
        :param version: SNMP version when using SNMP to poll switch.
        :type version: str, one in ['v1', 'v2c', 'v3']
        :param community: SNMP community when using SNMP to poll switch.
        :type community: str, usually 'public'.
        :param username: SSH username when using SSH to poll switch.
        :type username: str.
        :param password: SSH password when using SSH to poll switch.
        :type password: str.
        """
        data = {}
        if raw_data:
            data = raw_data
        else:
            data['switch'] = {}
            data['switch']['ip'] = switch_ip
            data['switch']['credential'] = {}
            if version:
                data['switch']['credential']['version'] = version

            if community:
                data['switch']['credential']['community'] = community

            if username:
                data['switch']['credential']['username'] = username

            if password:
                data['switch']['credential']['password'] = password

        return self._post('/switches', data=data)

    def update_switch(self, switch_id, ip_addr=None,
                      version=None, community=None,
                      username=None, password=None,
                      raw_data=None):
        """Updates a switch with specified details.

        .. note::
           It will trigger switch polling if successful. During
           the polling, MAC address of the devices connected to the
           switch will be learned by SNMP or SSH.

        :param switch_id: switch id
        :type switch_id: int.
        :param ip_addr: the switch ip address.
        :type ip_addr: str, as 'xxx.xxx.xxx.xxx' format.
        :param version: SNMP version when using SNMP to poll switch.
        :type version: str, one in ['v1', 'v2c', 'v3'].
        :param community: SNMP community when using SNMP to poll switch.
        :type community: str, usually be 'public'.
        :param username: username when using SSH to poll switch.
        :type username: str.
        :param password: password when using SSH to poll switch.
        """
        data = {}
        if raw_data:
            data = raw_data
        else:
            data['switch'] = {}
            if ip_addr:
                data['switch']['ip'] = ip_addr

            data['switch']['credential'] = {}
            if version:
                data['switch']['credential']['version'] = version

            if community:
                data['switch']['credential']['community'] = community

            if username:
                data['switch']['credential']['username'] = username

            if password:
                data['switch']['credential']['password'] = password

        return self._put('/switches/%s' % switch_id, data=data)

    def delete_switch(self, switch_id):
        """Not implemented in api."""
        return self._delete('/switches/%s' % switch_id)

    def get_machines(self, switch_id=None, vlan_id=None,
                     port=None, limit=None):
        """Get the details of machines.

        .. note::
           The machines can be filtered by switch_id, vlan_id, port
           and limit. These params can be None or missing. If the param
           is None or missing, the filter will be ignored.

        :param switch_id: Return machine(s) connected to the switch.
        :type switch_id: int.
        :param vlan_id: Return machine(s) belonging to the vlan.
        :type vlan_id: int.
        :param port: Return machine(s) connect to the port.
        :type port: int.
        :param limit: the maximum number of machines will be returned.
        :type limit: int. 0 means no limit.
        """
        params = {}
        if switch_id:
            params['switchId'] = switch_id

        if vlan_id:
            params['vlanId'] = vlan_id

        if port:
            params['port'] = port

        if limit:
            params['limit'] = limit

        return self._get('/machines', params=params)

    def get_machine(self, machine_id):
        """Lists the details for a specified machine.

        :param machine_id: Return machine with the id.
        :type machine_id: int.
        """
        return self._get('/machines/%s' % machine_id)

    def get_clusters(self):
        """Lists the details for all clusters.
        """
        return self._get('/clusters')

    def get_cluster(self, cluster_id):
        """Lists the details of the specified cluster.

        :param cluster_id: cluster id.
        :type cluster_id: int.
        """
        return self._get('/clusters/%d' % cluster_id)

    def add_cluster(self, cluster_name, adapter_id, raw_data=None):
        """Creates a cluster by specified name and given adapter id.

        :param cluster_name: cluster name.
        :type cluster_name: str.
        :param adapter_id: adapter id.
        :type adapter_id: int.
        """
        data = {}
        if raw_data:
            data = raw_data
        else:
            data['cluster'] = {}
            data['cluster']['name'] = cluster_name
            data['cluster']['adapter_id'] = adapter_id
        return self._post('/clusters', data=data)

    def add_hosts(self, cluster_id, machine_ids, raw_data=None):
        """add the specified machine(s) as the host(s) to the cluster.

        :param cluster_id: cluster id.
        :type cluster_id: int.
        :param machine_ids: machine ids to add to cluster.
        :type machine_ids: list of int, each is the id of one machine.
        """
        data = {}
        if raw_data:
            data = raw_data
        else:
            data['addHosts'] = machine_ids
        return self._post('/clusters/%d/action' % cluster_id, data=data)

    def remove_hosts(self, cluster_id, host_ids, raw_data=None):
        """remove the specified host(s) from the cluster.

        :param cluster_id: cluster id.
        :type cluster_id: int.
        :param host_ids: host ids to remove from cluster.
        :type host_ids: list of int, each is the id of one host.
        """
        data = {}
        if raw_data:
            data = raw_data
        else:
            data['removeHosts'] = host_ids
        return self._post('/clusters/%s/action' % cluster_id, data=data)

    def replace_hosts(self, cluster_id, machine_ids, raw_data=None):
        """replace the cluster hosts with the specified machine(s).

        :param cluster_id: int, The unique identifier of the cluster.
        :type cluster_id: int.
        :param machine_ids: the machine ids to replace the hosts in cluster.
        :type machine_ids: list of int, each is the id of one machine.
        """
        data = {}
        if raw_data:
            data = raw_data
        else:
            data['replaceAllHosts'] = machine_ids
        return self._post('/clusters/%s/action' % cluster_id, data=data)

    def deploy_hosts(self, cluster_id, raw_data=None):
        """Deploy the cluster.

        :param cluster_id: The unique identifier of the cluster
        :type cluster_id: int.
        """
        data = {}
        if raw_data:
            data = raw_data
        else:
            data['deploy'] = []
        return self._post('/clusters/%d/action' % cluster_id, data=data)

    @classmethod
    def parse_security(cls, kwargs):
        """parse the arguments to security data."""
        data = {}
        for key, value in kwargs.items():
            if '_' not in key:
                continue
            key_name, key_value = key.split('_', 1)
            data.setdefault(
                '%s_credentials' % key_name, {})[key_value] = value

        return data

    def set_security(self, cluster_id, **kwargs):
        """Update the cluster security configuration.

        :param cluster_id: cluster id.
        :type cluster_id: int.
        :param <security_name>_username: username of the security name.
        :type <security_name>_username: str.
        :param <security_name>_password: passowrd of the security name.
        :type <security_name>_password: str.

        .. note::
           security_name should be one of ['server', 'service', 'console'].
        """
        data = {}
        data['security'] = self.parse_security(kwargs)
        return self._put('/clusters/%d/security' % cluster_id, data=data)

    @classmethod
    def parse_networking(cls, kwargs):
        """parse arguments to network data."""
        data = {}
        global_keys = [
            'nameservers', 'search_path', 'gateway',
            'proxy', 'ntp_server', 'ha_vip']
        for key, value in kwargs.items():
            if key in global_keys:
                data.setdefault('global', {})[key] = value
            else:
                if '_' not in key:
                    continue

                key_name, key_value = key.split('_', 1)
                data.setdefault(
                    'interfaces', {}
                ).setdefault(
                    key_name, {}
                )[key_value] = value

        return data

    def set_networking(self, cluster_id, **kwargs):
        """Update the cluster network configuration.

        :param cluster_id: cluster id.
        :type cluster_id: int.
        :param nameservers: comma seperated nameserver ip address.
        :type nameservers: str.
        :param search_path: comma seperated dns name search path.
        :type search_path: str.
        :param gateway: gateway ip address for routing to outside.
        :type gateway: str.
        :param proxy: proxy url for downloading packages.
        :type proxy: str.
        :param ntp_server: ntp server ip address to sync timestamp.
        :type ntp_server: str.
        :param ha_vip: ha vip address to run ha proxy.
        :type ha_vip: str.
        :param <interface>_ip_start: start ip address to host's interface.
        :type <interface>_ip_start: str.
        :param <interface>_ip_end: end ip address to host's interface.
        :type <interface>_ip_end: str.
        :param <interface>_netmask: netmask to host's interface.
        :type <interface>_netmask: str.
        :param <interface>_nic: host physical interface name.
        :type <interface>_nic: str.
        :param <interface>_promisc: if the interface in promiscous mode.
        :type <interface>_promisc: int, 0 or 1.

        .. note::
           interface should be one of ['management', 'tenant',
           'public', 'storage'].
        """
        data = {}
        data['networking'] = self.parse_networking(kwargs)
        return self._put('/clusters/%d/networking' % cluster_id, data=data)

    @classmethod
    def parse_partition(cls, kwargs):
        """parse arguments to partition data."""
        data = {}
        for key, value in kwargs.items():
            if key.endswith('_percentage'):
                key_name = key[:-len('_percentage')]
                data[key_name] = '%s%%' % value
            elif key.endswitch('_mbytes'):
                key_name = key[:-len('_mbytes')]
                data[key_name] = str(value)

        return ';'.join([
            '/%s %s' % (key, value) for key, value in data.items()
        ])

    def set_partition(self, cluster_id, **kwargs):
        """Update the cluster partition configuration.

        :param cluster_id: cluster id.
        :type cluster_id: int.
        :param <partition>_percentage: the partiton percentage.
        :type <partition>_percentage: float between 0 to 100.
        :param <partition>_mbytes: the partition mbytes.
        :type <partition>_mbytes: int.

        .. note::
           partition should be one of ['home', 'var', 'tmp'].
        """
        data = {}
        data['partition'] = self.parse_partition(kwargs)
        return self._put('/clusters/%s/partition' % cluster_id, data=data)

    def get_hosts(self, hostname=None, clustername=None):
        """Lists the details of hosts.

        .. note::
           The hosts can be filtered by hostname, clustername.
           These params can be None or missing. If the param
           is None or missing, the filter will be ignored.

        :param hostname: The name of a host.
        :type hostname: str.
        :param clustername: The name of a cluster.
        :type clustername: str.
        """
        params = {}
        if hostname:
            params['hostname'] = hostname

        if clustername:
            params['clustername'] = clustername

        return self._get('/clusterhosts', params=params)

    def get_host(self, host_id):
        """Lists the details for the specified host.

        :param host_id: host id.
        :type host_id: int.
        """
        return self._get('/clusterhosts/%s' % host_id)

    def get_host_config(self, host_id):