############################################
# ________      ________    ________       #
# |\   __  \    |\   __  \  |\   __  \     #
# \ \  \|\  \   \ \  \|\  \ \ \  \|\  \    #
#  \ \  \\\  \   \ \   ____\ \ \   __  \   #
#   \ \  \\\  \   \ \  \___|  \ \  \ \  \  #
#    \ \_______\   \ \__\      \ \__\ \__\ #
#     \|_______|    \|__|       \|__|\|__| #
############################################

import requests
import urllib3
import json
import os
from user_agent import generate_user_agent
from typing import Union, Dict
from urllib.parse import urlencode
from .OpaExceptions import (CheckPermissionError,
                            ConnectionsError, DeleteDataError,
                            DeletePolicyError, PathNotFoundError,
                            PolicyNotFoundError, RegoParseError,
                            SSLError, FileError, TypeExecption,
                            QueryExecuteError)

__version__ = "1.3.1"
__author__ = "<NAME>"
__license__ = 'MIT'


class OpaClient:
    """ OpaClient client object to connect and manipulate OPA service.
        ```
        The class object holds session information necesseary to connect OPA service.
        param :: host : to connect OPA service ,defaults to localhost
        type  :: host: str
        param :: port : to connect OPA service ,defaults to 8181
        type  :: port : str or int
        param :: version : provided REST API version by OPA,defaults to v1
        type  :: version : str
        param :: ssl : verify ssl certificates for https requests,defaults to False
        type  :: ssl : bool
        param :: cert : path to client certificate information to use for mutual TLS authentification
        type  :: cert : str
        param :: headers :  dictionary of headers to send, defaults to None
        ```

    """

    def __init__(
        self,
        host: str = "localhost",
        port: int = 8181,
        version: str = "v1",
        ssl: bool = False,
        cert: Union[None, str] = None,
        headers: Union[None, dict] = None,
        **kwargs,
    ):
        host = host.lstrip()
        self.__port = port
        self.__version = version
        self.__policy_root = "{}/policies/{}"
        self.__data_root = "{}/data/{}"
        self.__secure = False
        self.__schema = "http://"
        self.retries = kwargs.get("retries", 2)
        self.timeout = kwargs.get("timeout", 1.5)

        if not isinstance(self.__port, int):
            raise TypeError("The port must be integer")

        if ssl:
            self.__ssl = ssl
            self.__cert = cert
            self.__secure = True
            self.__schema = "https://"

        if not cert and ssl is True:
            raise SSLError("ssl=True", "Make sure you  provide cert file")

        if host.startswith("https://"):
            self.__host = host
            self.__root_url = "{}:{}/{}".format(self.__host, self.__port,
                                                self.__version)

        elif host.startswith("http://"):
            self.__host = host
            if self.__secure:
                raise SSLError(
                    "ssl=True",
                    "With ssl enabled not possible to have connection with http",
                )

            self.__root_url = "{}:{}/{}".format(self.__host, self.__port,
                                                self.__version)

        else:
            self.__host = host

            self.__root_url = "{}{}:{}/{}".format(self.__schema, self.__host,
                                                  self.__port, self.__version)

        if headers:
            self.__headers = requests.utils.default_headers()
            self.__headers.update({**headers})
        else:
            self.__headers = requests.utils.default_headers()

            self.__headers.update({"User-Agent": generate_user_agent()})

        if self.__secure:
            self.__manager = urllib3.PoolManager(
                cert_reqs="CERT_REQUIRED",
                assert_hostname=False,
                ca_certs=self.__cert,
                headers=self.__headers,
            )
            self.__session = self.__manager.request
        else:
            self.__manager = urllib3.PoolManager(headers=self.__headers)
            self.__session = self.__manager.request

    def __del__(self):
        self.close_connection()

    def close_connection(self):
        """
        Close all currently open connections to the OPA server
        """
        try:
            self.__manager.clear()
        except:  # noqa: E722
            pass

    def check_connection(self):
        """
            Checks whether established connection config True or not.
            if not properly configured will raise an ConnectionError.
        """

        url = self.__policy_root.format(self.__root_url, "")
        try:
            response = self.__session(
                "GET", url, retries=self.retries, timeout=self.timeout)
            if response.status == 200:
                return "Yes I'm here :)"

        except Exception:
            raise ConnectionsError("service unreachable",
                                   "check config and try again")

        raise ConnectionsError("service unreachable",
                               "check config and try again")

    def check_health(self, query: Dict[str, bool] = None, diagnostic_url: str = None) -> bool:
        """
            Check OPA healthy. If you want check bundels or plugins, add query params for this.
            If your diagnostic url different than default url, you can provide it.
            ```
            param :: query : it is the url query string. default None
            param :: diagnostic_url : OPA diagnostic url

            example:
                print(client.check_health())
                print(client.check_health({"bundle": True}))
                print(client.check_health(diagnostic_url="http://localhost:8282/health"))
                print(client.check_health(query={"bundle": True}, diagnostic_url="http://localhost:8282/health"))

        """
        if diagnostic_url:
            url = diagnostic_url
        else:
            url = "{}{}:{}/{}".format(self.__schema, self.__host,
                                      self.__port, "health")
        if query:
            url = self.prepare_args(url, query)
        response = self.__session(
            "GET", url, retries=self.retries, timeout=self.timeout)
        if response.status == 200:
            return True
        return False

    def get_policies_list(self) -> list:
        """ Returns all  OPA policies in the service"""

        return self.__get_policies_list()

    def get_policies_info(self) -> dict:
        """ 
            Returns information about each policy, including
            policy path and policy rules
        """
        return self.__get_policies_info()

    def update_opa_policy_fromstring(self, new_policy: str,
                                     endpoint: str) -> bool:
        """ Write your rego policy with using python string type and update your OPA policies.
            ```
            param :: new_policy : is the name of your new defined  rego policy. 
            param :: endpoint : is the path of your new policy in OPA

            example:
                new_policy=
                    package play

                    default hello = false

                    hello {
                        m := input.message
                        m == "world"
                    }
                client = OpaClient()
                client.update_opa_policy_fromstring(new_policy,'exampleapi')
            ```

        """

        return self.__update_opa_policy_fromstring(new_policy, endpoint)

    def update_opa_policy_fromfile(self, filepath: str, endpoint: str) -> bool:
        """ Write your rego policy to file and update your OPA policies. """

        return self.__update_opa_policy_fromfile(filepath, endpoint)

    def update_opa_policy_fromurl(self, url: str, endpoint: str) -> bool:
        """Update your OPA policies from internet."""

        return self.__update_opa_policy_fromurl(url, endpoint)

    def update_or_create_opa_data(self, new_data: dict, endpoint: str) -> bool:
        """ Updates existing data or create new data for policy.
            ```
            param :: new_data : name of defined data
            type  :: new_data : dict
            param :: endpoint : is the path of your new data or existing one in OPA 
            type  :: endpoint : str
            example:
                my_policy_list = [
                    {"resource": "/api/someapi", "identity": "your_identity", "method": "PUT"},
                    {"resource": "/api/someapi", "identity": "your_identity", "method": "GET"},
                ]

                client = OpaClient()
                client.update_or_create_opa_data(my_policy_list,'exampledata/accesses')
            ```
        """

        return self.__update_opa_data(new_data, endpoint)

    def get_opa_raw_data(self, data_name: str = "", query_params: Dict[str, bool] = dict()) -> dict:
        """ Returns OPA raw data in string type 
            ```
            param :: data_name : OPA data name you want get
            param :: query_params : query params in url for more information about metrics
            ```
        """
        return self.__get_opa_raw_data(data_name, query_params)

    def opa_policy_to_file(self,
                           policy_name: str,
                           path: Union[str, None] = None,
                           filename: str = "opa_policy.rego"):
        """ Write OPA service policy to the  file.
        ```
            param :: policy_name : name of OPA policy
            type  :: policy_name : str

            param :: path : path to save file ,default current path
            type  :: path : str

            param :: filename : name of the file,default opa_policy.rego
            type  :: filename : str
        ```

        """
        return self.__opa_policy_to_file(policy_name, path, filename)

    def get_opa_policy(self, policy_name: str) -> dict:
        """Returns full info about policy, provided OPA service """

        return self.__get_opa_policy(policy_name)

    def delete_opa_policy(self, policy_name: str) -> bool:
        """ Deletes given OPA policy name """

        return self.__delete_opa_policy(policy_name)

    def delete_opa_data(self, data_name: str) -> bool:
        """ Deletes given OPA policy data name """

        return self.__delete_opa_data(data_name)

    def check_permission(self, input_data: dict, policy_name: str,
                         rule_name: str, query_params: Dict[str, bool] = dict()) -> dict:
        """
        ```
            params :: input_data    : data which you want check permission
                type   :: input_data  : dict

            params :: policy_name   : the name of policy resource
                type   :: policy_name  : str

            params :: rule_name   : the name included in the policy
                type   :: rule_name  : str
            param :: query_params : query params in url for more information about metrics
                type :: query_params : dict
        ```
        """

        return self.__check(input_data, policy_name, rule_name, query_params)

    def check_policy_rule(self,
                          input_data: dict,
                          package_path: str,
                          rule_name: str = None) -> dict:
        """
        Queries a package rule with the given input data
        """

        return self.__query(input_data, package_path, rule_name)

    def ad_hoc_query(self, *, query_params: Dict[str, str] = None, body: Dict[str, str] = None):
        """ Execute an ad-hoc query and return bindings for variables found in the query. 
        ```
            param :: query_params for sending query string in url
            param :: body  for sending query in request body

        ```
        """

        url = "{}{}:{}/{}/{}".format(self.__schema, self.__host,
                                     self.__port, "v1", "query")
        if body:
            encoded_json = json.dumps(body).encode("utf-8")
            response = self.__session(
                "POST",
                url,
                body=encoded_json,
                retries=self.retries,
                timeout=self.timeout,
            )
        elif query_params:
            url = self.prepare_args(url, query_params)
            response = self.__session("GET",
                                      url,
                                      retries=self.retries,
                                      timeout=self.timeout)
        data = json.loads(response.data.decode("utf-8"))
        if response.status == 200:
            return data

        raise QueryExecuteError(data.get("code"), data.get("message"))

    def prepare_args(self, url: str, query_params: dict) -> str:
        if query_params:
            query_params = urlencode(query_params)
            url = url + "?" + query_params
        return url

    def __get_opa_raw_data(self, data_name: str, query_params: Dict[str, bool]):
        url = self.__data_root.format(self.__root_url, data_name)
        url = self.prepare_args(url, query_params)
        response = self.__session("GET",
                                  url,
                                  retries=self.retries,
                                  timeout=self.timeout)
        code = response.status
        response = json.loads(response.data.decode("utf-8"))
        return response if code == 200 else (code, "not found")

    def __update_opa_data(self, new_data: dict, endpoint: str):
        url = self.__data_root.format(self.__root_url, endpoint)

        encoded_json = json.dumps(new_data).encode("utf-8")
        response = self.__session(
            "PUT",
            url,
            body=encoded_json,
            retries=self.retries,
            timeout=self.timeout,
        )
        code = response.status
        return True if code == 204 else False

    def __update_opa_policy_fromfile(self, filepath: str, endpoint: str):

        if os.path.isfile(filepath):
            with open(filepath, "r") as rf:
                return self.__update_opa_policy_fromstring(rf.read(), endpoint)

        raise FileError(f"{filepath}",
                        "is not a file, make sure you provide a file")

    def __update_opa_policy_fromstring(self, new_policy: str,
                                       endpoint: str) -> bool:

        if not isinstance(new_policy, str) or not isinstance(endpoint, str):
            raise TypeExecption(f"{new_policy} is not string type")

        if new_policy:
            url = self.__policy_root.format(self.__root_url, endpoint)

            response = self.__session(
                "PUT",
                url,
                body=new_policy.encode(),
                headers=self.__headers,
                retries=self.retries,
                timeout=self.timeout,
            )

            if response.status == 200:
                return True

            raise RegoParseError(
                json.loads(response.data.decode()).get("code"),
                json.loads(response.data.decode()).get("message"),
            )

        return False

    def __get_opa_policy(self, policy_name: str) -> dict:
        url = self.__policy_root.format(self.__root_url, policy_name)

        response = self.__session("GET",
                                  url,
                                  retries=self.retries,
                                  timeout=self.timeout)
        data = json.loads(response.data.decode("utf-8"))
        if response.status == 200:

            return data

        raise PolicyNotFoundError(data.get("code"), data.get("message"))

    def __update_opa_policy_fromurl(self, url: str, endpoint: str) -> bool:
        response = requests.get(url, headers=self.__headers)
        return self.__update_opa_policy_fromstring(response.text, endpoint)

    def __opa_policy_to_file(self, policy_name: str, path: Union[str, None],
                             filename: str) -> bool:
        raw_policy = self.__get_opa_policy(policy_name)
        if isinstance(raw_policy, dict):
            try:
                if path:

                    with open(f"{path}/{filename}", "wb") as wr:
                        wr.write(raw_policy.get("result").get("raw").encode())
                else:
                    with open(filename, "wb") as wr:
                        wr.write(raw_policy.get("result").get("raw").encode())
                return True

            except:  # noqa: E722
                raise PathNotFoundError("error when write file",
                                        "path not found")

    def __delete_opa_policy(self, policy_name: str) -> bool:
        url = self.__policy_root.format(self.__root_url, policy_name)

        response = self.__session("DELETE",
                                  url,
                                  retries=self.retries,
                                  timeout=self.timeout)
        data = json.loads(response.data.decode("utf-8"))
        if response.status == 200:
            return True

        raise DeletePolicyError(data.get("code"), data.get("message"))

    def __get_policies_list(self) -> list:
        url = self.__policy_root.format(self.__root_url, "")
        temp = []
        response = self.__session("GET",
                                  url,
                                  retries=self.retries,
                                  timeout=self.timeout,
                                  headers=self.__headers)

        response = json.loads(response.data.decode())

        for policy in response.get("result"):
            if policy.get("id"):
                temp.append(policy.get("id"))

        return temp

    def __delete_opa_data(self, data_name: str) -> bool:
        url = self.__data_root.format(self.__root_url, data_name)

        response = self.__session("DELETE",
                                  url,
                                  retries=self.retries,
                                  timeout=self.timeout)
        if response.data:
            data = json.loads(response.data.decode("utf-8"))
        if response.status == 204:
            return True

        raise DeleteDataError(data.get("code"), data.get("message"))

    def __get_policies_info(self) -> dict:
        url = self.__policy_root.format(self.__root_url, "")
        policy = self.__session("GET",
                                url,
                                retries=self.retries,
                                timeout=self.timeout,
                                headers=self.__headers)

        policy = json.loads(policy.data.decode())
        result = policy.get("result")

        temp_dict = {}
        for policy in result:
            temp_policy = []
            temp_rules = []
            temp_url = ""
            permission_url = self.__root_url
            for path in policy.get("ast").get("package").get("path"):
                permission_url += "/" + path.get("value")
            temp_policy.append(permission_url)
            for rule in policy.get("ast").get("rules"):
                if not rule.get("default"):
                    continue
                temp_url = permission_url
                temp_url += "/" + rule.get("head").get("name")
                temp_rules.append(temp_url)
            temp_dict[policy.get("id")] = {
                "path": temp_policy,
                "rules": temp_rules
            }

        return temp_dict

    def __check(self, input_data: dict, policy_name: str,
                rule_name: str, query_params: Dict[str, bool]) -> dict:
        url = self.__policy_root.format(self.__root_url, policy_name)

        policy = self.__session("GET",
                                url,
                                headers=self.__headers,
                                retries=self.retries,
                                timeout=self.timeout)
        policy = json.loads(policy.data.decode("utf-8"))
        result = policy.get("result")
        find = False
        permission_url = self.__root_url
        if result:
            for path in result.get("ast").get("package").get("path"):
                permission_url += "/" + path.get("value")

            for rule in result.get("ast").get("rules"):
                if not rule.get("default"):
                    continue
                if rule.get("head").get("name") == rule_name:

                    permission_url += "/" + rule.get("head").get("name")
                    find = True
        if find:
            encoded_json = json.dumps(input_data).encode("utf-8")
            permission_url = self.prepare_args(permission_url, query_params)
            response = self.__session("POST",
                                      permission_url,
                                      body=encoded_json,
                                      retries=self.retries,
                                      timeout=self.timeout)
            if response.data:
                data = json.loads(response.data.decode("utf-8"))
                return data

        raise CheckPermissionError(f"{rule_name} rule not found",
                                   "path or rule name not correct")

    def __query(self,
                input_data: dict,
                package_path: str,
                rule_name: str = None) -> dict:
        if '.' in package_path:
            package_path = package_path.replace('.', '/')
        if rule_name:
            package_path = package_path + '/' + rule_name
        url = self.__data_root.format(self.__root_url, package_path)

        encoded_json = json.dumps({'input': input_data}).encode("utf-8")
        response = self.__session("POST",
                                  url,
                                  body=encoded_json,
                                  retries=self.retries,
                                  timeout=self.timeout)
        if response.data:
            data = json.loads(response.data.decode("utf-8"))
            return data

        raise CheckPermissionError(f"{rule_name} rule not found",
                                   "policy or rule name not correct")

    @property
    def _host(self):
        return self.__host

    @property
    def _port(self):
        return self.__port

    @property
    def _version(self):
        return self.__version

    @property
    def _root_url(self):
        return self.__root_url

    @property
    def _schema(self):
        return self.__schema

    @property
    def _policy_root(self):
        return self.__policy_root

    @property
    def _data_root(self):
        return self.__data_root

    @property
    def _secure(self):
        return self.__secure

    @property
    def _ssl(self):
        return self.__ssl

    @property
    def _cert(self):
        return self.__cert

