# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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 requests
import urllib
import base64
import hmac
import hashlib
import time
from cloudstackAPI import queryAsyncJobResult
import jsonHelper
from marvin.codes import (
    FAILED,
    JOB_FAILED,
    JOB_CANCELLED,
    JOB_SUCCEEDED
)
from marvin.cloudstackException import (
    InvalidParameterException,
    GetDetailExceptionInfo)


class CSConnection(object):

    '''
    @Desc: Connection Class to make API\Command calls to the
           CloudStack Management Server
           Sends the GET\POST requests to CS based upon the
           information provided and retrieves the parsed response.
    '''

    def __init__(self, mgmtDet, asyncTimeout=3600, logger=None,
                 path='client/api'):
        self.apiKey = mgmtDet.apiKey
        self.securityKey = mgmtDet.securityKey
        self.mgtSvr = mgmtDet.mgtSvrIp
        self.port = mgmtDet.port
        self.user = mgmtDet.user
        self.passwd = mgmtDet.passwd
        self.certPath = ()
        if mgmtDet.certCAPath != "NA" and mgmtDet.certPath != "NA":
            self.certPath = (mgmtDet.certCAPath, mgmtDet.certPath)
        self.logger = logger
        self.path = path
        self.retries = 5
        self.__lastError = ''
        self.mgtDetails = mgmtDet
        self.asyncTimeout = asyncTimeout
        self.auth = True
        if self.port == 8096 or \
           (self.apiKey is None and self.securityKey is None):
            self.auth = False
        self.protocol = "https" if mgmtDet.useHttps == "True" else "http"
        self.httpsFlag = True if self.protocol == "https" else False
        self.baseUrl = "%s://%s:%d/%s"\
                       % (self.protocol, self.mgtSvr, self.port, self.path)

    def __copy__(self):
        return CSConnection(self.mgtDetails,
                            self.asyncTimeout,
                            self.logger,
                            self.path)

    def __poll(self, jobid, response_cmd):
        '''
        @Name : __poll
        @Desc: polls for the completion of a given jobid
        @Input 1. jobid: Monitor the Jobid for CS
               2. response_cmd:response command for request cmd
        @return: FAILED if jobid is cancelled,failed
                 Else return async_response
        '''
        try:
            cmd = queryAsyncJobResult.queryAsyncJobResultCmd()
            cmd.jobid = jobid
            timeout = self.asyncTimeout
            start_time = time.time()
            end_time = time.time()
            async_response = FAILED
            self.logger.debug("=== Jobid: %s Started ===" % (str(jobid)))
            while timeout > 0:
                async_response = self.\
                    marvinRequest(cmd, response_type=response_cmd)
                if async_response != FAILED:
                    job_status = async_response.jobstatus
                    if job_status in [JOB_CANCELLED,
                                      JOB_SUCCEEDED]:
                        break
                    elif job_status == JOB_FAILED:
                        raise Exception("Job failed: %s"\
                                         % async_response)
                time.sleep(5)
                timeout -= 5
                self.logger.debug("=== JobId:%s is Still Processing, "
                                  "Will TimeOut in:%s ====" % (str(jobid),
                                                               str(timeout)))
            end_time = time.time()
            tot_time = int(start_time - end_time)
            self.logger.debug(
                "===Jobid:%s ; StartTime:%s ; EndTime:%s ; "
                "TotalTime:%s===" %
                (str(jobid), str(time.ctime(start_time)),
                 str(time.ctime(end_time)), str(tot_time)))
            return async_response
        except Exception as e:
            self.__lastError = e
            self.logger.exception("==== __poll: Exception Occurred :%s ====" %
                                  str(self.__lastError))
            return FAILED

    def getLastError(self):
        '''
        @Name : getLastError
        @Desc : Returns the last error from marvinRequest
        '''
        return self.__lastError

    def __sign(self, payload):
        """
        @Name : __sign
        @Desc:signs a given request URL when the apiKey and
              secretKey are known
        @Input: payload: dictionary of params be signed
        @Output: the signature of the payload
        """
        params = zip(payload.keys(), payload.values())
        params.sort(key=lambda k: str.lower(k[0]))
        hash_str = "&".join(
            ["=".join(
                [str.lower(r[0]),
                 str.lower(
                     urllib.quote_plus(str(r[1]))
                ).replace("+", "%20")]
            ) for r in params]
        )
        signature = base64.encodestring(hmac.new(
            self.securityKey, hash_str, hashlib.sha1).digest()).strip()
        return signature

    def __sendPostReqToCS(self, url, payload):
        '''
        @Name : __sendPostReqToCS
        @Desc : Sends the POST Request to CS
        @Input : url: URL to send post req
                 payload:Payload information as part of request
        @Output: Returns response from POST output
                 else FAILED
        '''
        try:
            response = requests.post(url,
                                     params=payload,
                                     cert=self.certPath,
                                     verify=self.httpsFlag)
            return response
        except Exception as e:
            self.__lastError = e
            self.logger.\
                exception("__sendPostReqToCS : Exception "
                          "Occurred: %s" % str(self.__lastError))
            return FAILED

    def __sendGetReqToCS(self, url, payload):
        '''
        @Name : __sendGetReqToCS
        @Desc : Sends the GET Request to CS
        @Input : url: URL to send post req
                 payload:Payload information as part of request
        @Output: Returns response from GET output
                 else FAILED
        '''
        try:
            response = requests.get(url,
                                    params=payload,
                                    cert=self.certPath,
                                    verify=self.httpsFlag)
            return response
        except Exception as e:
            self.__lastError = e
            self.logger.exception("__sendGetReqToCS : Exception Occurred: %s" %
                                  str(self.__lastError))
            return FAILED

    def __sendCmdToCS(self, command, auth=True, payload={}, method='GET'):
        """
        @Name : __sendCmdToCS
        @Desc : Makes requests to CS using the Inputs provided
        @Input: command: cloudstack API command name
                    eg: deployVirtualMachineCommand
                auth: Authentication (apikey,secretKey) => True
                     else False for integration.api.port
                payload: request data composed as a dictionary
                method: GET/POST via HTTP
        @output: FAILED or else response from CS
        """
        try:
            payload["command"] = command
            payload["response"] = "json"

            if auth:
                payload["apiKey"] = self.apiKey
                payload["signature"] = self.__sign(payload)

            # Verify whether protocol is "http" or "https", then send the
            # request
            if self.protocol in ["http", "https"]:
                self.logger.debug("Payload: %s" % str(payload))
                if method == 'POST':
                    self.logger.debug("=======Sending POST Cmd : %s======="
                                      % str(command))
                    return self.__sendPostReqToCS(self.baseUrl, payload)
                if method == "GET":
                    self.logger.debug("========Sending GET Cmd : %s======="
                                      % str(command))
                    return self.__sendGetReqToCS(self.baseUrl, payload)
            else:
                self.logger.exception("__sendCmdToCS: Invalid Protocol")
                return FAILED
        except Exception as e:
            self.__lastError = e
            self.logger.exception("__sendCmdToCS: Exception:%s" %
                                  GetDetailExceptionInfo(e))
            return FAILED

    def __sanitizeCmd(self, cmd):
        """
        @Name : __sanitizeCmd
        @Desc : Removes None values, Validates all required params are present
        @Input: cmd: Cmd object eg: createPhysicalNetwork
        @Output: Returns command name, asynchronous or not,request payload
                 FAILED for failed cases
        """
        try:
            cmd_name = ''
            payload = {}
            required = []
            isAsync = "false"
            for attribute in dir(cmd):
                if not attribute.startswith('__'):
                    if attribute == "isAsync":
                        isAsync = getattr(cmd, attribute)
                    elif attribute == "required":
                        required = getattr(cmd, attribute)
                    else:
                        payload[attribute] = getattr(cmd, attribute)
            cmd_name = cmd.__class__.__name__.replace("Cmd", "")
            for required_param in required:
                if payload[required_param] is None:
                    self.logger.debug("CmdName: %s Parameter : %s is Required"
                                      % (cmd_name, required_param))
                    self.__lastError = InvalidParameterException(
                        "Invalid Parameters")
                    return FAILED
            for param, value in payload.items():
                if value is None:
                    payload.pop(param)
                elif param == 'typeInfo':
                    payload.pop(param)
                elif isinstance(value, list):
                    if len(value) == 0:
                        payload.pop(param)
                    else:
                        if not isinstance(value[0], dict):
                            payload[param] = ",".join(value)
                        else:
                            payload.pop(param)
                            i = 0
                            for val in value:
                                for k, v in val.iteritems():
                                    payload["%s[%d].%s" % (param, i, k)] = v
                                i += 1
            return cmd_name.strip(), isAsync, payload
        except Exception as e:
            self.__lastError = e
            self.logger.\
                exception("__sanitizeCmd: CmdName : "
                          "%s : Exception:%s" % (cmd_name,
                                                 GetDetailExceptionInfo(e)))
            return FAILED

    def __parseAndGetResponse(self, cmd_response, response_cls, is_async):
        '''
        @Name : __parseAndGetResponse
        @Desc : Verifies the  Response(from CS) and returns an
                appropriate json parsed Response
        @Input: cmd_response: Command Response from cs
                response_cls : Mapping class for this Response
                is_async: Whether the cmd is async or not.
        @Output:Response output from CS
        '''
        try:
            try:
                ret = jsonHelper.getResultObj(
                    cmd_response.json(),
                    response_cls)
            except TypeError:
                ret = jsonHelper.getResultObj(cmd_response.json, response_cls)

            '''
            If the response is asynchronous, poll and return response
            else return response as it is
            '''
            if is_async == "false":
                self.logger.debug("Response : %s" % str(ret))
                return ret
            else:
                response = self.__poll(ret.jobid, response_cls)
                self.logger.debug("Response : %s" % str(response))
                return response.jobresult if response != FAILED else FAILED
        except Exception as e:
            self.__lastError = e
            self.logger.\
                exception("Exception:%s" % GetDetailExceptionInfo(e))
            return FAILED

    def marvinRequest(self, cmd, response_type=None, method='GET', data=''):
        """
        @Name : marvinRequest
        @Desc: Handles Marvin Requests
        @Input  cmd: marvin's command from cloudstackAPI
                response_type: response type of the command in cmd
                method: HTTP GET/POST, defaults to GET
        @Output: Response received from CS
                 Exception in case of Error\Exception
        """
        try:
            '''
            1. Verify the Inputs Provided
            '''
            if (cmd is None or cmd == ''):
                self.logger.exception("marvinRequest : Invalid Command Input")
                raise InvalidParameterException("Invalid Parameter")

            '''
            2. Sanitize the Command
            '''
            sanitize_cmd_out = self.__sanitizeCmd(cmd)

            if sanitize_cmd_out == FAILED:
                raise self.__lastError

            cmd_name, is_async, payload = sanitize_cmd_out
            '''
            3. Send Command to CS
            '''
            cmd_response = self.__sendCmdToCS(cmd_name,
                                              self.auth,
                                              payload=payload,
                                              method=method)
            if cmd_response == FAILED:
                raise self.__lastError

            '''
            4. Check if the Command Response received above is valid or Not.
               If not return Invalid Response
            '''
            ret = self.__parseAndGetResponse(cmd_response,
                                             response_type,
                                             is_async)
            if ret == FAILED:
                raise self.__lastError
            return ret
        except Exception as e:
            self.logger.exception("marvinRequest : CmdName: %s Exception: %s" %
                                  (str(cmd), GetDetailExceptionInfo(e)))
            raise e
