import logging
import time
import zlib
import sys
import socket
import os

from ..packages import six

from ..packages import requests
from ..packages import simplejson as json

from .. import version as agent_version

from .addresses import platform_url, proxy_details
from .exceptions import (NetworkInterfaceException, ForceAgentRestart,
        ForceAgentDisconnect, DiscardDataForRequest, RetryDataForRequest,
        ServerIsUnavailable)

_logger = logging.getLogger(__name__)

# User agent string that must be used in all requests. The data collector
# does not rely on this, but is used to target specific agents if there
# is a problem with data collector handling requests.

USER_AGENT = 'NewRelic-PythonAgent/%s (Python %s %s)' % (
         agent_version, sys.version.split()[0], sys.platform)

# Platform agent collector interface.

class PlatformInterface(object):

    def __init__(self, license_key, host='platform-api.newrelic.com',
            port=None, ssl=True, timeout=30.0, proxy_host=None,
            proxy_port=None, proxy_user=None, proxy_pass=None):

        self.license_key = license_key

        self.host = host
        self.port = port
        self.ssl = ssl

        self.timeout = timeout

        self.proxy_host = proxy_host
        self.proxy_port = proxy_port
        self.proxy_user = proxy_user
        self.proxy_pass = proxy_pass

    def send_request(self, url, proxies, session, payload=()):
        """Constructs and sends a request to the data collector."""

        headers = {}
        config = {}

        start = time.time()

        # Validate that the license key was actually set and if not replace
        # it with a string which makes it more obvious it was not set.

        license_key = self.license_key

        if not self.license_key:
            license_key = 'NO LICENSE KEY WAS SET IN AGENT CONFIGURATION'

        headers['User-Agent'] = USER_AGENT
        headers['Content-Encoding'] = 'identity'
        headers['X-License-Key'] = license_key

        # At this time we use JSON content encoding for the data being
        # sent. Ensure that normal byte strings are interpreted as Latin-1
        # and that the final result is ASCII so that don't need to worry
        # about converting back to bytes again. We set the default fallback
        # encoder to treat any iterable as a list. Unfortunately the JSON
        # library can't use it as an iterable and so means that generator
        # will be consumed up front and everything collected in memory as a
        # list before then converting to JSON.
        #
        # If an error does occur when encoding the JSON, then it isn't
        # likely going to work later on in a subsequent request with same
        # data, even if aggregated with other data, so we need to log the
        # details and then flag that data should be thrown away. Don't mind
        # being noisy in the the log in this situation as it would indicate
        # a problem with the implementation of the agent.

        try:
            data = json.dumps(payload, ensure_ascii=True, encoding='Latin-1')

        except Exception as exc:
            _logger.error('Error encoding data for JSON payload '
                    'with payload of %r. Exception which occurred was %r. '
                    'Please report this problem to New Relic support.',
                    payload, exc)

            raise DiscardDataForRequest(str(exc))

        # Log details of call and/or payload for debugging. Use the JSON
        # encoded value so know that what is encoded is correct.

        _logger.debug('Calling data collector to report custom metrics '
                'with payload=%r.', data)

        # Compress the serialized JSON being sent as content if over 64KiB
        # in size. If less than 2MB in size compress for speed. If over
        # 2MB then compress for smallest size. This parallels what the Ruby
        # agent does.

        if len(data) > 64*1024:
            headers['Content-Encoding'] = 'deflate'
            level = (len(data) < 2000000) and 1 or 9
            data = zlib.compress(six.b(data), level)

        # If there is no requests session object provided for making
        # requests create one now. We want to close this as soon as we
        # are done with it.

        auto_close_session = False

        if not session:
            session = requests.session()
            auto_close_session = True

        # The 'requests' library can raise a number of exception derived
        # from 'RequestException' before we even manage to get a connection
        # to the data collector. The data collector can the generate a
        # number of different types of HTTP errors for requests.

        try:
            r = session.post(url, headers=headers, proxies=proxies,
                    timeout=self.timeout, data=data)

            # Read the content now so we can force close the socket
            # connection if this is a transient session as quickly
            # as possible.

            content = r.content

        except requests.RequestException as exc:
            if not self.proxy_host or not self.proxy_port:
                _logger.warning('Data collector is not contactable. This can '
                        'be because of a network issue or because of the data '
                        'collector being restarted. In the event that contact '
                        'cannot be made after a period of time then please '
                        'report this problem to New Relic support for further '
                        'investigation. The error raised was %r.', exc)
            else:
                _logger.warning('Data collector is not contactable via the '
                        'proxy host %r on port %r with proxy user of %r. This '
                        'can be because of a network issue or because of the '
                        'data collector being restarted. In the event that '
                        'contact cannot be made after a period of time then '
                        'please report this problem to New Relic support for '
                        'further investigation. The error raised was %r.',
                        self.proxy_host, self.proxy_port, self.proxy_user, exc)

            raise RetryDataForRequest(str(exc))

        finally:
            if auto_close_session:
                session.close()
                session = None

        if r.status_code != 200:
            _logger.debug('Received a non 200 HTTP response from the data '
                    'collector where url=%r, license_key=%r, headers=%r, '
                    'status_code=%r and content=%r.', url, license_key,
                    headers, r.status_code, r.content)

        if r.status_code == 400:
            if headers['Content-Encoding'] == 'deflate':
                data = zlib.decompress(data)

            _logger.error('Data collector is indicating that a bad '
                    'request has been submitted for url %r, headers of %r '
                    'and payload of %r with response of %r. Please report '
                    'this problem to New Relic support.', url, headers, data,
                    r.content)

            raise DiscardDataForRequest()

        elif r.status_code == 403:
            _logger.error('Data collector is indicating that the license '
                    'key %r is not valid.', license_key)

            raise DiscardDataForRequest()

        elif r.status_code == 413:
            _logger.warning('Data collector is indicating that a request '
                    'was received where the request content size '
                    'was over the maximum allowed size limit. The length of '
                    'the request content was %d. If this keeps occurring on a '
                    'regular basis, please report this problem to New Relic '
                    'support for further investigation.', len(data))

            raise DiscardDataForRequest()

        elif r.status_code in  (503, 504):
            _logger.warning('Data collector is unavailable. This can be a '
                    'transient issue because of the data collector or our '
                    'core application being restarted. If the issue persists '
                    'it can also be indicative of a problem with our servers. '
                    'In the event that availability of our servers is not '
                    'restored after a period of time then please report this '
                    'problem to New Relic support for further investigation.')

            raise ServerIsUnavailable()

        elif r.status_code != 200:
            if not self.proxy_host or not self.proxy_port:
                _logger.warning('An unexpected HTTP response was received '
                        'from the data collector of %r. The payload for '
                        'the request was %r. If this issue persists then '
                        'please report this problem to New Relic support '
                        'for further investigation.', r.status_code, payload)
            else:
                _logger.warning('An unexpected HTTP response was received '
                        'from the data collector of %r while connecting '
                        'via proxy host %r on port %r with proxy user of %r. '
                        'The payload for the request was %r. If this issue '
                        'persists then please report this problem to New '
                        'Relic support for further investigation.',
                        r.status_code, self.proxy_host, self.proxy_port,
                        self.proxy_user, payload)

            raise DiscardDataForRequest()

        # Log details of response payload for debugging. Use the JSON
        # encoded value so know that what original encoded value was.

        duration = time.time() - start

        _logger.debug('Valid response from data collector after %.2f '
                'seconds with content=%r.', duration, r.content)

        # If we got this far we should have a legitimate response from the
        # data collector. The response is JSON so need to decode it.
        # Everything will come back as Unicode. Make sure all strings are
        # decoded as 'UTF-8'.

        try:
            result = json.loads(r.content, encoding='UTF-8')

        except Exception as exc:
            _logger.error('Error decoding data for JSON payload '
                    'with payload of %r. Exception which occurred was %r. '
                    'Please report this problem to New Relic support.',
                    r.content, exc)

            raise DiscardDataForRequest(str(exc))

        # The decoded JSON can be either for a successful response or an
        # error. A successful response has a 'return_value' element and an
        # error an 'exception' element.

        if 'status' in result:
            return result['status']

        error_message = result['error']

        # Now need to check for server side exceptions. The following
        # exceptions can occur for abnormal events.

        _logger.debug('Received an exception from the data collector where '
                'url=%r, license_key=%r, headers=%r and error_message=%r. ',
                url, license_key, headers, error_message)

        raise DiscardDataForRequest(error_message)

    def create_session(self):
        url = platform_url(self.host, self.port, self.ssl)

        proxies = proxy_details(None, self.proxy_host, self.proxy_port,
                self.proxy_user, self.proxy_pass)

        return PlatformSession(self, url, proxies)

class PlatformSession(object):

    def __init__(self, interface, platform_url, http_proxies):
        self._interface = interface
        self._platform_url = platform_url
        self._http_proxies = http_proxies
        self._requests_session_object = None

    @property
    def _requests_session(self):
        if self._requests_session_object is None:
            self._requests_session_object = requests.session()
        return self._requests_session_object

    def close_connection(self):
        if self._requests_session_object:
            self._requests_session_object.close()
        self._requests_session_object = None

    def send_metric_data(self, name, guid, version, duration, metrics):
        agent = {}
        agent['host'] = socket.gethostname()
        agent['pid'] = os.getpid()
        agent['version'] = version or '0.0.0.'

        component = {}
        component['name'] = name
        component['guid'] = guid
        component['duration'] = duration
        component['metrics'] = metrics

        payload = {}
        payload['agent'] = agent
        payload['components'] = [component]

        return self._interface.send_request(self._platform_url,
                self._http_proxies, self._requests_session, payload)
