"""
NovaApi - module to interact with CFEngine Mission Portal Enterprise API

https://docs.cfengine.com/docs/3.18/enterprise-cfengine-guide-enterprise-api.html

Examples of usage:

```python
api = NovaApi() # defaults to CFE_ROBOT user and local hub certificate
print(api.fr_remote_hubs()) # should fail with message
api = NovaApi(api_user='admin', api_password='password')
response = api.fr_remote_hubs()
for hub in response['hubs']:
  print(hub)
  print(hub['ui_name'])
print(api.query("select * from hosts"))
print(api.query("select * from __hubs")["rows"])
print(api.status())
print(api.fr_hub_status())
print(api.get("user","admin"))
print(api.put('user',"yj",{"password":"quijibo"}))
print(api.put("role","yj",{}))
print(api.put_role_permissions("yj", ["query.post"]))
```
"""

import json
import os
import socket
import sys
import urllib3

_WORKDIR = os.environ.get("WORKDIR", "/var/cfengine")
_DEFAULT_SECRETS_PATH = "{}/httpd/secrets.ini".format(_WORKDIR)


class Unauthenticated(Exception):
    pass


class Unauthenticated2FA(Unauthenticated):
    pass


class NovaApi:
    def __init__(
        self,
        hostname=None,
        api_user="CFE_ROBOT",
        api_password=None,
        cert_path=None,
        ca_cert_dir=None,
        two_factor_token=None,
    ):
        self._hostname = hostname or str(socket.getfqdn())
        self._api_user = api_user
        if api_password is None:
            self._api_password = self._get_robot_password()
        else:
            self._api_password = api_password
        if cert_path is None:
            self._cert_path = "{}/httpd/ssl/certs/{}.cert".format(
                _WORKDIR, socket.getfqdn()
            )
        else:
            self._cert_path = cert_path
        if ca_cert_dir is None:
            self._ca_cert_dir = os.environ.get("SSL_CERT_DIR")
        else:
            self._ca_cert_dir = ca_cert_dir

        self._http = urllib3.PoolManager(
            cert_reqs="CERT_REQUIRED",
            ca_certs=self._cert_path,
            ca_cert_dir=self._ca_cert_dir,
        )
        self._headers = urllib3.make_headers(
            basic_auth="{}:{}".format(self._api_user, self._api_password)
        )
        self._headers["Content-Type"] = "application/json"
        if two_factor_token:
            self._headers["Cf-2fa-Token"] = two_factor_token
        # urllib3 v2.0 removed SubjectAltNameWarning and instead throws an error if no SubjectAltName is present in a certificate
        if hasattr(urllib3.exceptions, "SubjectAltNameWarning"):
            # if urllib3 is < v2.0 then SubjectAltNameWarning will exist and should be silenced
            if not sys.warnoptions:
                import warnings

                warnings.simplefilter(
                    "ignore", category=urllib3.exceptions.SubjectAltNameWarning
                )

    def __str__(self):
        return str(self.__class__) + ":" + str(self.__dict__)

    def _get_robot_password(self):
        with open(_DEFAULT_SECRETS_PATH) as file:
            for line in file:
                if "cf_robot_password" in line:
                    tokens = line.split("=")
                    if len(tokens) == 2:
                        return tokens[1].strip()
        raise Exception(
            "Could not parse CFE_ROBOT password from {} file".format(
                _DEFAULT_SECRETS_PATH
            )
        )

    def _request(self, method, path, body=None):
        url = "https://{}/api/{}".format(self._hostname, path)
        if type(body) is not str:
            payload = json.JSONEncoder().encode(body)
        else:
            payload = body
        response = self._http.request(method, url, headers=self._headers, body=payload)
        return self._build_response(response)

    def _build_response(self, response):
        if response.status != 200:
            value = {}
            message = response.data.decode("utf-8").strip()
            if not message:
                if response.status == 201:
                    message = "Created"
            if response.status == 401:
                if "Invalid two-factor" in message:
                    raise Unauthenticated2FA(message)
                raise Unauthenticated(message)
            value["message"] = message
            value["status"] = response.status
        else:
            body = response.data.decode("utf-8").strip()
            if not body:
                return {"status": response.status}
            data = json.loads(body)
            # some APIs like query API return a top-level data key which we want to skip for ease of use
            if "data" in data:
                # data response e.g. query API returns top-level key 'data'
                # which has a value of a list with one entry containing
                # the information.
                # see https://docs.cfengine.com/docs/master/reference-enterprise-api-ref-query.html#execute-sql-query
                value = data["data"][0]
                value["meta"] = data["meta"]
            else:
                value = data if type(data) is dict else {}
            value["status"] = response.status
        return value

    def query(self, sql):
        clean_sql = sql.replace("\n", " ").strip()
        return self._request(
            "POST",
            "query",
            body="""
           {{ "query": "{}" }}""".format(
                clean_sql
            ),
        )

    def status(self):
        return self._request("GET", "")

    def fr_remote_hubs(self):
        response = self._request("GET", "fr/remote-hub")
        values = {}
        values["hubs"] = [
            response[key] for key in response if type(response[key]) is dict
        ]
        values["status"] = response["status"]
        return values

    def fr_hub_status(self):
        return self._request("GET", "fr/hub-status")

    def fr_enable_as_superhub(self):
        return self._request("POST", "fr/setup-hub/superhub")

    def fr_enable_as_feeder(self):
        return self._request("POST", "fr/setup-hub/feeder")

    def get(self, entity, identifier):
        return self._request("GET", "{}/{}".format(entity, identifier))

    def put(self, entity, identifier, data):
        return self._request("PUT", "{}/{}".format(entity, identifier), data)

    def delete(self, entity, identifier):
        return self._request("DELETE", "{}/{}".format(entity, identifier))

    def put_role_permissions(self, identifier, data):
        return self._request("PUT", "role/{}/permissions".format(identifier), data)