############################################
# ________      ________    ________       #
# |\   __  \    |\   __  \  |\   __  \     #
# \ \  \|\  \   \ \  \|\  \ \ \  \|\  \    #
#  \ \  \\\  \   \ \   ____\ \ \   __  \   #
#   \ \  \\\  \   \ \  \___|  \ \  \ \  \  #
#    \ \_______\   \ \__\      \ \__\ \__\ #
#     \|_______|    \|__|       \|__|\|__| #
############################################

import requests
import urllib3
import json
import os
from user_agent import generate_user_agent
from typing import Union, Dict
from urllib.parse import urlencode
from .OpaExceptions import (CheckPermissionError,
                            ConnectionsError, DeleteDataError,
                            DeletePolicyError, PathNotFoundError,
                            PolicyNotFoundError, RegoParseError,
                            SSLError, FileError, TypeExecption,
                            QueryExecuteError)

__version__ = "1.3.1"
__author__ = "<NAME>"
__license__ = 'MIT'


class OpaClient:
    """ OpaClient client object to connect and manipulate OPA service.
        ```
        The class object holds session information necesseary to connect OPA service.
        param :: host : to connect OPA service ,defaults to localhost
        type  :: host: str
        param :: port : to connect OPA service ,defaults to 8181
        type  :: port : str or int
        param :: version : provided REST API version by OPA,defaults to v1
        type  :: version : str
        param :: ssl : verify ssl certificates for https requests,defaults to False
        type  :: ssl : bool
        param :: cert : path to client certificate information to use for mutual TLS authentification
        type  :: cert : str
        param :: headers :  dictionary of headers to send, defaults to None
        ```

    """

    def __init__(
        self,
        host: str = "localhost",
        port: int = 8181,
        version: str = "v1",
        ssl: bool = False,
        cert: Union[None, str] = None,
        headers: Union[None, dict] = None,
        **kwargs,
    ):
        host = host.lstrip()
        self.__port = port
        self.__version = version
        self.__policy_root = "{}/policies/{}"
        self.__data_root = "{}/data/{}"
        self.__secure = False
        self.__schema = "http://"
        self.retries = kwargs.get("retries", 2)
        self.timeout = kwargs.get("timeout", 1.5)

        if not isinstance(self.__port, int):
            raise TypeError("The port must be integer")

        if ssl:
            self.__ssl = ssl
            self.__cert = cert
            self.__secure = True
            self.__schema = "https://"

        if not cert and ssl is True:
            raise SSLError("ssl=True", "Make sure you  provide cert file")

        if host.startswith("https://"):
            self.__host = host
            self.__root_url = "{}:{}/{}".format(self.__host, self.__port,
                                                self.__version)

        elif host.startswith("http://"):
            self.__host = host
            if self.__secure:
                raise SSLError(
                    "ssl=True",
                    "With ssl enabled not possible to have connection with http",
                )

            self.__root_url = "{}:{}/{}".format(self.__host, self.__port,
                                                self.__version)

        else:
            self.__host = host

            self.__root_url = "{}{}:{}/{}".format(self.__schema, self.__host,
                                                  self.__port, self.__version)

        if headers:
            self.__headers = requests.utils.default_headers()
            self.__headers.update({**headers})
        else:
            self.__headers = requests.utils.default_headers()

            self.__headers.update({"User-Agent": generate_user_agent()})

        if self.__secure:
            self.__manager = urllib3.PoolManager(
                cert_reqs="CERT_REQUIRED",
                assert_hostname=False,
                ca_certs=self.__cert,
                headers=self.__headers,
            )
            self.__session = self.__manager.request
        else:
            self.__manager = urllib3.PoolManager(headers=self.__headers)
            self.__session = self.__manager.request

    def __del__(self):
        self.close_connection()

    def close_connection(self):
        """
        Close all currently open connections to the OPA server
        """
        try:
            self.__manager.clear()
        except:  # noqa: E722
            pass

    def check_connection(self):
        """
            Checks whether established connection config True or not.
            if not properly configured will raise an ConnectionError.
        """

        url = self.__policy_root.format(self.__root_url, "")
        try:
            response = self.__session(
                "GET", url, retries=self.retries, timeout=self.timeout)
            if response.status == 200:
                return "Yes I'm here :)"

        except Exception:
            raise ConnectionsError("service unreachable",
                                   "check config and try again")

        raise ConnectionsError("service unreachable",
                               "check config and try again")

    def check_health(self, query: Dict[str, bool] = None, diagnostic_url: str = None) -> bool:
        """
            Check OPA healthy. If you want check bundels or plugins, add query params for this.
            If your diagnostic url different than default url, you can provide it.
            ```
            param :: query : it is the url query string. default None
            param :: diagnostic_url : OPA diagnostic url

            example:
                print(client.check_health())
                print(client.check_health({"bundle": True}))
                print(client.check_health(diagnostic_url="http://localhost:8282/health"))
                print(client.check_health(query={"bundle": True}, diagnostic_url="http://localhost:8282/health"))

        """
        if diagnostic_url:
            url = diagnostic_url
        else:
            url = "{}{}:{}/{}".format(self.__schema, self.__host,
                                      self.__port, "health")
        if query:
            url = self.prepare_args(url, query)
        response = self.__session(
            "GET", url, retries=self.retries, timeout=self.timeout)
        if response.status == 200:
            return True
        return False

    def get_policies_list(self) -> list:
        """ Returns all  OPA policies in the service"""

        return self.__get_policies_list()

    def get_policies_info(self) -> dict:
        """ 
            Returns information about each policy, including
            policy path and policy rules
        """
        return self.__get_policies_info()

    def update_opa_policy_fromstring(self, new_policy: str,
                                     endpoint: str) -> bool:
        """ Write your rego policy with using python string type and update your OPA policies.
            ```
            param :: new_policy : is the name of your new defined  rego policy. 
            param :: endpoint : is the path of your new policy in OPA

            example:
                new_policy=
                    package play

                    default hello = false

                    hello {
                        m := input.message
                        m == "world"
                    }
                client = OpaClient()
                client.update_opa_policy_fromstring(new_policy,'exampleapi')
            ```

        """

        return self.__update_opa_policy_fromstring(new_policy, endpoint)

    def update_opa_policy_fromfile(self, filepath: str, endpoint: str) -> bool:
        """ Write your rego policy to file and update your OPA policies. """

        return self.__update_opa_policy_fromfile(filepath, endpoint)

    def update_opa_policy_fromurl(self, url: str, endpoint: str) -> bool:
        """Update your OPA policies from internet."""

        return self.__update_opa_policy_fromurl(url, endpoint)

    def update_or_create_opa_data(self, new_data: dict, endpoint: str) -> bool:
        """ Updates existing data or create new data for policy.
            ```
            param :: new_data : name of defined data
            type  :: new_data : dict
            param :: endpoint : is the path of your new data or existing one in OPA 
            type  :: endpoint : str
            example:
                my_policy_list = [
                    {"resource": "/api/someapi", "identity": "your_identity", "method": "PUT"},
                    {"resource": "/api/someapi", "identity": "your_identity", "method": "GET"},
                ]

                client = OpaClient()
                client.update_or_create_opa_data(my_policy_list,'exampledata/accesses')
            ```
        """

        return self.__update_opa_data(new_data, endpoint)

    def get_opa_raw_data(self, data_name: str = "", query_params: Dict[str, bool] = dict()) -> dict:
        """ Returns OPA raw data in string type 
            ```
            param :: data_name : OPA data name you want get
            param :: query_params : query params in url for more information about metrics
            ```
        """
        return self.__get_opa_raw_data(data_name, query_params)

    def opa_policy_to_file(self,
                           policy_name: str,
                           path: Union[str, None] = None,
                           filename: str = "opa_policy.rego"):
        """ Write OPA service policy to the  file.
        ```
            param :: policy_name : name of OPA policy
            type  :: policy_name : str

            param :: path : path to save file ,default current path
            type  :: path : str

            param :: filename : name of the file,default opa_policy.rego
            type  :: filename : str
        ```

        """
        return self.__opa_policy_to_file(policy_name, path, filename)

    def get_opa_policy(self, policy_name: str) -> dict:
        """Returns full info about policy, provided OPA service """

        return self.__get_opa_policy(policy_name)

    def delete_opa_policy(self, policy_name: str) -> bool:
        """ Deletes given OPA policy name """

        return self.__delete_opa_policy(policy_name)

    def delete_opa_data(self, data_name: str) -> bool:
        """ Deletes given OPA policy data name """

        return self.__delete_opa_data(data_name)

    def check_permission(self, input_data: dict, policy_name: str,
                         rule_name: str, query_params: Dict[str, bool] = dict()) -> dict:
        """
        ```
            params :: input_data    : data which you want check permission
                type   :: input_data  : dict

            params :: policy_name   : the name of policy resource
                type   :: policy_name  : str

            params :: rule_name   : the name included in the policy
                type   :: rule_name  : str
            param :: query_params : query params in url for more information about metrics
                type :: query_params : dict
        ```
        """

        return self.__check(input_data, policy_name, rule_name, query_params)

    def check_policy_rule(self,
                          input_data: dict,
                          package_path: str,
                          rule_name: str = None) -> dict:
        """
        Queries a package rule with the given input data
        """

        return self.__query(input_data, package_path, rule_name)

    def ad_hoc_query(self, *, query_params: Dict[str, str] = None, body: Dict[str, str] = None):
        """ Execute an ad-hoc query and return bindings for variables found in the query. 
        ```
            param :: query_params for sending query string in url
            param :: body  for sending query in request body

        ```
        """

        url = "{}{}:{}/{}/{}".format(self.__schema, self.__host,
                                     self.__port, "v1", "query")
        if body:
            encoded_json = json.dumps(body).encode("utf-8")
            response = self.__session(
                "POST",
                url,
                body=encoded_json,
                retries=self.retries,
                timeout=self.timeout,
            )
        elif query_params:
            url = self.prepare_args(url, query_params)
            response = self.__session("GET",
                                      url,
                                      retries=self.retries,
                                      timeout=self.timeout)
        data = json.loads(response.data.decode("utf-8"))
        if response.status == 200:
            return data

        raise QueryExecuteError(data.get("code"), data.get("message"))

    def prepare_args(self, url: str, query_params: dict) -> str:
        if query_params:
            query_params = urlencode(query_params)
            url = url + "?" + query_params
        return url

    def __get_opa_raw_data(self, data_name: str, query_params: Dict[str, bool]):
        url = self.__data_root.format(self.__root_url, data_name)
        url = self.prepare_args(url, query_params)
        response = self.__session("GET",
                                  url,
                                  retries=self.retries,
                                  timeout=self.timeout)
        code = response.status
        response = json.loads(response.data.decode("utf-8"))
        return response if code == 200 else (code, "not found")

    def __update_opa_data(self, new_data: dict, endpoint: str):
        url = self.__data_root.format(self.__root_url, endpoint)

        encoded_json = json.dumps(new_data).encode("utf-8")
        response = self.__session(
            "PUT",
            url,
            body=encoded_json,
            retries=self.retries,
            timeout=self.timeout,
        )
        code = response.status
        return True if code == 204 else False

    def __update_opa_policy_fromfile(self, filepath: str, endpoint: str):

        if os.path.isfile(filepath):
            with open(filepath, "r") as rf:
                return self.__update_opa_policy_fromstring(rf.read(), endpoint)

        raise FileError(f"{filepath}",
                        "is not a file, make sure you provide a file")

    def __update_opa_policy_fromstring(self, new_policy: str,
                                       endpoint: str) -> bool:

        if not isinstance(new_policy, str) or not isinstance(endpoint, str):
            raise TypeExecption(f"{new_policy} is not string type")

        if new_policy:
            url = self.__policy_root.format(self.__root_url, endpoint)

            response = self.__session(
                "PUT",
                url,
                body=new_policy.encode(),
                headers=self.__headers,
                retries=self.retries,
                timeout=self.timeout,
            )

            if response.status == 200:
                return True

            raise RegoParseError(
                json.loads(response.data.decode()).get("code"),
                json.loads(response.data.decode()).get("message"),
            )

        return False

    def __get_opa_policy(self, policy_name: str) -> dict:
        url = self.__policy_root.format(self.__root_url, policy_name)

        response = self.__session("GET",
                                  url,
                                  retries=self.retries,
                                  timeout=self.timeout)
        data = json.loads(response.data.decode("utf-8"))
        if response.status == 200:

            return data

        raise PolicyNotFoundError(data.get("code"), data.get("message"))

    def __update_opa_policy_fromurl(self, url: str, endpoint: str) -> bool:
        response = requests.get(url, headers=self.__headers)
        return self.__update_opa_policy_fromstring(response.text, endpoint)

    def __opa_policy_to_file(self, policy_name: str, path: Union[str, None],
                             filename: str) -> bool:
        raw_policy = self.__get_opa_policy(policy_name)
        if isinstance(raw_policy, dict):
            try:
                if path:

                    with open(f"{path}/{filename}", "wb") as wr:
                        wr.write(raw_policy.get("result").get("raw").encode())
                else:
                    with open(filename, "wb") as wr:
                        wr.write(raw_policy.get("result").get("raw").encode())
                return True

            except:  # noqa: E722
                raise PathNotFoundError("error when write file",
                                        "path not found")

    def __delete_opa_policy(self, policy_name: str) -> bool:
        url = self.__policy_root.format(self.__root_url, policy_name)

        response = self.__session("DELETE",
                                  url,
                                  retries=self.retries,
                                  timeout=self.timeout)
        data = json.loads(response.data.decode("utf-8"))
        if response.status == 200:
            return True

        raise DeletePolicyError(data.get("code"), data.get("message"))

    def __get_policies_list(self) -> list:
        url = self.__policy_root.format(self.__root_url, "")
        temp = []
        response = self.__session("GET",
                                  url,
                                  retries=self.retries,
                                  timeout=self.timeout,
                                  headers=self.__headers)

        response = json.loads(response.data.decode())

        for policy in response.get("result"):
            if policy.get("id"):
                temp.append(policy.get("id"))

        return temp

    def __delete_opa_data(self, data_name: str) -> bool:
        url = self.__data_root.format(self.__root_url, data_name)

        response = self.__session("DELETE",
                                  url,
                                  retries=self.retries,
                                  timeout=self.timeout)
        if response.data:
            data = json.loads(response.data.decode("utf-8"))
        if response.status == 204:
            return True

        raise DeleteDataError(data.get("code"), data.get("message"))

    def __get_policies_info(self) -> dict:
        url = self.__policy_root.format(self.__root_url, "")
        policy = self.__session("GET",
                                url,
                                retries=self.retries,
                                timeout=self.timeout,
                                headers=self.__headers)

        policy = json.loads(policy.data.decode())
        result = policy.get("result")

        temp_dict = {}
        for policy in result:
            temp_policy = []
            temp_rules = []
            temp_url = ""
            permission_url = self.__root_url
            for path in policy.get("ast").get("package").get("path"):
                permission_url += "/" + path.get("value")
            temp_policy.append(permission_url)
            for rule in policy.get("ast").get("rules"):
                if not rule.get("default"):
                    continue
                temp_url = permission_url
                temp_url += "/" + rule.get("head").get("name")
                temp_rules.append(temp_url)
            temp_dict[policy.get("id")] = {
                "path": temp_policy,
                "rules": temp_rules
            }

        return temp_dict

    def __check(self, input_data: dict, policy_name: str,
                rule_name: str, query_params: Dict[str, bool]) -> dict:
        url = self.__policy_root.format(self.__root_url, policy_name)

        policy = self.__session("GET",
                                url,
                                headers=self.__headers,
                                retries=self.retries,
                                timeout=self.timeout)
        policy = json.loads(policy.data.decode("utf-8"))
        result = policy.get("result")
        find = False
        permission_url = self.__root_url
        if result:
            for path in result.get("ast").get("package").get("path"):
                permission_url += "/" + path.get("value")

            for rule in result.get("ast").get("rules"):
                if not rule.get("default"):
                    continue
                if rule.get("head").get("name") == rule_name:

                    permission_url += "/" + rule.get("head").get("name")
                    find = True
        if find:
            encoded_json = json.dumps(input_data).encode("utf-8")
            permission_url = self.prepare_args(permission_url, query_params)
            response = self.__session("POST",
                                      permission_url,
                                      body=encoded_json,
                                      retries=self.retries,
                                      timeout=self.timeout)
            if response.data:
                data = json.loads(response.data.decode("utf-8"))
                return data

        raise CheckPermissionError(f"{rule_name} rule not found",
                                   "path or rule name not correct")

    def __query(self,
                input_data: dict,
                package_path: str,
                rule_name: str = None) -> dict:
        if '.' in package_path:
            package_path = package_path.replace('.', '/')
        if rule_name:
            package_path = package_path + '/' + rule_name
        url = self.__data_root.format(self.__root_url, package_path)

        encoded_json = json.dumps({'input': input_data}).encode("utf-8")
        response = self.__session("POST",
                                  url,
                                  body=encoded_json,
                                  retries=self.retries,
                                  timeout=self.timeout)
        if response.data:
            data = json.loads(response.data.decode("utf-8"))
            return data

        raise CheckPermissionError(f"{rule_name} rule not found",
                                   "policy or rule name not correct")

    @property
    def _host(self):
        return self.__host

    @property
    def _port(self):
        return self.__port

    @property
    def _version(self):
        return self.__version

    @property
    def _root_url(self):
        return self.__root_url

    @property
    def _schema(self):
        return self.__schema

    @property
    def _policy_root(self):
        return self.__policy_root

    @property
    def _data_root(self):
        return self.__data_root

    @property
    def _secure(self):
        return self.__secure

    @property
    def _ssl(self):
        return self.__ssl

    @property
    def _cert(self):
        return self.__cert

