import base64
import urllib3
import requests
import logging
import copy


LOGGER = logging.getLogger(__name__)


class Bluecat:
    """
    An HTTP client API calls to perform the BlueCAT update and delete
    """

    def __init__(self, url, username,password):
        self.url = url
        self.username = username
        self.password = password

    def get_bluecat_detail_configuration(self):
        with RequestClient(self.url, self.username, self.password) as client:
            client.login()
            response = client.http_get("/configurations")
            client.logout()
        return response

    def get_bluecat_configuration(self):
        all_config = self.get_bluecat_detail_configuration()
        configurations = all_config.json()["data"]
        config = []
        for configuration in configurations:
            config.append({configuration["id"]: configuration["name"]})
        return config

    def get_bluecat_all_networks(self):
        with RequestClient(self.url, self.username, self.password) as client:
            client.login()
            response = client.http_get("/networks")
            client.logout()
        return response

    def get_bluecat_all_zones(self):
        with RequestClient(self.url, self.username, self.password) as client:
            client.login()
            response = client.http_get("/zones")
            client.logout()
        return response

    def get_bluecat_zone_filterby_domain(self, domain, configuration):
        with RequestClient(self.url, self.username, self.password) as client:
            client.login()
            params = {
                "filter": f'type:eq("Zone") and absoluteName:eq("{domain}") \
                and configuration.name:eq("{configuration}")'
            }
            response = client.http_get("/zones", params=params)
            client.logout()
        return response

    def get_bluecat_network_filterby_type_range_config(
        self, net_type, network, configuration
    ):
        with RequestClient(self.url, self.username, self.password) as client:
            client.login()
            params = {
                "filter": f'type:eq("{net_type}") and range:eq("{network}") \
                    and configuration.name:eq("{configuration}")'
            }
            response = client.http_get("/networks", params=params)
            client.logout()
        return response

    def get_bluecat_network_addresses_filterby_type_config(
        self, ip_addr, configuration
    ):
        with RequestClient(self.url, self.username, self.password) as client:
            client.login()
            params = {
                "filter": f'address:eq("{ip_addr}") and configuration.name:eq("{configuration}")'
            }
            response = client.http_get("/addresses", params=params)
            client.logout()
        return response

    def post_bluecat_networkAddresses_in_networks(
        self, network_id, net_type, state, ipaddr
    ):
        with RequestClient(self.url, self.username, self.password) as client:
            client.login()
            json01 = {"type": net_type, "state": state, "address": ipaddr}
            response = client.http_post(
                f"/networks/{network_id}/addresses", json=json01
            )
            client.logout()
        return response

    def post_bluecat_resource_record_in_zone(
        self, zone_id, _type, name, absolute_name, addresses, user_defined_field
    ):
        with RequestClient(self.url, self.username, self.password) as client:
            client.login()
            json01 = {
                "type": _type,
                "name": name,
                "absoluteName": absolute_name,
                "addresses": addresses,
                "userDefinedFields": user_defined_field,
            }
            response = client.http_post(
                f"/zones/{zone_id}/resourceRecords", json=json01
            )
            client.logout()
        return response

    def delete_bluecat_networkAddress_in_networks(self, ip_addr_id):
        with RequestClient(self.url, self.username, self.password) as client:
            client.login()
            response = client.http_delete(f"/addresses/{ip_addr_id}")
            client.logout()
        return response


