# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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.

"""Utilities used by the client library."""

import logging
import re
import threading


from lxml import etree
import zeep


_COMMON_FILTER = None
LOGGER_FORMAT = '[%(asctime)s - %(name)s - %(levelname)s] %(message)s'


def GetGoogleAdsCommonFilter():
  global _COMMON_FILTER
  if not _COMMON_FILTER:
    _COMMON_FILTER = _GoogleAdsCommonFilter()
  return _COMMON_FILTER


class _AbstractDevTokenSOAPFilter(logging.Filter):
  """Interface for sanitizing logs containing SOAP request/response data."""

  _AUTHORIZATION_HEADER = 'authorization'
  _DEVELOPER_TOKEN_SUB = re.compile(
      r'(?<=\<(?:tns|ns0):developerToken\>)'
      r'.*?'
      r'(?=\</(?:tns|ns0):developerToken\>)')
  _REDACTED = 'REDACTED'

  def filter(self, record):
    raise NotImplementedError('You must implement filter().')


class _GoogleAdsCommonFilter(_AbstractDevTokenSOAPFilter):
  """Removes sensitive data from logs generated by googleads.common."""

  def filter(self, record):
    if record.levelno == logging.INFO and record.args:
      content = record.args[0].str()
      content = self._DEVELOPER_TOKEN_SUB.sub(self._REDACTED, content)
      record.args = (content,)
    return True


_RESPONSE_XML_LOG_LINE = 'Incoming response: \n%s'
_REQUEST_LOG_LINE = 'Request made: Service: "%s" Method: "%s" URL: "%s"'
_REQUEST_XML_LOG_LINE = 'Outgoing request: %s\n%s'
_SOAP_NAMESPACE = 'http://schemas.xmlsoap.org/soap/envelope/'
_FAULT_XPATH = './/{%s}Fault' % _SOAP_NAMESPACE
_HEADER_XPATH = './/{%s}Header' % _SOAP_NAMESPACE
_REMOVE_NS_REGEXP = re.compile(r'^\{.*?\}')


class ZeepLogger(zeep.Plugin, _AbstractDevTokenSOAPFilter):
  """Log zeep request/response data while removing sensitive information."""

  def __init__(self):
    """Instantiates a new ZeepLogger."""
    super(_AbstractDevTokenSOAPFilter, self).__init__()
    self._logger = logging.getLogger('googleads.soap')

  def ingress(self, envelope, http_headers, operation):
    """Overrides the ingress function for response logging.

    Args:
      envelope: An Element with the SOAP request data.
      http_headers: A dict of the current http headers.
      operation: The SoapOperation instance.

    Returns:
      A tuple of the envelope and headers.
    """
    if self._logger.isEnabledFor(logging.DEBUG):
      self._logger.debug(_RESPONSE_XML_LOG_LINE,
                         etree.tostring(envelope, pretty_print=True))

    if self._logger.isEnabledFor(logging.WARN):
      warn_data = {}
      header = envelope.find(_HEADER_XPATH)
      fault = envelope.find(_FAULT_XPATH)
      if fault is not None:
        warn_data['faultMessage'] = fault.find('faultstring').text

        if header is not None:
          header_data = {
              re.sub(_REMOVE_NS_REGEXP, '', child.tag): child.text
              for child in header[0]}
          warn_data.update(header_data)

        if 'serviceName' not in warn_data:
          warn_service_name = operation.binding.wsdl.services.keys()
          warn_data['serviceName'] = list(warn_service_name)[0]

        if 'methodName' not in warn_data:
          warn_data['methodName'] = operation.name

        self._logger.warn('Error summary: %s', warn_data)

    return envelope, http_headers

  def egress(self, envelope, http_headers, operation, binding_options):
    """Overrides the egress function ror request logging.

    Args:
      envelope: An Element with the SOAP request data.
      http_headers: A dict of the current http headers.
      operation: The SoapOperation instance.
      binding_options: An options dict for the SOAP binding.

    Returns:
      A tuple of the envelope and headers.
    """
    if self._logger.isEnabledFor(logging.INFO):
      service_keys = operation.binding.wsdl.services.keys()
      service_name = list(service_keys)[0]
      self._logger.info(_REQUEST_LOG_LINE, service_name, operation.name,
                        binding_options['address'])

    if self._logger.isEnabledFor(logging.DEBUG):
      http_headers_safe = http_headers.copy()
      if self._AUTHORIZATION_HEADER in http_headers_safe:
        http_headers_safe[self._AUTHORIZATION_HEADER] = self._REDACTED

      request_string = etree.tostring(envelope, pretty_print=True)
      safe_request = self._DEVELOPER_TOKEN_SUB.sub(
          self._REDACTED, request_string.decode('utf-8'))
      self._logger.debug(
          _REQUEST_XML_LOG_LINE, http_headers_safe, safe_request)

    return envelope, http_headers


class UtilityRegistry(object):
  """Utility that registers product utilities used in generating a request."""

  def __contains__(self, utility):
    with self._lock:
      return utility in self._registry

  def __init__(self):
    self._enabled = True
    self._registry = set()
    self._lock = threading.Lock()

  def __iter__(self):
    with self._lock:
      return iter(self._registry.copy())

  def __len__(self):
    with self._lock:
      return len(self._registry)

  def Add(self, obj):
    with self._lock:
      if self._enabled:
        self._registry.add(obj)

  def Clear(self):
    with self._lock:
      self._registry.clear()

  def SetEnabled(self, value):
    with self._lock:
      self._enabled = value