############################################ # ________      ________    ________       # # |\   __  \    |\   __  \  |\   __  \     # # \ \  \|\  \   \ \  \|\  \ \ \  \|\  \    # #  \ \  \\\  \   \ \   ____\ \ \   __  \   # #   \ \  \\\  \   \ \  \___|  \ \  \ \  \  # #    \ \_______\   \ \__\      \ \__\ \__\ # #     \|_______|    \|__|       \|__|\|__| # ############################################ OpaClient client object to connect and manipulate OPA service.         ```         The class object holds session information necesseary to connect OPA service.         param :: host : to connect OPA service ,defaults to localhost         type  :: host: str         param :: port : to connect OPA service ,defaults to 8181         type  :: port : str or int         param :: version : provided REST API version by OPA,defaults to v1         type  :: version : str         param :: ssl : verify ssl certificates for https requests,defaults to False         type  :: ssl : bool         param :: cert : path to client certificate information to use for mutual TLS authentification         type  :: cert : str         param :: headers :  dictionary of headers to send, defaults to None         ``` Close all currently open connections to the OPA server # noqa: E722 Checks whether established connection config True or not.             if not properly configured will raise an ConnectionError. Check OPA healthy. If you want check bundels or plugins, add query params for this.             If your diagnostic url different than default url, you can provide it.             ```             param :: query : it is the url query string. default None             param :: diagnostic_url : OPA diagnostic url              example:                 print(client.check_health())                 print(client.check_health({"bundle": True}))                 print(client.check_health(diagnostic_url="http://localhost:8282/health"))                 print(client.check_health(query={"bundle": True}, diagnostic_url="http://localhost:8282/health")) Returns all  OPA policies in the service Returns information about each policy, including             policy path and policy rules Write your rego policy with using python string type and update your OPA policies.             ```             param :: new_policy : is the name of your new defined  rego policy.              param :: endpoint : is the path of your new policy in OPA              example:                 new_policy=                     package play                      default hello = false                      hello {                         m := input.message                         m == "world"                     }                 client = OpaClient()                 client.update_opa_policy_fromstring(new_policy,'exampleapi')             ``` Write your rego policy to file and update your OPA policies. Update your OPA policies from internet. Updates existing data or create new data for policy.             ```             param :: new_data : name of defined data             type  :: new_data : dict             param :: endpoint : is the path of your new data or existing one in OPA              type  :: endpoint : str             example:                 my_policy_list = [                     {"resource": "/api/someapi", "identity": "your_identity", "method": "PUT"},                     {"resource": "/api/someapi", "identity": "your_identity", "method": "GET"},                 ]                  client = OpaClient()                 client.update_or_create_opa_data(my_policy_list,'exampledata/accesses')             ``` Returns OPA raw data in string type              ```             param :: data_name : OPA data name you want get             param :: query_params : query params in url for more information about metrics             ``` Write OPA service policy to the  file.         ```             param :: policy_name : name of OPA policy             type  :: policy_name : str              param :: path : path to save file ,default current path             type  :: path : str              param :: filename : name of the file,default opa_policy.rego             type  :: filename : str         ``` Returns full info about policy, provided OPA service Deletes given OPA policy name Deletes given OPA policy data name ```             params :: input_data    : data which you want check permission                 type   :: input_data  : dict              params :: policy_name   : the name of policy resource                 type   :: policy_name  : str              params :: rule_name   : the name included in the policy                 type   :: rule_name  : str             param :: query_params : query params in url for more information about metrics                 type :: query_params : dict         ``` Queries a package rule with the given input data Execute an ad-hoc query and return bindings for variables found in the query.          ```             param :: query_params for sending query string in url             param :: body  for sending query in request body          ``` # noqa: E722