class RequestClient:
    def __init__(self, url, username, password):
        self.username = username
        self.password = password
        self.url = urllib3.util.url.parse_url(url)
        self.session = requests.Session()
        self.auth = None
        self._last_response = None
        self.__session_data = None

    def _http_request(
        self,
        method,
        url,
        params=None,
        data=None,
        auth=None,
        headers=None,
        timeout=None,
        verify=None,
        json=None,
        expected_status_codes=None,
    ):
        url = self.url._replace(path="/api/v2").url + url
        verify = self.session.verify if verify is None else verify
        try:
            response = self.session.request(
                method=method,
                url=url,
                params=params,
                data=data,
                headers=headers,
                timeout=timeout,
                auth=auth,
                verify=verify,
                json=json,
            )
            self._last_response = response
        except Exception as e:
            self._last_response = None
            raise BADResponse(f"Error in HTTP communication with target service. {e}")
        if not response.ok:
            raise BADResponse(
                f"Response with HTTP code: {response.status_code} \n {response.json()}"
            )
        if expected_status_codes and response.status_code not in expected_status_codes:
            raise BADResponse("Response with unexpected HTTP status code.", response)
        return response

    def http_post(
        self,
        url,
        params=None,
        data=None,
        json=None,
        expected_status_codes=None,
        verify=None,
        **kwargs,
    ):
        return self._http_request(
            "POST",
            url,
            params,
            data,
            json=json,
            verify=verify,
            expected_status_codes=expected_status_codes,
            **kwargs,
        )

    def http_get(
        self,
        url,
        params=None,
        json=None,
        expected_status_codes=None,
        verify=None,
        **kwargs,
    ):
        if expected_status_codes is None:
            expected_status_codes = (requests.codes.ok,)
        return self._http_request(
            "GET",
            url,
            params,
            json=json,
            expected_status_codes=expected_status_codes,
            verify=verify,
            **kwargs,
        )

    def http_delete(
        self,
        url,
        params=None,
        data=None,
        json=None,
        expected_status_codes=None,
        verify=None,
        **kwargs,
    ):
        return self._http_request(
            "DELETE",
            url,
            params,
            data,
            json=json,
            expected_status_codes=expected_status_codes,
            verify=verify,
            **kwargs,
        )

    def http_patch(
        self,
        url,
        params=None,
        data=None,
        json=None,
        expected_status_codes=None,
        verify=None,
        **kwargs,
    ):
        return self._http_request(
            "PATCH",
            url,
            params,
            data,
            json=json,
            expected_status_codes=expected_status_codes,
            verify=verify,
            **kwargs,
        )

    def login(self, optional: dict = None):
        password = decode_ipreg_cookie(self.password)
        data = {"username": self.username, "password": password}

        if optional:
            data.update(optional)

        response = self.http_post("/sessions", json=data)
        self.__session_data = response.json()
        self.auth = "Basic " + self.__session_data["basicAuthenticationCredentials"]
        self.session.headers.update({"Authorization": self.auth})
        return copy.deepcopy(self.__session_data)

    def logout(self) -> dict:
        """
        Log user out from BAM and clear any stored authentication token.

        :return: The logged-out session information is returned by BAM API v2.
        """
        # Added in the initial version: 23.1.0
        sessions_id = self.__session_data["id"]
        data = {"state": "LOGGED_OUT"}
        headers = {"Content-Type": "application/merge-patch+json"}
        res = self.http_patch(f"/sessions/{sessions_id}", json=data, headers=headers)
        self.__session_data = None
        del self.auth
        return res

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.session.close()


class BADResponse(Exception):

    def __init__(self, message, *args, **kwargs):
        self.message = message
        super().__init__(*args, **kwargs)

    def __str__(self):
        return self.message


def decode_ipreg_cookie(ipreg_cookie):
    """
    Base64-decoding of ipgreg cookie.

    :param ipreg_cookie: string to base64-decode.
    :return: decoded ipreg_cookie as a string or None on error.
    """
    try:
        decoded = base64.b64decode(ipreg_cookie.encode("ascii")).decode("ascii")
        return decoded.strip()
    except Exception as error:
        LOGGER.error("IPREG_COOKIE_B64_ENCODED cannot be decoded. Error: %s", error)
        return None


