# Copyright (c) 2019 ShenZhen SandStone Data Technologies Co., Ltd.
# All Rights Reserved.
#
#    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.
"""SandStone iSCSI Driver."""

import hashlib
import json
import re
import time

from oslo_log import log as logging
import requests

from cinder import exception
from cinder.i18n import _
from cinder.volume.drivers.sandstone import constants

LOG = logging.getLogger(__name__)


class RestCmd(object):
    """Restful api class."""

    def __init__(self, address, user, password,
                 suppress_requests_ssl_warnings):
        """Init RestCmd class.

        :param address: Restapi uri.
        :param user: login web username.
        :param password: login web password.
        """
        self.address = "https://%(address)s" % {"address": address}
        self.user = user
        self.password = password
        self.pagesize = constants.PAGESIZE
        self.session = None
        self.short_wait = 10
        self.long_wait = 12000
        self.debug = True
        self._init_http_header()

    def _init_http_header(self):
        self.session = requests.Session()
        self.session.headers.update({
            "Content-Type": "application/json",
            "Connection": "keep-alive",
            "Accept-Encoding": "gzip, deflate",
        })
        self.session.verify = False

    def run(self, url, method, data=None, json_flag=True,
            filter_flag=False, om_op_flag=False):
        """Run rest cmd function.

        :param url: rest api uri resource.
        :param data: rest api uri json parameter.
        :param filter_flag: controller whether filter log. (default 'No')
        :param om_op_flag: api op have basic and om, use different prefix uri.
        """
        kwargs = {}
        if data:
            kwargs["data"] = json.dumps(data)
        if om_op_flag:
            rest_url = self.address + constants.OM_URI + url
        else:
            rest_url = self.address + constants.BASIC_URI + url

        func = getattr(self.session, method.lower())
        try:
            result = func(rest_url, **kwargs)
        except requests.RequestException as err:
            msg = _('Bad response from server: %(url)s. '
                    'Error: %(err)s') % {'url': rest_url, 'err': err}
            raise exception.VolumeBackendAPIException(msg)

        try:
            result.raise_for_status()
        except requests.HTTPError as exc:
            if exc.response.status_code == constants.CONNECT_ERROR:
                try:
                    self.login()
                except requests.ConnectTimeout as err:
                    msg = (_("Sandstone web server may be abnormal "
                             "or storage may be poweroff. Error: %(err)s")
                           % {'err': err})
                    raise exception.VolumeBackendAPIException(msg)
            else:
                return {"error": {"code": exc.response.status_code,
                                  "description": str(exc)}}

        if not filter_flag:
            LOG.info('''
            Request URL: %(url)s,
            Call Method: %(method)s,
            Request Data: %(data)s,
            Response Data: %(res)s,
            Result Data: %(res_json)s.''', {'url': url, 'method': method,
                                            'data': data, 'res': result,
                                            'res_json': result.json()})

        if json_flag:
            return result.json()
        return result

    def _assert_restapi_result(self, result, err):
        if result.get("success") != 1:
            msg = (_('%(err)s\nresult:%(res)s') % {"err": err,
                                                   "res": result})
            raise exception.VolumeBackendAPIException(data=msg)

    def login(self):
        """Login web get with token session."""
        url = 'user/login'

        sha256 = hashlib.sha256()
        sha256.update(self.password.encode("utf8"))
        password = sha256.hexdigest()

        data = {"username": self.user, "password": password}
        result = self.run(url=url, data=data, method='POST', json_flag=False,
                          om_op_flag=True)
        self._assert_restapi_result(result.json(), _('Login error.'))
        cookies = result.cookies
        set_cookie = result.headers['Set-Cookie']
        self.session.headers['Cookie'] = ';'.join(
            ['XSRF-TOKEN={}'.format(cookies['XSRF-TOKEN']),
             ' username={}'.format(self.user),
             ' sdsom_sessionid={}'.format(self._find_sessionid(set_cookie))])
        self.session.headers["Referer"] = self.address
        self.session.headers["X-XSRF-TOKEN"] = cookies["XSRF-TOKEN"]

    def _find_sessionid(self, headers):
        sessionid = re.findall("sdsom_sessionid=(\\w+);", headers)
        if sessionid:
            return sessionid[0]
        return ""

    def _check_special_result(self, result, contain):
        if result.get("success") == 0 and contain in result.get("data"):
            return True

    def logout(self):
        """Logout release resource."""
        url = 'user/logout'
        data = {"username": self.user}
        result = self.run(url, 'POST', data=data,
                          om_op_flag=True)
        self._assert_restapi_result(result, _("Logout out error."))

    def query_capacity_info(self):
        """Query cluster capacity."""
        url = 'capacity'
        capacity_info = {}

        result = self.run(url, 'POST', filter_flag=True)
        self._assert_restapi_result(result, _("Query capacity error."))
        capacity_info["capacity_bytes"] = result["data"].get(
            "capacity_bytes", 0)
        capacity_info["free_bytes"] = result["data"].get("free_bytes", 0)
        return capacity_info

    def query_pool_info(self):
        """Query use pool status."""
        url = 'pool/list'

        result = self.run(url, 'POST')
        self._assert_restapi_result(result, _("Query pool status error."))
        return result["data"]

    def get_poolid_from_poolname(self):
        """Use poolname get poolid from pool/list maps."""
        data = self.query_pool_info()
        poolname_map_poolid = {}
        if data:
            for pool in data:
                poolname_map_poolid[pool["realname"]] = pool["pool_id"]
        return poolname_map_poolid

    def create_initiator(self, initiator_name):
        """Create client iqn in storage cluster."""
        url = 'resource/initiator/create'
        data = {"iqn": initiator_name, "type": "iSCSI",
                "remark": "Cinder iSCSI"}
        result = self.run(url, 'POST', data=data)
        # initiator exist, return no err.
        if self._check_special_result(result, "already exist"):
            return
        self._assert_restapi_result(result, _("Create initiator error."))

    def _delaytask_list(self, pagesize=20):
        url = 'delaytask/list'
        data = {"pageno": 1, "pagesize": pagesize}
        return self.run(url, 'POST', data=data, om_op_flag=True)

    def _judge_delaytask_status(self, wait_time, func_name, *args):
        # wait 10 seconds for task
        func = getattr(self, func_name.lower())
        for wait in range(1, wait_time + 1):
            try:
                task_status = func(*args)
                if self.debug:
                    LOG.info(task_status)
            except exception.VolumeBackendAPIException as exc:
                msg = (_("Task: run %(task)s failed, "
                         "err: %(err)s.")
                       % {"task": func_name,
                          "err": exc})
                LOG.error(msg)
            if task_status.get('run_status') == "failed":
                msg = (_("Task : run %(task)s failed, "
                         "parameter : %(parameter)s, "
                         "progress is %(process)d.")
                       % {"task": func_name,
                          "process": task_status.get('progress'),
                          "parameter": args})
                raise exception.VolumeBackendAPIException(data=msg)
            elif task_status.get('run_status') != "completed":
                msg = (_("Task : running %(task)s , "
                         "parameter : %(parameter)s, "
                         "progress is %(process)d, "
                         "waited for 1 second, "
                         "total waited %(total)d second.")
                       % {"task": func_name,
                          "process": task_status.get('progress', 0),
                          "parameter": args,
                          "total": wait})
                LOG.info(msg)
                time.sleep(1)
            elif task_status.get('run_status') == "completed":
                msg = (_("Task : running %(task)s successfully, "
                         "parameter : %(parameter)s, "
                         "progress is %(process)d, "
                         "total spend %(total)d second.")
                       % {"task": func_name,
                          "process": task_status.get('progress'),
                          "parameter": args,
                          "total": wait})
                LOG.info(msg)
                break

    def add_initiator_to_target(self, target_name, initiator_name):
        """Bind client iqn to storage target iqn."""
        url = 'resource/target/add_initiator_to_target'
        data = {"targetName": target_name,
                "iqns": [{"ip": "", "iqn": initiator_name}]}
        result = self.run(url, 'POST', data=data)
        # wait 10 seconds to map initiator
        self._judge_delaytask_status(self.short_wait,
                                     "query_map_initiator_porcess",
                                     target_name, initiator_name)
        self._assert_restapi_result(result, _("Add initiator "
                                              "to target error."))

    def query_map_initiator_porcess(self, target_name,
                                    initiator_name):
        """Query initiator add to target process."""
        result = self._delaytask_list()
        self._assert_restapi_result(result, _("Query mapping "
                                              "initiator process error."))

        result = result["data"].get("results", None) or []
        expected_parameter = [{"target_name": target_name,
                               "iqns": [{"ip": "", "iqn": initiator_name}]}]
        task = [map_initiator_task for map_initiator_task in result
                if map_initiator_task["executor"] == "MapInitiator"
                and map_initiator_task["parameter"] == expected_parameter]
        if task:
            return task[0]
        return {}

    def query_initiator_by_name(self, initiator_name):
        """Query initiator exist or not."""
        url = 'resource/initiator/list'
        data = {"initiatorMark": "", "pageno": 1,
                "pagesize": self.pagesize, "type": "iSCSI"}
        result = self.run(url, 'POST', data=data)
        self._assert_restapi_result(result, _("Query initiator "
                                              "by name error."))

        result = result["data"].get("results", None) or []
        initiator_info = [initiator for initiator in result
                          if initiator.get("iqn", None) == initiator_name]
        if initiator_info:
            return initiator_info[0]
        return None

    def query_target_initiatoracl(self, target_name, initiator_name):
        """Query target iqn bind client iqn info."""
        url = 'resource/target/get_target_acl_list'
        data = {"pageno": 1, "pagesize": self.pagesize,
                "targetName": target_name}
        result = self.run(url, 'POST', data=data)
        self._assert_restapi_result(result, _("Query target "
                                              "initiatoracl error."))

        results = result["data"].get("results", None)
        acl_info = [acl for acl in results or []
                    if acl.get("name", None) == initiator_name]
        return acl_info or None

    def query_node_by_targetips(self, target_ips):
        """Query target ip relation with node."""
        url = 'block/gateway/server/list'
        result = self.run(url, 'POST')
        self._assert_restapi_result(result, _("Query node by "
                                              "targetips error."))

        targetip_to_hostid = {}
        for node in result["data"]:
            for node_access_ip in node.get("networks"):
                goal_ip = node_access_ip.get("address")
                if goal_ip in target_ips:
                    targetip_to_hostid[goal_ip] =\
                        node_access_ip.get("hostid", None)
        return targetip_to_hostid

    def query_target_by_name(self, target_name):
        """Query target iqn exist or not."""
        url = 'resource/target/list'
        data = {"pageno": 1, "pagesize": self.pagesize,
                "thirdParty": [0, 1], "targetMark": ""}
        result = self.run(url, 'POST', data=data)
        self._assert_restapi_result(result, _("Query target by name error."))

        result = result["data"].get("results", None) or []
        target_info = [target for target in result
                       if target.get("name", None) == target_name]
        if target_info:
            return target_info[0]
        return None

    def create_target(self, target_name, targetip_to_hostid):
        """Create target iqn."""
        url = 'resource/target/create'
        data = {"type": "iSCSI", "readOnly": 0,
                "thirdParty": 1, "targetName": target_name,
                "networks": [{"hostid": host_id, "address": address}
                             for address, host_id in
                             targetip_to_hostid.items()]}
        result = self.run(url, 'POST', data=data)
        # target exist, return no err.
        if self._check_special_result(result, "already exist"):
            return
        self._assert_restapi_result(result, _("Create target error."))

    def add_chap_by_target(self, target_name, username, password):
        """Add chap to target, only support forward."""
        url = 'resource/target/add_chap'
        data = {"password": password,
                "user": username, "targetName": target_name}
        result = self.run(url, 'POST', data=data)
        self._assert_restapi_result(result, _("Add chap by target error."))

    def query_chapinfo_by_target(self, target_name, username):
        """Query chapinfo by target, check chap add target or not."""
        url = 'resource/target/get_chap_list'
        data = {"targetName": target_name}
        result = self.run(url, 'POST', data=data)
        self._assert_restapi_result(result, _("Query chapinfo "
                                              "by target error."))

        result = result.get('data') or []
        chapinfo = [c for c in result if c.get("user") == username]
        if chapinfo:
            return chapinfo[0]
        return None

    def create_lun(self, capacity_bytes, poolid, volume_name):
        """Create lun resource."""
        url = 'resource/lun/add'
        data = {"capacity_bytes": capacity_bytes,
                "poolId": poolid, "priority": "normal",
                "qosSettings": {}, "volumeName": volume_name}
        result = self.run(url, 'POST', data=data)
        self._assert_restapi_result(result, _("Create lun error."))

    def delete_lun(self, poolid, volume_name):
        """Delete lun resource."""
        url = 'resource/lun/batch_delete'
        data = {"delayTime": 0, "volumeNameList": [{
            "poolId": poolid,
            "volumeName": volume_name}]}
        result = self.run(url, 'POST', data=data)
        # lun deleted, return no err.
        if self._check_special_result(result, "not found"):
            return
        self._assert_restapi_result(result, _("Delete lun error."))

    def extend_lun(self, capacity_bytes, poolid, volume_name):
        """Extend lun, only support enlarge."""
        url = 'resource/lun/resize'

        data = {"capacity_bytes": capacity_bytes,
                "poolId": poolid,
                "volumeName": volume_name}
        result = self.run(url, 'POST', data=data)
        self._assert_restapi_result(result, _("Extend lun error."))

    def unmap_lun(self, target_name, poolid, volume_name, pool_name):
        """Unbind lun from target iqn."""
        url = 'resource/target/unmap_luns'
        volume_info = self.query_lun_by_name(volume_name, poolid)
        result = {"success": 0}
        if volume_info:
            uuid = volume_info.get("uuid", None)
            data = {"targetName": target_name,
                    "targetLunList": [uuid],
                    "targetSnapList": []}
            result = self.run(url, 'POST', data=data)
            # lun unmaped, return no err.
            if self._check_special_result(result, "not mapped"):
                return
            # wait for 10 seconds to unmap lun.
            self._judge_delaytask_status(self.short_wait,
                                         "query_unmapping_lun_porcess",
                                         target_name, volume_name,
                                         uuid, pool_name)
            self._assert_restapi_result(result, _("Unmap lun error."))
        else:
            self._assert_restapi_result(result,
                                        _("Unmap lun error, uuid is None."))

    def mapping_lun(self, target_name, poolid, volume_name, pool_name):
        """Bind lun to target iqn."""
        url = 'resource/target/map_luns'
        volume_info = self.query_lun_by_name(volume_name, poolid)
        result = {"success": 0}
        if volume_info:
            uuid = volume_info.get("uuid", None)
            data = {"targetName": target_name,
                    "targetLunList": [uuid],
                    "targetSnapList": []}
            result = self.run(url, 'POST', data=data)
            # lun maped, return no err.
            if self._check_special_result(result, "already mapped"):
                return
            # wait for 10 seconds to map lun.
            self._judge_delaytask_status(self.short_wait,
                                         "query_mapping_lun_porcess",
                                         target_name, volume_name,
                                         uuid, pool_name)
            self._assert_restapi_result(result, _("Map lun error."))
        else:
            self._assert_restapi_result(result,
                                        _("Map lun error, uuid is None."))

    def query_mapping_lun_porcess(self, target_name, volume_name,
                                  uuid, pool_name):
        """Query mapping lun process."""
        result = self._delaytask_list()
        self._assert_restapi_result(result, _("Query mapping "
                                              "lun process error."))

        expected_parameter = {"target_name": target_name,
                              "image_id": uuid,
                              "target_realname": target_name,
                              "meta_pool": pool_name,
                              "image_realname": volume_name}
        result = result["data"].get("results", None) or []
        task = [map_initiator_task for map_initiator_task in result
                if map_initiator_task["executor"] == "TargetMap"
                and map_initiator_task["parameter"] == expected_parameter]
        if task:
            return task[0]
        return {}

    def query_unmapping_lun_porcess(self, target_name, volume_name,
                                    uuid, pool_name):
        """Query mapping lun process."""
        result = self._delaytask_list()
        self._assert_restapi_result(result, _("Query mapping "
                                              "lun process error."))

        expected_parameter = {"target_name": target_name,
                              "image_id": uuid,
                              "target_realname": target_name,
                              "meta_pool": pool_name,
                              "image_name": volume_name}
        result = result["data"].get("results", None) or []
        task = [map_initiator_task for map_initiator_task in result
                if map_initiator_task["executor"] == "TargetUnmap"
                and map_initiator_task["parameter"] == expected_parameter]
        if task:
            return task[0]
        return {}

    def query_target_lunacl(self, target_name, poolid, volume_name):
        """Query target iqn relation with lun."""
        url = 'resource/target/get_luns'
        data = {"pageno": 1, "pagesize": self.pagesize,
                "pools": [poolid], "targetName": target_name}
        result = self.run(url, 'POST', data=data)
        self._assert_restapi_result(result, _("Query target lunacl error."))

        # target get_luns use results
        result = result["data"].get("results", None) or []
        lunid = [volume.get("lid", None) for volume in result
                 if volume.get("name", None) == volume_name
                 and volume.get("pool_id") == poolid]
        if lunid:
            return lunid[0]
        return None

    def query_lun_by_name(self, volume_name, poolid):
        """Query lun exist or not."""
        url = 'resource/lun/list'
        data = {"pageno": 1, "pagesize": self.pagesize,
                "volumeMark": volume_name,
                "sortType": "time", "sortOrder": "desc",
                "pools": [poolid], "thirdParty": [0, 1]}
        result = self.run(url, 'POST', data=data)
        self._assert_restapi_result(result, _("Query lun by name error."))

        result = result["data"].get("results", None) or []
        volume_info = [volume for volume in result
                       if volume.get("volumeName", None) == volume_name]
        if volume_info:
            return volume_info[0]
        return None

    def query_target_by_lun(self, volume_name, poolid):
        """Query lun already mapped target name."""
        url = "resource/lun/targets"
        data = {"poolId": poolid, "volumeName": volume_name}
        result = self.run(url, 'POST', data=data)
        self._assert_restapi_result(result, _("Query target by lun error."))

        data = result["data"]
        target_name = data[0].get("name", None)
        return target_name

    def create_snapshot(self, poolid, volume_name, snapshot_name):
        """Create lun snapshot."""
        url = 'resource/snapshot/add'
        data = {"lunName": volume_name,
                "poolId": poolid,
                "remark": "Cinder iSCSI snapshot.",
                "snapName": snapshot_name}
        result = self.run(url, 'POST', data=data)
        # snapshot existed, return no err.
        if self._check_special_result(result, "has exists"):
            return
        # wait for 10 seconds to create snapshot
        self._judge_delaytask_status(self.short_wait,
                                     "query_create_snapshot_process",
                                     poolid, volume_name, snapshot_name)
        self._assert_restapi_result(result, _("Create snapshot error."))

    def query_create_snapshot_process(self, poolid,
                                      volume_name, snapshot_name):
        """Query create snapshot process."""
        result = self._delaytask_list()
        self._assert_restapi_result(result, _("Query flatten "
                                              "lun process error."))
        result = result["data"].get("results", None) or []
        task = [flatten_task for flatten_task in result
                if flatten_task["executor"] == "SnapCreate"
                and flatten_task["parameter"].get("pool_id", None)
                == poolid
                and flatten_task["parameter"].get("snap_name", None)
                == snapshot_name
                and flatten_task["parameter"].get("lun_name", None)
                == volume_name]

        if task:
            return task[0]
        return {}

    def delete_snapshot(self, poolid, volume_name, snapshot_name):
        """Delete lun snapshot."""
        url = 'resource/snapshot/delete'
        data = {"lunName": volume_name,
                "poolId": poolid, "snapName": snapshot_name}
        result = self.run(url, 'POST', data=data)
        # snapshot deleted, need return no err.
        if self._check_special_result(result, "not found"):
            return
        # wait for 10 seconds to delete snapshot
        self._judge_delaytask_status(self.short_wait,
                                     "query_delete_snapshot_process",
                                     poolid, volume_name, snapshot_name)
        self._assert_restapi_result(result, _("Delete snapshot error."))

    def query_delete_snapshot_process(self, poolid,
                                      volume_name, snapshot_name):
        """Query delete snapshot process."""
        result = self._delaytask_list()
        self._assert_restapi_result(result, _("Query delete "
                                              "snapshot process error."))
        result = result["data"].get("results", None) or []
        task = [flatten_task for flatten_task in result
                if flatten_task["executor"] == "SnapDelete"
                and flatten_task["parameter"].get("pool_id", None)
                == poolid
                and flatten_task["parameter"].get("snap_name", None)
                == snapshot_name
                and flatten_task["parameter"].get("lun_name", None)
                == volume_name]

        if task:
            return task[0]
        return {}

    def create_lun_from_snapshot(self, snapshot_name, src_volume_name,
                                 poolid, dst_volume_name):
        """Create lun from source lun snapshot."""
        url = 'resource/snapshot/clone'
        data = {"snapshot": {"poolId": poolid,
                             "lunName": src_volume_name,
                             "snapName": snapshot_name},
                "cloneLun": {"lunName": dst_volume_name,
                             "poolId": poolid}}
        result = self.run(url, 'POST', data=data)
        # clone volume exsited, return no err.
        if self._check_special_result(result, "already exists"):
            return
        # wait for 10 seconds to clone lun
        self._judge_delaytask_status(self.short_wait,
                                     "query_clone_lun_process",
                                     poolid, src_volume_name, snapshot_name)
        self._assert_restapi_result(result, _("Create lun "
                                              "from snapshot error."))
        self.flatten_lun(dst_volume_name, poolid)

    def query_clone_lun_process(self, poolid, volume_name, snapshot_name):
        """Query clone lun process."""
        result = self._delaytask_list()
        self._assert_restapi_result(result, _("Query flatten "
                                              "lun process error."))
        result = result["data"].get("results", None) or []
        task = [flatten_task for flatten_task in result
                if flatten_task["executor"] == "SnapClone"
                and flatten_task["parameter"].get("pool_id", None)
                == poolid
                and flatten_task["parameter"].get("snap_name", None)
                == snapshot_name
                and flatten_task["parameter"].get("lun_name", None)
                == volume_name]

        if task:
            return task[0]
        return {}

    def flatten_lun(self, volume_name, poolid):
        """Flatten lun."""
        url = 'resource/lun/flatten'
        data = {"poolId": poolid,
                "volumeName": volume_name}
        result = self.run(url, 'POST', data=data)
        # volume flattened, return no err.
        if self._check_special_result(result, "not need flatten"):
            return
        # wait for longest 200 min to flatten
        self._judge_delaytask_status(self.long_wait,
                                     "query_flatten_lun_process",
                                     poolid, volume_name)
        self._assert_restapi_result(result, _("Flatten lun error."))

    def query_flatten_lun_process(self, poolid, volume_name):
        """Query flatten lun process."""
        result = self._delaytask_list()
        self._assert_restapi_result(result, _("Query flatten "
                                              "lun process error."))
        result = result["data"].get("results", None) or []
        task = [flatten_task for flatten_task in result
                if flatten_task["executor"] == "LunFlatten"
                and flatten_task["parameter"].get("pool_id", None)
                == poolid
                and flatten_task["parameter"].get("lun_name", None)
                == volume_name]
        if task:
            return task[0]
        return {}

    def create_lun_from_lun(self, dst_volume_name, poolid, src_volume_name):
        """Clone lun from source lun."""
        tmp_snapshot_name = 'temp' + src_volume_name + 'clone' +\
                            dst_volume_name
        self.create_snapshot(poolid, src_volume_name, tmp_snapshot_name)
        self.create_lun_from_snapshot(tmp_snapshot_name, src_volume_name,
                                      poolid, dst_volume_name)
        self.flatten_lun(dst_volume_name, poolid)

        self.delete_snapshot(poolid, src_volume_name, tmp_snapshot_name)

    def query_snapshot_by_name(self, volume_name, poolid, snapshot_name):
        """Query snapshot exist or not."""
        url = 'resource/snapshot/list'
        data = {"lunName": volume_name, "pageno": 1,
                "pagesize": self.pagesize, "poolId": poolid,
                "snapMark": ""}
        result = self.run(url, 'POST', data=data)
        self._assert_restapi_result(result, _("Query snapshot by name error."))

        result = result["data"].get("results", None) or []
        snapshot_info = [snapshot for snapshot in result
                         if snapshot.get("snapName", None) ==
                         snapshot_name]
        return snapshot_info
