# (c) Copyright 2015 Hewlett Packard Enterprise Development LP
# Copyright 2015 Universidade Federal de Campina Grande
#
#    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 json
import time

import requests
import retrying

from oneview_client import exceptions
from oneview_client import ilo_utils
from oneview_client.models import ServerHardware
from oneview_client.models import ServerProfile
from oneview_client.models import ServerProfileTemplate
from oneview_client import states


SUPPORTED_ONEVIEW_VERSION = 200

WAIT_DO_REQUEST_IN_MILLISECONDS = 1000
WAIT_TASK_IN_MILLISECONDS = 1000

GET_REQUEST_TYPE = 'GET'
PUT_REQUEST_TYPE = 'PUT'
POST_REQUEST_TYPE = 'POST'
DELETE_REQUEST_TYPE = 'DELETE'

MOMENTARY_PRESS = 'MomentaryPress'
PRESS_AND_HOLD = 'PressAndHold'

SERVER_HARDWARE_PREFIX_URI = '/rest/server-hardware/'
SERVER_PROFILE_TEMPLATE_PREFIX_URI = '/rest/server-profile-templates/'


class Client(object):

    def __init__(
        self, manager_url, username, password,
        allow_insecure_connections=False, tls_cacert_file='',
        max_polling_attempts=20
    ):
        self.manager_url = manager_url
        self.username = username
        self.password = password
        self.allow_insecure_connections = allow_insecure_connections
        self.tls_cacert_file = tls_cacert_file
        self.max_polling_attempts = max_polling_attempts

        self.session_id = self.get_session()

    def verify_credentials(self):
        return self._authenticate()

    def get_session(self):
        response = self._authenticate()
        return response.json().get('sessionID')

    def _authenticate(self):
        if self.manager_url in ("", None):
            raise exceptions.OneViewConnectionError(
                "Can't connect to OneView: 'manager_url' configuration"
                "parameter is blank")

        url = '%s/rest/login-sessions' % self.manager_url
        body = {
            'userName': self.username,
            'password': self.password
        }
        headers = {
            'content-type': 'application/json'
        }

        verify_ssl = self._get_verify_connection_option()

        r = requests.post(url,
                          data=json.dumps(body),
                          headers=headers,
                          verify=verify_ssl)
        if r.status_code == 400:
            raise exceptions.OneViewNotAuthorizedException()
        else:
            return r

    def _get_verify_connection_option(self):
        verify_status = False
        user_cacert = self.tls_cacert_file

        if self.allow_insecure_connections is False:
            if not user_cacert:
                verify_status = True
            else:
                verify_status = user_cacert
        return verify_status

    def verify_oneview_version(self):
        if not self._is_oneview_version_compatible():
            raise exceptions.IncompatibleOneViewAPIVersion(
                "The version of the OneView's API is unsupported. Supported "
                "version is '%s'" % SUPPORTED_ONEVIEW_VERSION)

    def _is_oneview_version_compatible(self):
        versions = self.get_oneview_version()
        v = SUPPORTED_ONEVIEW_VERSION
        min_version_compatible = versions.get("minimumVersion") <= v
        max_version_compatible = versions.get("currentVersion") >= v
        return min_version_compatible and max_version_compatible

    def get_oneview_version(self):
        url = '%s/rest/version' % self.manager_url
        headers = {"Accept-Language": "en_US"}

        verify_ssl = self._get_verify_connection_option()

        try:
            versions = requests.get(
                url, headers=headers, verify=verify_ssl
            ).json()
            return versions
        except requests.RequestException as e:
            raise exceptions.OneViewConnectionError(e.message)

    # --- Power Driver ---
    def get_node_power_state(self, node_info):
        return self.get_server_hardware(node_info).power_state

    def power_on(self, node_info):
        if self.get_node_power_state(node_info) == states.ONEVIEW_POWER_ON:
            ret = states.ONEVIEW_POWER_ON
        else:
            ret = self.set_node_power_state(
                node_info, states.ONEVIEW_POWER_ON
            )
        return ret

    def power_off(self, node_info):
        if self.get_node_power_state(node_info) == states.ONEVIEW_POWER_OFF:
            ret = states.ONEVIEW_POWER_OFF
        else:
            ret = self.set_node_power_state(
                node_info, states.ONEVIEW_POWER_OFF, PRESS_AND_HOLD
            )
        return ret

    def set_node_power_state(
        self, node_info, state, press_type=MOMENTARY_PRESS
    ):
        body = {'powerState': state, 'powerControl': press_type}
        power_state_uri = (node_info.get('server_hardware_uri') +
                           '/powerState')
        task = self._prepare_and_do_request(
            uri=power_state_uri, body=body, request_type=PUT_REQUEST_TYPE
        )
        try:
            self._wait_for_task_to_complete(task)
        except exceptions.OneViewTaskError as e:
            raise exceptions.OneViewErrorStateSettingPowerState(e.message)

        current_state = self.get_node_power_state(node_info)

        if current_state is states.ONEVIEW_ERROR:
            message = (
                "Error setting node power state to %(state)s" %
                {"state": state}
            )
            raise exceptions.OneViewErrorStateSettingPowerState(message)

        return current_state

    # --- ManagementDriver ---
    def get_server_hardware(self, node_info):
        uuid = node_info['server_hardware_uri'].split("/")[-1]

        return self.get_server_hardware_by_uuid(uuid)

    def get_server_hardware_by_uuid(self, uuid):
        server_hardware_uri = SERVER_HARDWARE_PREFIX_URI + str(uuid)
        server_hardware_json = self._prepare_and_do_request(
            uri=server_hardware_uri
        )
        if server_hardware_json.get("uri") is None:
            message = "OneView Server Hardware resource not found."
            raise exceptions.OneViewResourceNotFoundError(message)

        return ServerHardware.from_json(server_hardware_json)

    def get_server_profile_from_hardware(self, node_info):
        server_hardware = self.get_server_hardware(node_info)
        server_profile_uri = server_hardware.server_profile_uri

        if server_profile_uri is None:
            message = (
                "There is no server profile assigned to"
                " %(server_hardware_uri)s" %
                {'server_hardware_uri': node_info.get('server_hardware_uri')}
            )
            raise exceptions.OneViewServerProfileAssociatedError(message)

        server_profile_json = self._prepare_and_do_request(
            uri=server_profile_uri
        )

        if server_profile_json.get('uri') is None:
            message = "OneView Server Profile resource not found."
            raise exceptions.OneViewResourceNotFoundError(message)

        return ServerProfile.from_json(server_profile_json)

    def get_server_profile_template(self, node_info):
        uuid = node_info['server_profile_template_uri'].split("/")[-1]

        return self.get_server_profile_template_by_uuid(uuid)

    def get_server_profile_template_by_uuid(self, uuid):
        server_profile_template_uri = SERVER_PROFILE_TEMPLATE_PREFIX_URI \
            + str(uuid)

        spt_json = self._prepare_and_do_request(
            uri=server_profile_template_uri
        )

        if spt_json.get("uri") is None:
            message = "OneView Server Profile Template resource not found."
            raise exceptions.OneViewResourceNotFoundError(message)

        return ServerProfileTemplate.from_json(spt_json)

    def get_boot_order(self, node_info):
        server_profile = self.get_server_profile_from_hardware(
            node_info
        )
        return server_profile.boot.get("order")

    def _update_boot_order(self, server_profile, order):
        manageBoot = server_profile.boot.get("manageBoot")
        server_profile.boot = {
            "manageBoot": manageBoot,
            "order": order
        }
        return server_profile

    def set_boot_device(self, node_info, new_primary_boot_device):
        boot_order = self.get_boot_order(node_info)

        if new_primary_boot_device is None:
            raise exceptions.OneViewBootDeviceInvalidError()

        if new_primary_boot_device in boot_order:
            boot_order.remove(new_primary_boot_device)

        boot_order.insert(0, new_primary_boot_device)

        server_profile = self.get_server_profile_from_hardware(
            node_info
        )

        server_profile_updated = self._update_boot_order(
            server_profile,
            boot_order
        )

        boot_order_dict = server_profile_updated.to_oneview_dict()

        task = self._prepare_and_do_request(
            uri=server_profile.uri, body=boot_order_dict,
            request_type=PUT_REQUEST_TYPE
        )
        try:
            self._wait_for_task_to_complete(task)
        except exceptions.OneViewTaskError as e:
            raise exceptions.OneViewErrorSettingBootDevice(e.message)

    # ---- Node validate ----
    def validate_node_server_hardware(
        self, node_info, node_memorymb, node_cpus
    ):
        node_sh_uri = node_info.get('server_hardware_uri')
        server_hardware = self.get_server_hardware(node_info)
        server_hardware_cpus = (server_hardware.processor_core_count
                                * server_hardware.processor_count)
        if server_hardware.memory_mb != node_memorymb:
            message = (
                "Node memory_mb is inconsistent with OneView's"
                " server hardware %(server_hardware_uri)s memoryMb."
                % {'server_hardware_uri': node_sh_uri}
            )
            raise exceptions.OneViewInconsistentResource(message)
        elif server_hardware_cpus != node_cpus:
            message = (
                "Node cpus is inconsistent with OneView's"
                " server hardware %(server_hardware_uri)s cpus."
                % {'server_hardware_uri': node_sh_uri}
            )
            raise exceptions.OneViewInconsistentResource(message)

    def validate_node_server_hardware_type(self, node_info):
        node_sht_uri = node_info.get('server_hardware_type_uri')
        server_hardware = self.get_server_hardware(node_info)
        server_hardware_sht_uri = server_hardware.server_hardware_type_uri

        if server_hardware_sht_uri != node_sht_uri:
            message = (
                "Node server_hardware_type_uri is inconsistent"
                " with OneView's server hardware %(server_hardware_uri)s"
                " serverHardwareTypeUri." %
                {'server_hardware_uri': node_info.get('server_hardware_uri')}
            )
            raise exceptions.OneViewInconsistentResource(message)

    def check_server_profile_is_applied(self, node_info):
        self.get_server_profile_from_hardware(node_info)

    def validate_node_enclosure_group(self, node_info):
        server_hardware = self.get_server_hardware(node_info)
        sh_enclosure_group_uri = server_hardware.enclosure_group_uri
        node_enclosure_group_uri = node_info.get('enclosure_group_uri')

        if node_enclosure_group_uri not in ('', 'None', None):
            if sh_enclosure_group_uri != node_enclosure_group_uri:
                message = (
                    "Node enclosure_group_uri '%(node_enclosure_group_uri)s' "
                    "is inconsistent with OneView's server hardware "
                    "serverGroupUri '%(sh_enclosure_group_uri)s' of "
                    "ServerHardware %(server_hardware)s"
                    % {
                        'node_enclosure_group_uri': node_enclosure_group_uri,
                        'sh_enclosure_group_uri': sh_enclosure_group_uri,
                        'server_hardware': server_hardware.uuid,
                    }
                )
                raise exceptions.OneViewInconsistentResource(message)

    def is_node_port_mac_compatible_with_server_profile(
        self, node_info, ports
    ):
        server_profile = self.get_server_profile_from_hardware(
            node_info
        )

        if server_profile.connections:
            primary_boot_connection = None

            for connection in server_profile.connections:
                boot = connection.get('boot')
                if (boot is not None and
                   boot.get('priority').lower() == 'primary'):
                    primary_boot_connection = connection

            if primary_boot_connection is None:
                message = (
                    "No primary boot connection configured for server profile"
                    " %s." % server_profile.uri
                )
                raise exceptions.OneViewInconsistentResource(message)

            mac = primary_boot_connection.get('mac')

        else:
            # If no connections on Server Profile, the connections are not
            # being managed or the server is rack based. In both cases, fall
            # back to 1st nic from iLO
            server_hardware = self.get_server_hardware(node_info)
            mac = self.get_sh_mac_from_ilo(server_hardware.uuid, nic_index=0)

        is_mac_address_compatible = True
        for port in ports:
            port_address = port.__dict__.get('_obj_address')
            if port_address is None:
                port_address = port.__dict__.get('_address')
            if port_address.lower() != mac.lower():
                is_mac_address_compatible = False

        if (not is_mac_address_compatible) or len(ports) == 0:
            message = (
                "The ports of the node are not compatible with its"
                " server profile %(server_profile_uri)s." %
                {'server_profile_uri': server_profile.uri}
            )
            raise exceptions.OneViewInconsistentResource(message)

    def is_node_port_mac_compatible_with_server_hardware(
        self, node_info, ports
    ):
        server_hardware = self.get_server_hardware(node_info)
        try:
            first_physical_port = server_hardware.get_mac(nic_index=0)
            mac = first_physical_port.get('mac').lower()
        except exceptions.OneViewException:
            mac = self.get_sh_mac_from_ilo(server_hardware.uuid, nic_index=0)

        is_mac_address_compatible = True
        for port in ports:
            port_address = port.__dict__.get('_obj_address')
            if port_address is None:
                port_address = port.__dict__.get('_address')

            if port_address.lower() != mac:
                is_mac_address_compatible = False

        if (not is_mac_address_compatible) or len(ports) == 0:
            message = (
                "The ports of the node are not compatible with its"
                " server hardware %(server_hardware_uri)s." %
                {'server_hardware_uri': server_hardware.uri}
            )
            raise exceptions.OneViewInconsistentResource(message)

    def validate_node_server_profile_template(self, node_info):
        node_spt_uri = node_info.get('server_profile_template_uri')

        server_profile_template = self.get_server_profile_template(
            node_info
        )
        spt_server_hardware_type_uri = server_profile_template \
            .server_hardware_type_uri
        spt_enclosure_group_uri = server_profile_template.enclosure_group_uri

        server_hardware = self.get_server_hardware(node_info)
        sh_server_hardware_type_uri = server_hardware.server_hardware_type_uri
        sh_enclosure_group_uri_uri = server_hardware.enclosure_group_uri
        if spt_server_hardware_type_uri != sh_server_hardware_type_uri:
            message = (
                "Server profile template %(spt_uri)s serverHardwareTypeUri is"
                " inconsistent with server hardware %(server_hardware_uri)s"
                " serverHardwareTypeUri." %
                {'spt_uri': node_spt_uri,
                 'server_hardware_uri': node_info.get('server_hardware_uri')}
            )
            raise exceptions.OneViewInconsistentResource(message)
        if spt_enclosure_group_uri != sh_enclosure_group_uri_uri:
            message = (
                "Server profile template %(spt_uri)s enclosureGroupUri is"
                " inconsistent with server hardware %(server_hardware_uri)s"
                " serverGroupUri." %
                {'spt_uri': node_spt_uri,
                 'server_hardware_uri': node_info.get('server_hardware_uri')}
            )
            raise exceptions.OneViewInconsistentResource(message)

        for connection in server_profile_template.connections:
            boot = connection.get('boot')
            if boot is not None and boot.get('priority').lower() != 'primary':
                message = (
                    "No primary boot connection configured for server profile"
                    " template %s." % server_profile_template.uri
                )
                raise exceptions.OneViewInconsistentResource(message)

    # --- Requests ---
    def _prepare_and_do_request(
        self, uri, body={}, request_type=GET_REQUEST_TYPE
    ):
        json_response = {}
        try:
            if not self.session_id:
                self.session_id = self.get_session()

            headers = {
                'content-type': 'application/json',
                'X-Api-Version': SUPPORTED_ONEVIEW_VERSION,
                'Auth': self.session_id
            }
            url = '%s%s' % (self.manager_url, uri)
            body = json.dumps(body)
            response = self._do_request(url, headers, body, request_type)

            json_response = response.json()
        except requests.RequestException as e:
            connection_error = str(e.message).split(':')[-1]
            log_message = ("Can't connect to OneView: %s" % connection_error)
            raise exceptions.OneViewConnectionError(log_message)

        return json_response

    def _do_request(self, url, headers, body, request_type):
        verify_status = self._get_verify_connection_option()

        @retrying.retry(
            stop_max_attempt_number=self.max_polling_attempts,
            retry_on_result=lambda response: _check_request_status(response),
            wait_fixed=WAIT_DO_REQUEST_IN_MILLISECONDS
        )
        def request(url, headers, body, request_type):

            if request_type == PUT_REQUEST_TYPE:
                response = requests.put(
                    url, data=body, headers=headers, verify=verify_status
                )
            elif request_type == POST_REQUEST_TYPE:
                response = requests.post(
                    url, data=body, headers=headers, verify=verify_status
                )
            elif request_type == DELETE_REQUEST_TYPE:
                response = requests.delete(
                    url, headers=headers, verify=verify_status
                )
            else:
                response = requests.get(
                    url, headers=headers, verify=verify_status
                )
            return response
        return request(url, headers, body, request_type)

    def _wait_for_task_to_complete(self, task):
        @retrying.retry(
            retry_on_result=lambda task: task.get('percentComplete') < 100,
            wait_fixed=WAIT_TASK_IN_MILLISECONDS,
            retry_on_exception=lambda task: False
        )
        def wait(task):
            uri = task.get('uri')
            task = self._prepare_and_do_request(uri)

            task_state = task.get("taskState")
            error_code = task.get("errorCode")
            if (not task_state) and error_code:
                details = task.get("details")
                if error_code == "RESOURCE_NOT_FOUND":
                    raise exceptions.OneViewResourceNotFoundError(details)
                else:
                    raise exceptions.OneViewTaskError("%s - %s"
                                                      % (error_code, details))
            elif task_state.lower() == 'error':
                raise exceptions.OneViewTaskError("The task '%s' returned an "
                                                  "error state" % uri)
            return task
        return wait(task)

    def _get_ilo_access(self, server_hardware_uuid):
        uri = ("/rest/server-hardware/%s/remoteConsoleUrl"
               % server_hardware_uuid)
        json = self._prepare_and_do_request(uri)
        url = json.get("remoteConsoleUrl")
        ip_key = "addr="
        host_ip = url[url.rfind(ip_key) + len(ip_key):]
        host_ip = host_ip[:host_ip.find("&")]
        session_key = "sessionkey="
        token = url[url.rfind(session_key) + len(session_key):]

        return host_ip, token

    def get_sh_mac_from_ilo(self, server_hardware_uuid, nic_index=0):
        host_ip, ilo_token = self._get_ilo_access(server_hardware_uuid)
        try:
            return ilo_utils.get_mac_from_ilo(host_ip, ilo_token, nic_index)
        finally:
            ilo_utils.ilo_logout(host_ip, ilo_token)


def _check_request_status(response):
    repeat = False
    status = response.status_code

    if status == 401:
        raise exceptions.OneViewNotAuthorizedException()
    elif status == 404:
        raise exceptions.OneViewResourceNotFoundError()
    elif status in (408, 409,):
        time.sleep(10)
        repeat = True
    elif status == 500:
        raise exceptions.OneViewInternalServerError(
            "OneView returned HTTP 500"
        )
    # Any other unexpected status are logged
    elif status not in (200, 202,):
        message = (
            "OneView appliance returned an unknown response status: %s"
            % status
        )
        raise exceptions.UnknowOneViewResponseError(message)
    return repeat
