import requests
import logging
import jsonschema

from requests.exceptions import HTTPError
from enum import Enum, auto
from functools import reduce

logger = logging.getLogger(__name__)


class INTERFACE_TYPES(Enum):
    UNKNOWN = auto()
    LOGICAL = auto()
    PHYSICAL = auto()
    AGGREGATE = auto()


class BRIAN_DASHBOARDS(Enum):
    CLS = auto()
    RE_PEER = auto()
    RE_CUST = auto()
    GEANTOPEN = auto()
    GCS = auto()
    L2_CIRCUIT = auto()
    LHCONE_PEER = auto()
    LHCONE_CUST = auto()
    MDVPN_CUSTOMERS = auto()
    INFRASTRUCTURE_BACKBONE = auto()
    IAS_PRIVATE = auto()
    IAS_PUBLIC = auto()
    IAS_CUSTOMER = auto()
    IAS_UPSTREAM = auto()
    GWS_PHY_UPSTREAM = auto()
    GBS_10G = auto()

    # aggregate dashboards
    CLS_PEERS = auto()
    IAS_PEERS = auto()
    GWS_UPSTREAMS = auto()
    LHCONE = auto()
    CAE1 = auto()
    IC1 = auto()
    COPERNICUS = auto()
    ANA = auto()

    # NREN customer
    NREN = auto()


class PORT_TYPES(Enum):
    ACCESS = auto()
    SERVICE = auto()
    UNKNOWN = auto()


# only used in INTERFACE_LIST_SCHEMA and sphinx docs
_DASHBOARD_IDS = [d.name for d in list(BRIAN_DASHBOARDS)]

_PORT_TYPES = [t.name for t in list(PORT_TYPES)]

_INTERFACE_TYPES = [i.name for i in list(INTERFACE_TYPES)]

ROUTER_INTERFACES_SCHEMA = {
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "description": {"type": "string"},
            "router": {"type": "string"},
            "bundle": {
                "type": "array",
                "items": {"type": "string"}
            },
            "ipv4": {
                "type": "array",
                "items": {"type": "string"}
            },
            "ipv6": {
                "type": "array",
                "items": {"type": "string"}
            },
            "logical-system": {"type": "string"},
        },
        "required": ["name", "router", "ipv4", "ipv6"]
    }
}

INTERFACE_LIST_SCHEMA = {
    'definitions': {
        'service': {
            'type': 'object',
            'properties': {
                'id': {'type': 'integer'},
                'name': {'type': 'string'},
                'type': {'type': 'string'},
                'status': {'type': 'string'},
            },
            'required': ['id', 'name', 'type', 'status']
        },
        'db_info': {
            'type': 'object',
            'properties': {
                'name': {'type': 'string'},
                'interface_type': {'type': 'string'},
            },
            'required': ['name', 'interface_type']
        },
        'interface': {
            'type': 'object',
            'properties': {
                'router': {'type': 'string'},
                'name': {'type': 'string'},
                'description': {'type': 'string'},
                'dashboards': {
                    'type': 'array',
                    'items': {'type': 'string'}
                },
                'dashboards_info': {
                    'type': 'array',
                    'items': {'$ref': '#/definitions/db_info'}
                },
                'port_type': {'enum': _PORT_TYPES}
            },
            'required': [
                'router', 'name', 'description',
                'dashboards']
        },
    },

    'type': 'array',
    'items': {'$ref': '#/definitions/interface'}
}

GWS_DIRECT_DATA_SCHEMA = {
    'definitions': {
        'oid': {
            'type': 'string',
            'pattern': r'^(\d+\.)*\d+$'
        },
        'snmp-v2': {
            'type': 'object',
            'properties': {
                'community': {'type': 'string'}
            },
            'required': ['community']
        },
        'snmp-v3-cred': {
            'type': 'object',
            'properties': {
                'protocol': {'enum': ['MD5', 'DES']},
                'password': {'type': 'string'}
            },
            'required': ['protocol', 'password']
        },
        'snmp-v3': {
            'type': 'object',
            'properties': {
                'sec-name': {'type': 'string'},
                'auth': {'$ref': '#/definitions/snmp-v3-cred'},
                'priv': {'$ref': '#/definitions/snmp-v3-cred'}
            },
            'required': ['sec-name']
        },
        'counter': {
            'type': 'object',
            'properties': {
                'field': {
                    'enum': [
                        'discards_in',
                        'discards_out',
                        'errors_in',
                        'errors_out',
                        'traffic_in',
                        'traffic_out'
                    ]
                },
                'oid': {'$ref': '#/definitions/oid'},
                'snmp': {
                    'oneOf': [
                        {'$ref': '#/definitions/snmp-v2'},
                        {'$ref': '#/definitions/snmp-v3'}
                    ]
                }
            },
            'required': ['field', 'oid']
        },
        'interface-counters': {
            'type': 'object',
            'properties': {
                'nren': {'type': 'string'},
                'isp': {'type': 'string'},
                'hostname': {'type': 'string'},
                'tag': {'type': 'string'},
                'counters': {
                    'type': 'array',
                    'items': {'$ref': '#/definitions/counter'},
                    'minItems': 1
                },
                'info': {'type': 'string'}
            },
            'required': ['nren', 'isp', 'hostname', 'tag', 'counters']
        }
    },

    'type': 'array',
    'items': {'$ref': '#/definitions/interface-counters'}
}

