#pylint: disable=invalid-name, missing-function-docstring, line-too-long, logging-fstring-interpolation, missing-class-docstring, missing-module-docstring
import logging
import json
import time
from typing import Optional, List #Any, Iterator, , Union
import re
import requests
import urllib3
from .connection import Connection
from .transport_capacity import TransportCapacity
from .constellation import Constellation

# https://confluence.infinera.com/display/CR/XR+Network+Service
# https://confluence.infinera.com/pages/viewpage.action?spaceKey=CR&title=XR+Network+Connection+Service#XRNetworkConnectionService-North-boundInterface
# https://bitbucket.infinera.com/projects/XRCM/repos/cm-api/browse/yaml/ncs/v1/ncs.yaml

LOGGER = logging.getLogger(__name__)

class ExpiringValue:
    def __init__(self, value, expiry):
        self.__value = value
        self.__expiry = expiry
        self.__created = time.monotonic()

    def get_value(self):
        return self.__value

    def is_valid_for(self, duration):
        if self.__created + self.__expiry >= time.monotonic()+duration:
            return True
        else:
            return False

class CmConnection:
    def __init__(self, address: str, port: int, username: str, password: str, timeout=30, tls_verify=True) -> None:
        self.__tls_verify = tls_verify
        if not tls_verify:
            urllib3.disable_warnings()

        self.__timeout = timeout
        self.__username = username
        self.__password = password
        self.__cm_root = 'https://' + address + ':' + str(port)
        self.__access_token = None

    def __post_w_headers(self, path, data, headers, data_as_json=True):
        url = self.__cm_root + path
        try:
            if data_as_json:
                response = requests.post(url, headers=headers, json=data, timeout=self.__timeout, verify=self.__tls_verify)
            else:
                response = requests.post(url, headers=headers, data=data, timeout=self.__timeout, verify=self.__tls_verify)

            LOGGER.info(f"POST: {url} ==> {response.status_code=}, {response.text=}")
            resp = json.loads(response.text)
            return (response.status_code, resp)
        except requests.exceptions.Timeout:
            LOGGER.info(f"POST: {url} ==> timeout")
            return None
        except json.JSONDecodeError as json_err:
            LOGGER.info(f"POST: {url} ==> response json decode error: {str(json_err)}")
            return None
        except Exception as e:  # pylint: disable=broad-except
            es=str(e)
            LOGGER.info(f"POST: {url} ==> unexpected exception: {es}")
            return None

    def __post(self, path, data, data_as_json=True):
        return self.__post_w_headers(path, data, self.__http_headers(), data_as_json=data_as_json)

    def __put(self, path, data, data_as_json=True):
        url = self.__cm_root + path
        headers = self.__http_headers()
        try:
            if data_as_json:
                response = requests.put(url, headers=headers, json=data, timeout=self.__timeout, verify=self.__tls_verify)
            else:
                response = requests.put(url, headers=headers, data=data, timeout=self.__timeout, verify=self.__tls_verify)

            LOGGER.info(f"PUT: {url} ==> {response.status_code}")

            if  response.content == b'null':
                return (response.status_code, None)
            resp = json.loads(response.text)
            return (response.status_code, resp)
        except requests.exceptions.Timeout:
            LOGGER.info(f"PUT: {url} ==> timeout")
            return None
        except json.JSONDecodeError as json_err:
            LOGGER.info(f"PUT: {url} ==> response json decode error: {str(json_err)}")
            return None
        except Exception as e:  # pylint: disable=broad-except
            es=str(e)
            LOGGER.info(f"PUT: {url} ==> unexpected exception: {es}")
            return None

    def __delete(self, path, data=None):
        url = self.__cm_root + path
        headers = self.__http_headers()
        try:
            response = requests.delete(url, headers=headers, data=data, timeout=self.__timeout, verify=self.__tls_verify)
            LOGGER.info(f"DELETE: {url} ==> {response.status_code}")

            if  response.content == b'null':
                return (response.status_code, None)
            resp = json.loads(response.text)
            return (response.status_code, resp)
        except requests.exceptions.Timeout:
            LOGGER.info(f"DELETE: {url} ==> timeout")
            return None
        except json.JSONDecodeError as json_err:
            LOGGER.info(f"DELETE: {url} ==> response json decode error: {str(json_err)}")
            return None
        except Exception as e:  # pylint: disable=broad-except
            es=str(e)
            LOGGER.info(f"DELETE: {url} ==> unexpected exception: {es}")
            return None

    def __http_headers(self):
        self.__ensure_valid_access_token()
        if self.__access_token:
            return {'Authorization': 'Bearer '+ self.__access_token.get_value()}
        else:
            return {}

    def __get_json(self, path, params=None):
        url = self.__cm_root + path
        try:
            response = requests.get(url,headers=self.__http_headers(), timeout=self.__timeout,verify=self.__tls_verify, params=params)
            LOGGER.info(f"GET: {url} {params=} ==> {response.status_code}")
            resp = json.loads(response.text)
            return (response.status_code, resp)
        except requests.exceptions.Timeout:
            LOGGER.info(f"GET: {url} {params=} ==> timeout")
            return None
        except json.JSONDecodeError as json_err:
            LOGGER.info(f"GET: {url} {params=} ==> response json decode error: {str(json_err)}")
            return None
        except Exception as e:  # pylint: disable=broad-except
            es=str(e)
            LOGGER.info(f"GET: {url} {params=} ==> unexpected exception: {es}")
            return None

    def __acquire_access_token(self):
        path = '/realms/xr-cm/protocol/openid-connect/token'
        req = {
            "username": self.__username,
            "password": self.__password,
            "grant_type": "password",
            "client_secret": "xr-web-client",
            "client_id": "xr-web-client"
        }
        (status_code, response) = self.__post_w_headers(path, req, None, data_as_json=False)
        if 200 != status_code or 'access_token' not in response:
            LOGGER.error(f"Authentication failure, status code {status_code}, data {response}")
            return False
        access_token = response['access_token']
        expires = int(response["expires_in"]) if "expires_in" in response else 0
        LOGGER.info(f"Obtained access token {access_token}, expires in {expires}")
        self.__access_token = ExpiringValue(access_token, expires)
        return True

    def __ensure_valid_access_token(self):
        if not self.__access_token or not self.__access_token.is_valid_for(60):
            self.__acquire_access_token()

    def Connect(self) -> bool:
        return self.__acquire_access_token()

    def list_constellations(self) -> List[Constellation]:
        status_code, constellations = self.__get_json("/api/v1/ns/xr-networks?content=expanded")
        if not constellations or status_code != 200:
            return []
        return [Constellation(c) for c in constellations]


    def get_constellation_by_hub_name(self, hub_module_name: str) -> Optional[Constellation]:
        qparams = [
            ('content', 'expanded'),
            ('q', '{"hubModule.state.module.moduleName": "' + hub_module_name + '"}')
        ]
        status_code, constellations = self.__get_json("/api/v1/ns/xr-networks?content=expanded", params=qparams)
        if not constellations or status_code != 200 or len(constellations) != 1:
            return None
        return Constellation(constellations[0])

    def get_transport_capacities(self) -> List[TransportCapacity]:
        status_code, tc = self.__get_json("/api/v1/ns/transport-capacities?content=expanded")
        if not tc or status_code != 200:
            return []
        return [TransportCapacity(from_json=t) for t in tc]

    def get_transport_capacity_by_name(self, tc_name: str) -> Optional[Connection]:
        qparams = [
            ('content', 'expanded'),
            ('q', '{"state.name": "' + tc_name + '"}')
        ]
        r = self.__get_json("/api/v1/ns/transport-capacities?content=expanded", params=qparams)
        if r and r[0] == 200 and len(r[1]) == 1:
            return TransportCapacity(from_json=r[1][0])
        else:
            return None

    def get_transport_capacity_by_teraflow_uuid(self, uuid: str) -> Optional[Connection]:
        return self.get_transport_capacity_by_name(f"TF:{uuid}")

    def create_transport_capacity(self, tc: TransportCapacity) -> Optional[str]:
        # Create wants a list, so wrap connection to list
        tc_config = [tc.create_config()]
        resp = self.__post("/api/v1/ns/transport-capacities", tc_config)
        if resp and resp[0] == 202 and len(resp[1]) == 1 and "href" in resp[1][0]:
            tc.href = resp[1][0]["href"]
            LOGGER.info(f"Created transport-capcity {tc}")
            #LOGGER.info(self.__get_json(f"/api/v1/ns/transport-capacities{tc.href}?content=expanded"))
            return tc.href
        else:
            return None

    def delete_transport_capacity(self, href: str) -> bool:
        resp = self.__delete(f"/api/v1/ns/transport-capacities{href}")

        # Returns empty body
        if resp and resp[0] == 202:
            LOGGER.info(f"Deleted transport-capacity {href=}")
            return True
        else:
            LOGGER.info(f"Deleting transport-capacity {href=} failed, status {resp[0]}")
            return False

    def create_connection(self, connection: Connection) -> Optional[str]:
        # Create wants a list, so wrap connection to list
        cfg = [connection.create_config()]

        resp = self.__post("/api/v1/ncs/network-connections", cfg)
        if resp and resp[0] == 202 and len(resp[1]) == 1 and "href" in resp[1][0]:
            connection.href = resp[1][0]["href"]
            LOGGER.info(f"Created connection {connection}")
            return connection.href
        else:
            LOGGER.error(f"Create failure for connection {connection}, result {resp}")
            return None

    def update_connection(self, href: str, connection: Connection, existing_connection: Optional[Connection]=None) -> Optional[str]:
        cfg = connection.create_config()

        # Endpoint updates
        # Current CM implementation returns 501 (not implemented) for all of these actions

        # CM does not accept endpoint updates properly in same format that is used in initial creation.
        # Instead we work around by using more granular APIs.
        if "endpoints" in cfg:
            del cfg["endpoints"]
        if existing_connection is None:
            existing_connection = self.get_connection_by_href(href)
        print(existing_connection)
        ep_deletes, ep_creates, ep_updates = connection.get_endpoint_updates(existing_connection)
        #print(ep_deletes)
        #print(ep_creates)
        #print(ep_updates)

        # Perform deletes
        for ep_href in ep_deletes:
            resp = self.__delete(f"/api/v1/ncs{ep_href}")
            if resp and resp[0] == 202:
                LOGGER.info(f"update_connection: EP-UPDATE: Deleted connection endpoint {ep_href}")
            else:
                LOGGER.info(f"update_connection: EP-UPDATE: Failed to delete connection endpoint {ep_href}: {resp}")

        # Update capacities for otherwise similar endpoints
        for ep_href, ep_cfg in ep_updates:
            resp = self.__put(f"/api/v1/ncs{ep_href}", ep_cfg)
            if resp and resp[0] == 202:
                LOGGER.info(f"update_connection: EP-UPDATE: Updated connection endpoint {ep_href} with {ep_cfg}")
            else:
                LOGGER.info(f"update_connection: EP-UPDATE: Failed to update connection endpoint {ep_href} with {ep_cfg}: {resp}")

        # Perform adds
        resp = self.__post(f"/api/v1/ncs{href}/endpoints", ep_creates)
        if resp and resp[0] == 202 and len(resp[1]) == len(ep_creates):
            LOGGER.info(f"update_connection: EP-UPDATE: Created connection endpoints {resp[1]} with {ep_creates}")
        else:
            LOGGER.info(f"update_connection: EP-UPDATE: Failed to create connection endpoints {resp[1]} with {ep_creates}: {resp}")

        # Connection update (excluding endpoints)
        resp = self.__put(f"/api/v1/ncs{href}", cfg)
        # Returns empty body
        if resp and resp[0] == 202:
            LOGGER.info(f"update_connection: Updated connection {connection}")
            # Return href used for update to be consisten with create
            return href
        else:
            LOGGER.error(f"update_connection: Update failure for connection {connection}, result {resp}")
            return None

    def delete_connection(self, href: str) -> bool:
        resp = self.__delete(f"/api/v1/ncs{href}")
        #print(resp)
        # Returns empty body
        if resp and resp[0] == 202:
            LOGGER.info(f"Deleted connection {href=}")
            return True
        else:
            return False

    # Always does the correct thing, that is update if present, otherwise create
    def create_or_update_connection(self, connection: Connection) -> Optional[str]:
        existing_connection = self.get_connection_by_name(connection.name)
        if existing_connection:
            return self.update_connection(existing_connection.href, connection)
        else:
            return self.create_connection(connection)

    def get_connection_by_name(self, connection_name: str) -> Optional[Connection]:
        qparams = [
            ('content', 'expanded'),
            ('q', '{"state.name": "' + connection_name + '"}')
        ]
        r = self.__get_json("/api/v1/ncs/network-connections", params=qparams)
        if r and r[0] == 200 and len(r[1]) == 1:
            return Connection(from_json=r[1][0])
        else:
            return None

    def get_connection_by_href(self, href: str) -> Optional[Connection]:
        qparams = [
            ('content', 'expanded'),
        ]
        r = self.__get_json(f"/api/v1/ncs{href}", params=qparams)
        if r and r[0] == 200:
            return Connection(from_json=r[1])
        else:
            return None

    def get_connection_by_teraflow_uuid(self, uuid: str) -> Optional[Connection]:
        return self.get_connection_by_name(f"TF:{uuid}")

    def get_connections(self):
        r = self.__get_json("/api/v1/ncs/network-connections?content=expanded")
        if r and r[0] == 200:
            return [Connection(from_json=c) for c in r[1]]
        else:
            return []

    def service_uuid(self, key: str) -> Optional[str]:
        service = re.match(r"^/service\[(.+)\]$", key)
        if service:
            return service.group(1)
        else:
            return None
