# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import os
import re
import socket
import string
import threading

from newrelic.common.agent_http import InsecureHttpClient
from newrelic.common.encoding_utils import json_decode
from newrelic.core.internal_metrics import internal_count_metric

_logger = logging.getLogger(__name__)
VALID_CHARS_RE = re.compile(r'[0-9a-zA-Z_ ./-]')

class UtilizationHttpClient(InsecureHttpClient):
    SOCKET_TIMEOUT = 0.05

    def send_request(self, *args, **kwargs):
        sock = socket.socket()
        sock.settimeout(self.SOCKET_TIMEOUT)

        # If we cannot connect to the metadata host in SOCKET_TIMEOUT time,
        # this will raise an exception, terminating the fetch before attempting
        # to use an http client.
        # This is an optimization which will speed up connect.
        try:
            sock.connect((self._host, self._port))
        finally:
            sock.close()

        return super(UtilizationHttpClient, self).send_request(*args, **kwargs)


class CommonUtilization(object):
    METADATA_HOST = ''
    METADATA_PATH = ''
    METADATA_QUERY = None
    HEADERS = None
    EXPECTED_KEYS = ()
    VENDOR_NAME = ''
    FETCH_TIMEOUT = 0.4
    CLIENT_CLS = UtilizationHttpClient

    @classmethod
    def record_error(cls, resource, data):
        # As per spec
        internal_count_metric(
                'Supportability/utilization/%s/error' % cls.VENDOR_NAME, 1)
        _logger.warning('Invalid %r data (%r): %r',
                cls.VENDOR_NAME, resource, data)

    @classmethod
    def fetch(cls):
        try:
            with cls.CLIENT_CLS(cls.METADATA_HOST,
                                timeout=cls.FETCH_TIMEOUT) as client:
                resp = client.send_request(method='GET',
                                           path=cls.METADATA_PATH,
                                           params=cls.METADATA_QUERY,
                                           headers=cls.HEADERS)
            if not 200 <= resp[0] < 300:
                raise ValueError(resp[0])
            return resp[1]
        except Exception as e:
            _logger.debug('Unable to fetch %s data from %s%s: %r',
                    cls.VENDOR_NAME, cls.METADATA_HOST, cls.METADATA_PATH, e)
            return None

    @classmethod
    def get_values(cls, response):
        if response is None:
            return

        try:
            return json_decode(response.decode('utf-8'))
        except ValueError:
            _logger.debug('Invalid %s data (%s%s): %r',
                    cls.VENDOR_NAME, cls.METADATA_HOST,
                    cls.METADATA_PATH, response)

    @classmethod
    def valid_chars(cls, data):
        if data is None:
            return False

        for c in data:
            if not VALID_CHARS_RE.match(c) and ord(c) < 0x80:
                return False

        return True

    @classmethod
    def valid_length(cls, data):
        if data is None:
            return False

        b = data.encode('utf-8')
        valid = len(b) <= 255
        if valid:
            return True

        return False

    @classmethod
    def normalize(cls, key, data):
        if data is None:
            return

        try:
            stripped = data.strip()

            if (stripped and cls.valid_length(stripped) and
                    cls.valid_chars(stripped)):
                return stripped
        except:
            pass

    @classmethod
    def sanitize(cls, values):
        if values is None:
            return

        out = {}
        for key in cls.EXPECTED_KEYS:
            metadata = values.get(key, None)
            if not metadata:
                cls.record_error(key, metadata)
                return

            normalized = cls.normalize(key, metadata)
            if not normalized:
                cls.record_error(key, metadata)
                return

            out[key] = normalized

        return out

    @classmethod
    def detect(cls):
        response = cls.fetch()
        values = cls.get_values(response)
        return cls.sanitize(values)


class AWSUtilization(CommonUtilization):
    EXPECTED_KEYS = ('availabilityZone', 'instanceId', 'instanceType')
    METADATA_HOST = '169.254.169.254'
    METADATA_PATH = '/2016-09-02/dynamic/instance-identity/document'
    VENDOR_NAME = 'aws'