MULTICAST_SUBSCRIPTION_LIST_SCHEMA = {
    'definitions': {
        'ipv4-address': {
            'type': 'string',
            'pattern': r'^(\d+\.){3}\d+$'
        },
        'subscription': {
            'type': 'object',
            'properties': {
                'router': {'type': 'string'},
                'subscription': {'$ref': '#/definitions/ipv4-address'},
                'endpoint': {'$ref': '#/definitions/ipv4-address'},
                'oid': {
                    'type': 'string',
                    'pattern': r'^(\d+\.)*\d+$'
                },
                'community': {'type': 'string'}
            },
            'required': [
                'router', 'subscription', 'endpoint', 'oid', 'community']
        },
    },

    'type': 'array',
    'items': {'$ref': '#/definitions/subscription'}
}

NREN_REGION_LIST_SCHEMA = {
    'definitions': {
        'nren_region': {
            'type': 'object',
            'properties': {
                'nren': {'type': 'string'},
                'region': {'type': 'string'}
            }
        }
    },

    'type': 'array',
    'items': {'$ref': '#/definitions/nren_region'}
}


def _get_ip_info(host):
    """
    Get IP information for all interfaces on all routers.

    :param host: Hostname to perform the request to.
    :return: A lookup table of the form:
        {
            'router1': {
                'interface1': {
                    'ipv4': [
                        '62.40.109.193/30'
                    ],
                    'ipv6': [
                        '2001:798:cc:1::4a/126'
                    ]
                },
                'interface2': {
                    'ipv4': [
                        '62.40.109.193/30'
                    ],
                    'ipv6': [
                        '2001:798:cc:1::4a/126'
                    ]
                }
            },
            'router2': {
                'interface1': {
                    'ipv4': [
                        '62.40.109.193/30'
                    ],
                    'ipv6': [
                        '2001:798:cc:1::4a/126'
                    ]
                },
            }
        }
    """

    def reduce_func(prev, curr):
        """
        Reduce function to build the lookup table.

        :param prev: The accumulator. The lookup table.
        :param curr: The current interface.
        :return: The updated lookup table.
        """

        interface_name = curr.get('name')
        router_name = curr.get('router')
        if interface_name and router_name:
            router = prev.get(router_name, {})
            interface = router.get(interface_name, {})
            ipv4 = curr.get('ipv4', [])
            ipv6 = curr.get('ipv6', [])
            interface['ipv4'] = ipv4
            interface['ipv6'] = ipv6
            router[interface_name] = interface
            prev[router_name] = router

        return prev
    try:
        r = requests.get(f'{host}/data/interfaces')
        r.raise_for_status()
        interfaces = r.json()
    except HTTPError:
        logger.exception('Failed to get IP info')
        interfaces = []

    jsonschema.validate(interfaces, ROUTER_INTERFACES_SCHEMA)
    return reduce(reduce_func, interfaces, {})


def get_interfaces(host):
    """
    Get all interfaces that have dashboards assigned to them.

    :param host: Hostname to perform the request to.
    :return: A list of interfaces with IP information added, if present.
    """

    r = requests.get(f'{host}/poller/interfaces')
    try:
        r.raise_for_status()
        interfaces = r.json()
    except HTTPError:
        logger.exception('Failed to get interfaces')
        interfaces = []

    jsonschema.validate(interfaces, INTERFACE_LIST_SCHEMA)

    ip_info = _get_ip_info(host)

    def enrich(interface):
        router_name = interface.get('router')
        router = ip_info.get(router_name)
        if not router:
            return interface

        ip = router.get(interface['name'])
        if not ip:
            return interface
        ipv4 = ip['ipv4']
        ipv6 = ip['ipv6']
        interface['ipv4'] = ipv4
        interface['ipv6'] = ipv6
        return interface

    filtered = filter(lambda i: len(i['dashboards']) > 0, interfaces)
    enriched = list(map(enrich, filtered))
    return enriched


def get_gws_direct(host):
    """
    Get all GWS Direct data.
    Follows the schema defined in GWS_DIRECT_DATA_SCHEMA.

    :param host: Hostname to perform the request to.
    :return: GWS direct data
    """

    r = requests.get(f'{host}/poller/gws/direct')
    try:
        r.raise_for_status()
        interfaces = r.json()
    except HTTPError:
        logger.exception('Failed to get GWS direct data')
        interfaces = []

    jsonschema.validate(interfaces, GWS_DIRECT_DATA_SCHEMA)
    return interfaces


def get_gws_indirect(host):
    """
    Get all GWS Indirect data.

    :param host: Hostname to perform the request to.
    :return: GWS Indirect data
    """
    try:
        r = requests.get(f'{host}/poller/gws/indirect')
        r.raise_for_status()
        interfaces = r.json()
    except HTTPError:
        logger.exception('Failed to get GWS indirect data')
        interfaces = []
    return interfaces


def get_eumetsat_multicast_subscriptions(host):
    """
    Get all EUMETSAT multicast subscriptions.

    :param host: Hostname to perform the request to.
    :return: EUMETSAT multicast subscriptions
    """
    try:
        r = requests.get(f'{host}/poller/eumetsat-multicast')
        r.raise_for_status()
        data = r.json()
    except HTTPError:
        logger.exception('Failed to get EUMETSAT multicast subscriptions')
        data = []

    jsonschema.validate(data, MULTICAST_SUBSCRIPTION_LIST_SCHEMA)
    return data


def get_nren_regions(host):
    """
    Get all NREN regions, where specified.

    :param host: Hostname to perform the request to.
    :return: A list of NRENs and regions.
    """
    try:
        r = requests.get(f'{host}/poller/regions')
        r.raise_for_status()
        data = r.json()
    except HTTPError:
        logger.exception('Failed to get NREN regions')
        data = []

    jsonschema.validate(data, NREN_REGION_LIST_SCHEMA)
    # ugly hack: if a region isn't on the list of recognised regions, prune it from the region list
    # TODO: figure out something that isn't this
    recognised_regions = ['EAP']
    data = [d for d in data if d['region'] in recognised_regions]
    return data