# Copyright (c) 2015 Mirantis Inc.
#
# 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 oslo_log import log as logging
from oslo_serialization import jsonutils
from requests import auth

from sahara import context
from sahara.i18n import _
from sahara.plugins.ambari import requests_helper as r_helper
from sahara.plugins import exceptions as p_exc


LOG = logging.getLogger(__name__)


class AmbariNotFound(Exception):
    pass


class AmbariClient(object):
    def __init__(self, instance, port="8080", **kwargs):
        kwargs.setdefault("username", "admin")
        kwargs.setdefault("password", "admin")

        self._port = port
        self._base_url = "http://{host}:{port}/api/v1".format(
            host=instance.management_ip, port=port)
        self._instance = instance
        self._http_client = instance.remote().get_http_client(port)
        self._headers = {"X-Requested-By": "sahara"}
        self._auth = auth.HTTPBasicAuth(kwargs["username"], kwargs["password"])
        self._default_client_args = {"verify": False, "auth": self._auth,
                                     "headers": self._headers}

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        self.close()

    def close(self):
        self._instance.remote().close_http_session(self._port)

    def get(self, *args, **kwargs):
        kwargs.update(self._default_client_args)
        return self._http_client.get(*args, **kwargs)

    def post(self, *args, **kwargs):
        kwargs.update(self._default_client_args)
        return self._http_client.post(*args, **kwargs)

    def put(self, *args, **kwargs):
        kwargs.update(self._default_client_args)
        return self._http_client.put(*args, **kwargs)

    def delete(self, *args, **kwargs):
        kwargs.update(self._default_client_args)
        return self._http_client.delete(*args, **kwargs)

    def get_alerts_data(self, cluster):
        url = self._base_url + "/clusters/%s/alerts?fields=*" % cluster.name
        resp = self.get(url)
        data = self.check_response(resp)
        return data.get('items', [])

    @staticmethod
    def check_response(resp, handle_not_found=False):
        if handle_not_found and resp.status_code == 404:
            raise AmbariNotFound()
        resp.raise_for_status()
        if resp.text:
            return jsonutils.loads(resp.text)

    @staticmethod
    def req_id(response):
        if not response.text:
            raise p_exc.HadoopProvisionError("Cannot find request id. "
                                             "No response body")
        body = jsonutils.loads(response.text)
        if "Requests" not in body or "id" not in body["Requests"]:
            raise p_exc.HadoopProvisionError("Cannot find request id. "
                                             "Unexpected response format")
        return body["Requests"]["id"]

    def import_credential(self, cl_name, alias, data):
        url = self._base_url + "/clusters/%s/credentials/%s" % (cl_name, alias)
        resp = self.post(url, data=jsonutils.dumps(data))
        self.check_response(resp)

    def get_credential(self, cl_name, alias):
        url = self._base_url + "/clusters/%s/credentials/%s" % (cl_name, alias)
        resp = self.get(url)
        self.check_response(resp, handle_not_found=True)

    def regenerate_keytabs(self, cl_name):
        url = (self._base_url +
               "/clusters/%s?regenerate_keytabs=missing" % cl_name)
        data = jsonutils.dumps({"Clusters": {"security_type": "KERBEROS"}})
        resp = self.put(url, data=data)
        self.check_response(resp)
        return self.req_id(resp)

    def get_registered_hosts(self):
        url = self._base_url + "/hosts"
        resp = self.get(url)
        data = self.check_response(resp)
        return data.get("items", [])

    def get_host_info(self, host):
        url = self._base_url + "/hosts/%s" % host
        resp = self.get(url)
        data = self.check_response(resp)
        return data.get("Hosts", {})

    def update_user_password(self, user, old_password, new_password):
        url = self._base_url + "/users/%s" % user
        data = jsonutils.dumps({
            "Users": {
                "old_password": old_password,
                "password": new_password
            }
        })
        resp = self.put(url, data=data)
        self.check_response(resp)

    def create_blueprint(self, name, data):
        url = self._base_url + "/blueprints/%s" % name
        resp = self.post(url, data=jsonutils.dumps(data))
        return self.check_response(resp)

    def create_cluster(self, name, data):
        url = self._base_url + "/clusters/%s" % name
        resp = self.post(url, data=jsonutils.dumps(data))
        return self.check_response(resp).get("Requests")

    def add_host_to_cluster(self, instance):
        cluster_name = instance.cluster.name
        hostname = instance.fqdn()
        url = self._base_url + "/clusters/{cluster}/hosts/{hostname}".format(
            cluster=cluster_name, hostname=hostname)
        resp = self.post(url)
        self.check_response(resp)

    def get_config_groups(self, cluster):
        url = self._base_url + "/clusters/%s/config_groups" % cluster.name
        resp = self.get(url)
        return self.check_response(resp)

    def get_detailed_config_group(self, cluster, cfg_id):
        url = self._base_url + "/clusters/%s/config_groups/%s" % (
            cluster.name, cfg_id)
        resp = self.get(url)
        return self.check_response(resp)

    def remove_config_group(self, cluster, cfg_id):
        url = self._base_url + "/clusters/%s/config_groups/%s" % (
            cluster.name, cfg_id)
        resp = self.delete(url)
        return self.check_response(resp)

    def create_config_group(self, cluster, data):
        url = self._base_url + "/clusters/%s/config_groups" % cluster.name
        resp = self.post(url, data=jsonutils.dumps(data))
        return self.check_response(resp)

    def add_service_to_host(self, inst, service):
        url = "{pref}/clusters/{cluster}/hosts/{host}/host_components/{proc}"
        url = url.format(pref=self._base_url, cluster=inst.cluster.name,
                         host=inst.fqdn(), proc=service)
        self.check_response(self.post(url))

    def start_service_on_host(self, inst, service, final_state):
        url = "{pref}/clusters/{cluster}/hosts/{host}/host_components/{proc}"
        url = url.format(
            pref=self._base_url, cluster=inst.cluster.name, host=inst.fqdn(),
            proc=service)
        data = {
            'HostRoles': {
                'state': final_state
            },
            'RequestInfo': {
                'context': "Starting service {service}, moving to state "
                           "{state}".format(service=service, state=final_state)
            }
        }
        resp = self.put(url, data=jsonutils.dumps(data))
        self.check_response(resp)
        # return req_id to check health of request
        return self.req_id(resp)

    def decommission_nodemanagers(self, cluster_name, instances):
        url = self._base_url + "/clusters/%s/requests" % cluster_name
        data = r_helper.build_nodemanager_decommission_request(cluster_name,
                                                               instances)
        resp = self.post(url, data=jsonutils.dumps(data))
        self.wait_ambari_request(self.req_id(resp), cluster_name)

    def decommission_datanodes(self, cluster_name, instances):
        url = self._base_url + "/clusters/%s/requests" % cluster_name
        data = r_helper.build_datanode_decommission_request(cluster_name,
                                                            instances)
        resp = self.post(url, data=jsonutils.dumps(data))
        self.wait_ambari_request(self.req_id(resp), cluster_name)

    def remove_process_from_host(self, cluster_name, instance, process):
        url = self._base_url + "/clusters/%s/hosts/%s/host_components/%s" % (
            cluster_name, instance.fqdn(), process)
        resp = self.delete(url)

        return self.check_response(resp)

    def stop_process_on_host(self, cluster_name, instance, process):
        url = self._base_url + "/clusters/%s/hosts/%s/host_components/%s" % (
            cluster_name, instance.fqdn(), process)
        check_installed_resp = self.check_response(self.get(url))

        if check_installed_resp["HostRoles"]["state"] != "INSTALLED":
            data = {"HostRoles": {"state": "INSTALLED"},
                    "RequestInfo": {"context": "Stopping %s" % process}}
            resp = self.put(url, data=jsonutils.dumps(data))

            self.wait_ambari_request(self.req_id(resp), cluster_name)

    def restart_namenode(self, cluster_name, instance):
        url = self._base_url + "/clusters/%s/requests" % cluster_name
        data = r_helper.build_namenode_restart_request(cluster_name, instance)
        resp = self.post(url, data=jsonutils.dumps(data))
        self.wait_ambari_request(self.req_id(resp), cluster_name)

    def restart_resourcemanager(self, cluster_name, instance):
        url = self._base_url + "/clusters/%s/requests" % cluster_name
        data = r_helper.build_resourcemanager_restart_request(cluster_name,
                                                              instance)
        resp = self.post(url, data=jsonutils.dumps(data))
        self.wait_ambari_request(self.req_id(resp), cluster_name)

    def restart_service(self, cluster_name, service_name):
        url = self._base_url + "/clusters/{}/services/{}".format(
            cluster_name, service_name)

        data = r_helper.build_stop_service_request(service_name)
        resp = self.put(url, data=jsonutils.dumps(data))
        self.wait_ambari_request(self.req_id(resp), cluster_name)

        data = r_helper.build_start_service_request(service_name)
        resp = self.put(url, data=jsonutils.dumps(data))
        self.wait_ambari_request(self.req_id(resp), cluster_name)

    def delete_host(self, cluster_name, instance):
        url = self._base_url + "/clusters/%s/hosts/%s" % (cluster_name,
                                                          instance.fqdn())
        resp = self.delete(url)
        return self.check_response(resp)

    def check_request_status(self, cluster_name, req_id):
        url = self._base_url + "/clusters/%s/requests/%d" % (cluster_name,
                                                             req_id)
        resp = self.get(url)
        return self.check_response(resp).get("Requests")

    def list_host_processes(self, cluster_name, instance):
        url = self._base_url + "/clusters/%s/hosts/%s" % (
            cluster_name, instance.fqdn())
        resp = self.get(url)
        body = jsonutils.loads(resp.text)

        procs = [p["HostRoles"]["component_name"]
                 for p in body["host_components"]]
        return procs

    def set_up_mirror(self, stack_version, os_type, repo_id, repo_url):
        url = self._base_url + (
            "/stacks/HDP/versions/%s/operating_systems/%s/repositories/%s") % (
                stack_version, os_type, repo_id)
        data = {
            "Repositories": {
                "base_url": repo_url,
                "verify_base_url": True
            }
        }
        resp = self.put(url, data=jsonutils.dumps(data))
        self.check_response(resp)

    def set_rack_info_for_instance(self, cluster_name, instance, rack_name):
        url = self._base_url + "/clusters/%s/hosts/%s" % (
            cluster_name, instance.fqdn())
        data = {
            "Hosts": {
                "rack_info": rack_name
            }
        }
        resp = self.put(url, data=jsonutils.dumps(data))
        self.check_response(resp)

    def get_request_info(self, cluster_name, request_id):
        url = self._base_url + ("/clusters/%s/requests/%s" %
                                (cluster_name, request_id))
        resp = self.check_response(self.get(url))
        return resp.get('Requests')

    def wait_ambari_requests(self, requests, cluster_name):
        requests = set(requests)
        failed = []
        context.sleep(20)
        while len(requests) > 0:
            completed, not_completed = set(), set()
            for req_id in requests:
                request = self.get_request_info(cluster_name, req_id)
                status = request.get("request_status")
                if status == 'COMPLETED':
                    completed.add(req_id)
                elif status in ['IN_PROGRESS', 'PENDING']:
                    not_completed.add(req_id)
                else:
                    failed.append(request)
            if failed:
                msg = _("Some Ambari request(s) "
                        "not in COMPLETED state: %(description)s.")
                descrs = []
                for req in failed:
                    descr = _(
                        "request %(id)d: %(name)s - in status %(status)s")
                    descrs.append(descr %
                                  {'id': req.get("id"),
                                   'name': req.get("request_context"),
                                   'status': req.get("request_status")})
                raise p_exc.HadoopProvisionError(msg % {'description': descrs})
            requests = not_completed
            context.sleep(5)
            LOG.debug("Waiting for %d ambari request(s) to be completed",
                      len(not_completed))
        LOG.debug("All ambari requests have been completed")

    def wait_ambari_request(self, request_id, cluster_name):
        context.sleep(20)
        while True:
            status = self.check_request_status(cluster_name, request_id)
            LOG.debug("Task %(context)s in %(status)s state. "
                      "Completed %(percent).1f%%",
                      {'context': status["request_context"],
                       'status': status["request_status"],
                       'percent': status["progress_percent"]})
            if status["request_status"] == "COMPLETED":
                return
            if status["request_status"] in ["IN_PROGRESS", "PENDING"]:
                context.sleep(5)
            else:
                raise p_exc.HadoopProvisionError(
                    _("Ambari request in %s state") % status["request_status"])