def add_bluecat_ipreg(url, username, password,
    name, fqdn, ipaddr, configuration, network, domain, ifs_contact, remark
):
    """To register the IP in the Bluecat

    Args:
        name (str): Name of the IP to register e.g "ifs-tim-svm-22-cifs"
        fqdn (str): Fully Qualified name for IP e.g: "ifs-tim-svm-22-cifs.ifs.uis.private.cam.ac.uk"
        ipaddr (str): IP address to reserve e.g: "10.128.7.14"
        configuration (str): Registeration to be performed in which configuration eg: "Playground" or "Production"
        network (str): IP to be register in which network information e.g: network = "10.128.7.0/24"
        domain (str): Search the network in the Domain e.g: domain = "uis.private.cam.ac.uk"
        ifs_contact (str): details of the requestor.
        remark (str): Any remark for the IP to register.

    Returns:
        bool: True if the DNS is register success otherwise false
    """
    blue = Bluecat(url, username, password)
    bluecat_zone_response = blue.get_bluecat_zone_filterby_domain(domain, configuration)
    if not bluecat_zone_response.ok:
        LOGGER.error("No response from get_bluecat_zone_filterby_domain")
        return False

    bluecat_zone_id = bluecat_zone_response.json()["data"][0]["id"]
    bluecat_network_response = blue.get_bluecat_network_filterby_type_range_config(
        "IPv4Network", network, configuration
    )

    if not bluecat_network_response.ok:
        LOGGER.error("No response from get_bluecat_network_filterby_type_range_config")
        return False

    bluecat_network_id = bluecat_network_response.json()["data"][0]["id"]
    # Assign fixed IP on Network
    ipaddress_response = blue.post_bluecat_networkAddresses_in_networks(
        bluecat_network_id, "IPv4Address", "STATIC", ipaddr
    )
    if not ipaddress_response.ok:
        LOGGER.error(
            f"IP Address post call for post_bluecat_networkAddresses_in_networks : \
                     Failed \n {ipaddress_response.json()}"
        )
        return False

    LOGGER.info(
        f"Assigned IP response in configuration {configuration}: {ipaddress_response.json()}"
    )

    address_id = ipaddress_response.json()["id"]
    record_type = "HostRecord"
    name = name
    absolute_name = f"{fqdn}"
    addresses = [{"id": address_id, "type": "IPv4Address"}]
    user_defined_fields = {
        "equipment": "Bluecat",
        "location": "Bluecat",
        "owner": ifs_contact,
        "enduser": ifs_contact,
        "sysadmin": ifs_contact,
        "remark": remark,
    }

    bluecat_post_zone_response = blue.post_bluecat_resource_record_in_zone(
        bluecat_zone_id,
        record_type,
        name,
        absolute_name,
        addresses,
        user_defined_fields,
    )

    if not bluecat_post_zone_response.ok:
        LOGGER.error(
            f"IP Address post call for post_bluecat_networkAddresses_in_networks \
                     : Failed \n  {bluecat_post_zone_response.json()}"
        )
        return False
    LOGGER.info(
        f"Host Record is updated in configuration {configuration}: {bluecat_post_zone_response.json()}"
    )
    return True


def delete_bluecat_ipreg(url, username, password, ip_address, configuration="Playground"):
    """To delete the IP address from the bluecat

    Args:
        ip_address (str): IP address need to be free up from the Bluecat
        configuration (str, optional): Registeration to be performed in which configuration
                                       eg: "Playground" or "Production". Defaults to "Playground".

    Returns:
        bool: True if the IP free up successfully otherwise false
    """
    blue = Bluecat(url, username, password)
    bluecat_ipaddress_response = (
        blue.get_bluecat_network_addresses_filterby_type_config(
            ip_address, configuration
        )
    )
    if not ("data" in bluecat_ipaddress_response.json()):
        LOGGER.error(
            "No response from get_bluecat_network_addresses_filterby_type_config"
        )
        return False

    ip_address_id = bluecat_ipaddress_response.json()["data"][0]["id"]
    delete_bluecat_address_response = blue.delete_bluecat_networkAddress_in_networks(
        ip_address_id
    )
    if not delete_bluecat_address_response.ok:
        LOGGER.error("Deletion has been failed : delete_bluecat_address_response")
        return False
    LOGGER.info(
        f"Succefully deleted IP {ip_address} from configuration {configuration}."
    )
    return True
