from zope.interface import implementer
from slapos.grid.promise import interface
from slapos.grid.promise.generic import GenericPromise

import datetime
import email.utils
import json
import os
import time
from six.moves.urllib.parse import urlparse
import operator


@implementer(interface.IPromise)
class RunPromise(GenericPromise):
  EXTENDED_STATUS_CODE_MAPPING = {
    '520': 'Too many redirects',
    '523': 'Connection error',
    '524': 'Connection timeout',
    '526': 'SSL Error',

  }

  def __init__(self, config):
    super(RunPromise, self).__init__(config)
    # Set frequency compatible to default surykatka interval - 2 minutes
    self.setPeriodicity(float(self.getConfig('frequency', 2)))
    self.failure_amount = int(
      self.getConfig('failure-amount', self.getConfig('failure_amount', 1)))
    self.result_count = self.failure_amount
    self.error = False
    self.message_list = []
    # Make promise test-less, as it's result is not important for instantiation
    self.setTestLess()

  def appendMessage(self, message):
    self.message_list.append(message)

  def emitLog(self):
   if self.error:
     emit = self.logger.error
   else:
     emit = self.logger.info

   url = self.getConfig('url')
   if url:
     self.message_list.insert(0, '%s :' % (url,))
   emit(' '.join(self.message_list))

  def senseBotStatus(self):
    key = 'bot_status'

    def appendError(msg, *args):
      self.error = True
      self.appendMessage(key + ': ERROR ' + msg % args)

    if key not in self.surykatka_json:
      appendError("%r not in %r", key, self.json_file)
      return
    bot_status_list = self.surykatka_json[key]
    if len(bot_status_list) == 0:
      appendError("%r empty in %r", key, self.json_file)
      return
    bot_status = bot_status_list[0]
    if bot_status.get('text') != 'loop':
      appendError(
        "bot_status is %r instead of 'loop' in %r",
        str(bot_status.get('text')), self.json_file)
      return
    timetuple = email.utils.parsedate(bot_status['date'])
    last_bot_datetime = datetime.datetime.fromtimestamp(time.mktime(timetuple))
    delta = self.utcnow - last_bot_datetime
    # sanity check
    if delta < datetime.timedelta(minutes=0):
      appendError('Last bot datetime is in future')
      return
    if delta > datetime.timedelta(minutes=15):
      appendError('Last bot datetime is more than 15 minutes old')
      return

    self.appendMessage('%s: OK Last bot status' % (key,))

  def senseSslCertificate(self):
    key = 'ssl_certificate'

    def appendError(msg, *args):
      self.error = True
      self.appendMessage(key + ': ERROR ' + msg % args)

    url = self.getConfig('url')
    parsed_url = urlparse(url)
    if parsed_url.scheme == 'https':
      hostname = parsed_url.netloc
      ssl_check = True
      certificate_expiration_days = self.getConfig(
        'certificate-expiration-days', '15')
      try:
        certificate_expiration_days = int(certificate_expiration_days)
      except ValueError:
        certificate_expiration_days = None
    else:
      ssl_check = False
      certificate_expiration_days = None
    if not ssl_check:
      self.appendMessage('%s: OK No check needed' % (key,))
      return
    if certificate_expiration_days is None:
      appendError(
        'certificate-expiration-days %r is incorrect',
        self.getConfig('certificate-expiration-days'))
      return
    if not hostname:
      appendError('url is incorrect')
      return
    if key not in self.surykatka_json:
      appendError(
        'No key %r. If the error persist, please update surykatka.' % (key,))
      return
    entry_list = [
      q for q in self.surykatka_json[key] if q['hostname'] == hostname]
    if len(entry_list) == 0:
      appendError('No data')
      return
    if len(entry_list) > 0:
      self.appendMessage('%s:' % (key,))

    def addError(msg, *args):
      self.error = True
      self.appendMessage('ERROR ' + msg % args)
    for entry in sorted(entry_list, key=operator.itemgetter('ip')):
      timetuple = email.utils.parsedate(entry['not_after'])
      if timetuple is None:
        addError('IP %s no information' % (entry['ip'],))
      else:
        certificate_expiration_time = datetime.datetime.fromtimestamp(
          time.mktime(timetuple))
        if certificate_expiration_time - datetime.timedelta(
          days=certificate_expiration_days) < self.utcnow:
          addError(
            'IP %s will expire in < %s days',
            entry['ip'], certificate_expiration_days)
        else:
          self.appendMessage(
            'OK IP %s will expire in > %s days' % (
              entry['ip'], certificate_expiration_days))

  def senseHttpQuery(self):
    key = 'http_query'

    def appendError(msg, *args):
      self.error = True
      self.appendMessage(key + ': ERROR ' + msg % args)

    if key not in self.surykatka_json:
      appendError("%r not in %r", key, self.json_file)
      return

    url = self.getConfig('url')
    status_code = self.getConfig('status-code')
    http_header_dict = json.loads(self.getConfig('http-header-dict', '{}'))

    entry_list = [q for q in self.surykatka_json[key] if q['url'] == url]
    if len(entry_list) == 0:
      appendError('No data')
      return

    def addError(msg, *args):
      self.error = True
      self.appendMessage('ERROR ' + msg % args)
    self.appendMessage('%s:' % (key,))
    for entry in sorted(entry_list, key=operator.itemgetter('ip')):
      entry_status_code = str(entry['status_code'])
      if entry_status_code != status_code:
        status_code_explanation = self.EXTENDED_STATUS_CODE_MAPPING.get(
          entry_status_code)
        if status_code_explanation:
          status_code_explanation = '%s (%s)' % (
            entry_status_code, status_code_explanation)
        else:
          status_code_explanation = entry_status_code
        addError(
          'IP %s expected status_code %s != %s' % (
            entry['ip'], status_code_explanation, status_code))
      else:
        self.appendMessage(
          'OK IP %s status_code %s' % (entry['ip'], status_code))
      if http_header_dict:
        if http_header_dict != entry['http_header_dict']:
          addError(
            'IP %s expected HTTP Header %s != of %s' % (
              entry['ip'],
              json.dumps(http_header_dict, sort_keys=True),
              json.dumps(entry['http_header_dict'], sort_keys=True)))
        else:
          self.appendMessage(
            'OK IP %s HTTP Header %s' % (
              entry['ip'], json.dumps(http_header_dict, sort_keys=True)))

  def senseDnsQuery(self):
    key = 'dns_query'

    def appendError(msg, *args):
      self.error = True
      self.appendMessage(key + ': ERROR ' + msg % args)

    if key not in self.surykatka_json:
      appendError("%r not in %r", key, self.json_file)
      return

    url = self.getConfig('url')
    hostname = urlparse(url).hostname
    ip_set = set(self.getConfig('ip-list', '').split())

    entry_list = [
      q for q in self.surykatka_json[key]
      if q['domain'] == hostname and q['rdtype'] == 'A']
    if len(entry_list) == 0:
      appendError('No data')
      return

    self.appendMessage('%s:' % (key,))
    if len(ip_set):
      for entry in sorted(entry_list, key=operator.itemgetter('resolver_ip')):
        response_ip_set = set([
          q.strip() for q in entry['response'].split(",") if q.strip()])
        if ip_set != response_ip_set:
          self.error = True
          self.appendMessage(
            "ERROR resolver %s expected %s != %s" % (
              entry['resolver_ip'], ' '.join(sorted(ip_set)),
              ' '.join(sorted(response_ip_set)) or "empty-reply"))
        else:
          self.appendMessage(
            "OK resolver %s returned expected set of IPs %s" % (
              entry['resolver_ip'], ' '.join(sorted(ip_set)),))
    else:
      self.appendMessage('OK No check configured')

  def senseTcpServer(self):
    key = 'tcp_server'

    def appendError(msg, *args):
      self.error = True
      self.appendMessage(key + ': ERROR ' + msg % args)

    if key not in self.surykatka_json:
      appendError("%r not in %r", key, self.json_file)
      return

    url = self.getConfig('url')
    parsed_url = urlparse(url)
    hostname = parsed_url.hostname
    if parsed_url.port is not None:
      port = parsed_url.port
    else:
      if parsed_url.scheme == 'https':
        port = 443
      else:
        port = 80
    ip_set = set(self.getConfig('ip-list', '').split())

    entry_list = [
      q for q in self.surykatka_json[key]
      if hostname in [
        r.strip() for r in q['domain'].split(',')] and q['port'] == port]
    if len(entry_list) == 0:
      appendError('No data')
      return
    self.appendMessage('%s:' % (key,))
    if len(ip_set) > 0:
      for ip in sorted(ip_set):
        ok = False
        for entry in sorted(entry_list, key=operator.itemgetter('ip')):
          if entry['ip'] == ip:
            if entry['state'] == 'closed':
              ok = False
              break
            if entry['state'] == 'open':
              ok = True
        if ok:
          self.appendMessage('OK IP %s:%s' % (ip, port))
        else:
          self.error = True
          self.appendMessage('ERROR IP %s:%s' % (ip, port))
    else:
      self.appendMessage('OK No check configured')

  def senseElapsedTime(self):
    key = 'elapsed_time'
    surykatka_key = 'http_query'

    def appendError(msg, *args):
      self.error = True
      self.appendMessage('ERROR ' + msg % args)

    if surykatka_key not in self.surykatka_json:
      self.error = True
      self.appendMessage(
        '%s: ERROR No key %r. If the error persist, please update '
        'surykatka.' % (
          key, surykatka_key,))
      return

    url = self.getConfig('url')
    maximum_elapsed_time = self.getConfig('maximum-elapsed-time')

    entry_list = [
      q for q in self.surykatka_json[surykatka_key] if q['url'] == url]
    if len(entry_list) == 0:
      self.error = True
      self.appendMessage('%s: ERROR No data' % (key,))
      return
    self.appendMessage('%s:' % (key,))
    if maximum_elapsed_time:
      found = False
      for entry in sorted(entry_list, key=operator.itemgetter('ip')):
        if 'total_seconds' in entry:
          found = True
          maximum_elapsed_time = float(maximum_elapsed_time)
          if entry['total_seconds'] == 0.:
            appendError('IP %s failed to reply' % (entry['ip']))
          elif entry['total_seconds'] > maximum_elapsed_time:
            appendError(
              'IP %s replied > %.2fs' %
              (entry['ip'], maximum_elapsed_time))
          else:
            self.appendMessage(
              'OK IP %s replied < %.2fs' % (
                entry['ip'], maximum_elapsed_time))
      if not found:
        appendError(
          "No entry with total_seconds found. If the error persist, please "
          "update surykatka")
    else:
      self.appendMessage("OK No check configured")

  def sense(self):
    """
      Check if frontend URL is available
    """
    self.utcnow = datetime.datetime.utcnow()

    self.json_file = self.getConfig('json-file', '')
    if not os.path.exists(self.json_file):
      self.error = True
      self.appendMessage('ERROR File %r does not exists' % self.json_file)
    else:
      with open(self.json_file) as fh:
        try:
          self.surykatka_json = json.load(fh)
        except Exception:
          self.error = True
          self.appendMessage(
            "ERROR loading JSON from %r" % self.json_file)
        else:
          report = self.getConfig('report')
          if report == 'bot_status':
            self.senseBotStatus()
          elif report == 'http_query':
            self.senseDnsQuery()
            self.senseTcpServer()
            self.senseHttpQuery()
            self.senseSslCertificate()
            self.senseElapsedTime()
          else:
            self.error = True
            self.appendMessage(
              "ERROR Report %r is not supported" % report)
    self.emitLog()

  def anomaly(self):
    return self._test(
      result_count=self.result_count, failure_amount=self.failure_amount)