class AzureUtilization(CommonUtilization):
    METADATA_HOST = '169.254.169.254'
    METADATA_PATH = '/metadata/instance/compute'
    METADATA_QUERY = {'api-version': '2017-03-01'}
    EXPECTED_KEYS = ('location', 'name', 'vmId', 'vmSize')
    HEADERS = {'Metadata': 'true'}
    VENDOR_NAME = 'azure'


class GCPUtilization(CommonUtilization):
    EXPECTED_KEYS = ('id', 'machineType', 'name', 'zone')
    HEADERS = {'Metadata-Flavor': 'Google'}
    METADATA_HOST = 'metadata.google.internal'
    METADATA_PATH = '/computeMetadata/v1/instance/'
    METADATA_QUERY = {'recursive': 'true'}
    VENDOR_NAME = 'gcp'

    @classmethod
    def normalize(cls, key, data):
        if data is None:
            return

        if key in ('machineType', 'zone'):
            formatted = data.strip().split('/')[-1]
        elif key == 'id':
            formatted = str(data)
        else:
            formatted = data

        return super(GCPUtilization, cls).normalize(key, formatted)


class PCFUtilization(CommonUtilization):
    EXPECTED_KEYS = ('cf_instance_guid', 'cf_instance_ip', 'memory_limit')
    VENDOR_NAME = 'pcf'

    @staticmethod
    def fetch():
        cf_instance_guid = os.environ.get('CF_INSTANCE_GUID')
        cf_instance_ip = os.environ.get('CF_INSTANCE_IP')
        memory_limit = os.environ.get('MEMORY_LIMIT')
        pcf_vars = (cf_instance_guid, cf_instance_ip, memory_limit)
        if all(pcf_vars):
            return pcf_vars

    @classmethod
    def get_values(cls, response):
        if response is None or len(response) != 3:
            return

        values = {}
        for k, v in zip(cls.EXPECTED_KEYS, response):
            if hasattr(v, 'decode'):
                v = v.decode('utf-8')
            values[k] = v
        return values


class DockerUtilization(CommonUtilization):
    VENDOR_NAME = 'docker'
    EXPECTED_KEYS = ('id',)
    METADATA_FILE = '/proc/self/cgroup'
    DOCKER_RE = re.compile(r'([0-9a-f]{64,})')

    @classmethod
    def fetch(cls):
        try:
            with open(cls.METADATA_FILE, 'rb') as f:
                for line in f:
                    stripped = line.decode('utf-8').strip()
                    cgroup = stripped.split(':')
                    if len(cgroup) != 3:
                        continue
                    subsystems = cgroup[1].split(',')
                    if 'cpu' in subsystems:
                        return cgroup[2]
        except:
            # There are all sorts of exceptions that can occur here
            # (i.e. permissions, non-existent file, etc)
            pass

    @classmethod
    def get_values(cls, contents):
        if contents is None:
            return

        value = contents.split('/')[-1]
        match = cls.DOCKER_RE.search(value)
        if match:
            value = match.group(0)
            return {'id': value}

    @classmethod
    def valid_chars(cls, data):
        if data is None:
            return False

        hex_digits = set(string.hexdigits)

        valid = all((c in hex_digits for c in data))
        if valid:
            return True

        return False

    @classmethod
    def valid_length(cls, data):
        if data is None:
            return False

        # Must be exactly 64 characters
        valid = len(data) == 64
        if valid:
            return True

        return False


class KubernetesUtilization(CommonUtilization):
    EXPECTED_KEYS = ('kubernetes_service_host', )
    VENDOR_NAME = 'kubernetes'

    @staticmethod
    def fetch():
        kubernetes_service_host = os.environ.get('KUBERNETES_SERVICE_HOST')
        if kubernetes_service_host:
            return kubernetes_service_host

    @classmethod
    def get_values(cls, v):
        if v is None:
            return

        if hasattr(v, 'decode'):
            v = v.decode('utf-8')

        return {'kubernetes_service_host': v}
