"""
P4 driver plugin for the TeraFlow SDN controller.
"""

import logging
import threading
from typing import Any, Iterator, List, Optional, Tuple, Union
from .p4_util import P4RuntimeClient,\
    P4_ATTR_DEV_ID, P4_ATTR_DEV_NAME, P4_ATTR_DEV_VENDOR,\
    P4_ATTR_DEV_HW_VER, P4_ATTR_DEV_SW_VER, P4_ATTR_DEV_PIPECONF,\
    P4_VAL_DEF_VENDOR, P4_VAL_DEF_HW_VER, P4_VAL_DEF_SW_VER, P4_VAL_DEF_PIPECONF

try:
    from _Driver import _Driver
except ImportError:
    from device.service.driver_api._Driver import _Driver

LOGGER = logging.getLogger(__name__)


class P4Driver(_Driver):
    """
    P4Driver class inherits the abstract _Driver class to support P4 devices.

    Attributes
    ----------
    address : str
        IP address of the P4Runtime server running on the P4 device
    port : int
        transport port number of the P4Runtime server running on the P4 device
    **settings : map
        id : int
            P4 device ID (Mandatory)
        name : str
            P4 device name (Optional)
        vendor : str
            P4 device vendor (Optional)
        hw_ver : str
            Hardware version of the P4 device (Optional)
        sw_ver : str
            Software version of the P4 device (Optional)
        pipeconf : str
            P4 device table configuration (Optional)
    """

    def __init__(self, address: str, port: int, **settings) -> None:
        # pylint: disable=super-init-not-called
        self.__client = None
        self.__address = address
        self.__port = int(port)
        self.__settings = settings

        try:
            self.__dev_id = self.__settings.get(P4_ATTR_DEV_ID)
        except Exception as ex:
            LOGGER.error('P4 device ID is a mandatory setting')
            raise Exception from ex

        if P4_ATTR_DEV_NAME in self.__settings:
            self.__dev_name = self.__settings.get(P4_ATTR_DEV_NAME)
        else:
            self.__dev_name = str(self.__dev_id)
            LOGGER.warning(
                'No device name is provided. Setting default name: %s',
                self.__dev_name)

        if P4_ATTR_DEV_VENDOR in self.__settings:
            self.__dev_vendor = self.__settings.get(P4_ATTR_DEV_VENDOR)
        else:
            self.__dev_vendor = P4_VAL_DEF_VENDOR
            LOGGER.warning(
                'No vendor is provided. Setting default vendor: %s',
                self.__dev_vendor)

        if P4_ATTR_DEV_HW_VER in self.__settings:
            self.__dev_hw_version = self.__settings.get(P4_ATTR_DEV_HW_VER)
        else:
            self.__dev_hw_version = P4_VAL_DEF_HW_VER
            LOGGER.warning(
                'No HW version is provided. Setting default HW version: %s',
                self.__dev_hw_version)

        if P4_ATTR_DEV_SW_VER in self.__settings:
            self.__dev_sw_version = self.__settings.get(P4_ATTR_DEV_SW_VER)
        else:
            self.__dev_sw_version = P4_VAL_DEF_SW_VER
            LOGGER.warning(
                'No SW version is provided. Setting default SW version: %s',
                self.__dev_sw_version)

        if P4_ATTR_DEV_PIPECONF in self.__settings:
            self.__dev_pipeconf = self.__settings.get(P4_ATTR_DEV_PIPECONF)
        else:
            self.__dev_pipeconf = P4_VAL_DEF_PIPECONF
            LOGGER.warning(
                'No P4 pipeconf is provided. Setting default P4 pipeconf: %s',
                self.__dev_pipeconf)

        self.__lock = threading.Lock()
        self.__started = threading.Event()
        self.__terminate = threading.Event()

        LOGGER.info('Initializing P4 device at %s:%d with settings:',
                    self.__address, self.__port)

        for key, value in settings.items():
            LOGGER.info('\t%8s = %s', key, value)

    def Connect(self) -> bool:
        """
        Establishes a connection between the P4 device driver and a P4 device.

        :return: boolean connection status.
        """
        LOGGER.info(
            'Connecting to P4 device %s:%d ...',
            self.__address, self.__port)

        with self.__lock:
            # Skip if already connected
            if self.__started.is_set():
                return True

            # Instantiate a gRPC channel with the P4 device
            grpc_address = f'{self.__address}:{self.__port}'
            election_id = (1, 0)
            self.__client = P4RuntimeClient(
                self.__dev_id, grpc_address, election_id)
            LOGGER.info('\tConnected!')
            self.__started.set()

            return True

    def Disconnect(self) -> bool:
        """
        Terminates the connection between the P4 device driver and a P4 device.

        :return: boolean disconnection status.
        """
        LOGGER.info(
            'Disconnecting from P4 device %s:%d ...',
            self.__address, self.__port)

        # If not started, assume it is already disconnected
        if not self.__started.is_set():
            return True

        # gRPC client must already be instantiated
        assert self.__client

        # Trigger termination of loops and processes
        self.__terminate.set()

        # Trigger connection tear down with the P4Runtime server
        self.__client.tear_down()
        self.__client = None

        LOGGER.info('\tDisconnected!')

        return True

    def GetInitialConfig(self) -> List[Tuple[str, Any]]:
        """
        Retrieves the initial configuration of a P4 device.

        :return: list of initial configuration items.
        """
        LOGGER.info('P4 GetInitialConfig()')
        return []

    def GetConfig(self, resource_keys : List[str] = [])\
            -> List[Tuple[str, Union[Any, None, Exception]]]:
        """
        Retrieves the current configuration of a P4 device.

        :param resource_keys: configuration parameters to retrieve.
        :return: list of values associated with the requested resource keys.
        """

        LOGGER.info('P4 GetConfig()')
        return []

    def SetConfig(self, resources : List[Tuple[str, Any]])\
            -> List[Union[bool, Exception]]:
        """
        Submits a new configuration to a P4 device.

        :param resources: configuration parameters to set.
        :return: list of results for resource key changes requested.
        """
        LOGGER.info('P4 SetConfig()')
        return []

    def DeleteConfig(self, resources : List[Tuple[str, Any]])\
            -> List[Union[bool, Exception]]:
        """
        Revokes P4 device configuration.

        :param resources: list of tuples with resource keys to be deleted.
        :return: list of results for resource key deletions requested.
        """
        LOGGER.info('P4 DeleteConfig()')
        return []

    def GetResource(self, endpoint_uuid : str) -> Optional[str]:
        """
        Retrieves a certain resource from a P4 device.

        :param endpoint_uuid: target endpoint UUID.
        :return: The path of the endpoint or None if not found.
        """
        LOGGER.info('P4 GetResource()')
        return ""

    def GetState(self, blocking=False) -> Iterator[Tuple[str, Any]]:
        """
        Retrieves the state of a P4 device.

        :param blocking: if non-blocking, the driver terminates the loop and
        returns.
        :return: sequences of state sample.
        """
        LOGGER.info('P4 GetState()')
        return []

    def SubscribeState(self, subscriptions : List[Tuple[str, float, float]])\
            -> List[Union[bool, Exception]]:
        """
        Subscribes to certain state information.

        :param subscriptions: list of tuples with resources to be subscribed.
        :return: list of results for resource subscriptions requested.
        """
        LOGGER.info('P4 SubscribeState()')
        return []

    def UnsubscribeState(self, subscriptions : List[Tuple[str, float, float]])\
            -> List[Union[bool, Exception]]:
        """
        Unsubscribes from certain state information.

        :param subscriptions: list of tuples with resources to be unsubscribed.
        :return: list of results for resource un-subscriptions requested.
        """
        LOGGER.info('P4 UnsubscribeState()')
        return []
