<reponame>ZeitOnline/briefkasten
import click
import logging
import re
import sys
from imapclient import IMAPClient
from datetime import datetime
from os import environ, path
from time import sleep
from zope.testbrowser.browser import Browser
from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
from pyquery import PyQuery

import configparser as configparser

find_drop_id = re.compile("Drop\W(\w+)\s.*")
log = logging.getLogger(__name__)


class ConfigParser(configparser.ConfigParser):
    """ a ConfigParser that can provide its values as simple dictionary.
    taken from http://stackoverflow.com/questions/3220670
    """

    def as_dict(self):
        d = dict(self._sections)
        for k in d:
            d[k] = dict(self._defaults, **d[k])
            d[k].pop("__name__", None)
        return d


class WatchdogError(object):

    subject = ""
    message = ""

    def __init__(self, subject, message):
        self.subject = subject
        self.message = message

    def __repr__(self):
        return "[%s] %s" % (self.subject, self.message)


def perform_submission(app_url, testing_secret):
    token = None
    errors = []
    now = datetime.now()
    browser = Browser()
    try:
        browser.open(app_url)
    except Exception as exc:
        errors.append(
            WatchdogError(
                subject="Couldn't open submission page",
                message=u"The attempt to access the submission form resulted in an exception (%s)"
                % exc,
            )
        )
        return token, now, errors
    try:
        submit_form = browser.getForm(id="briefkasten-form")
    except LookupError:
        errors.append(
            WatchdogError(
                subject=u"Couldn't find submission form",
                message=u"""The contact form was not accessible""",
            )
        )
        return token, now, errors
    submit_form.getControl(
        name="message"
    ).value = u"This is an automated test submission from the watchdog instance."
    submit_form.getControl(name="testing_secret").value = testing_secret
    # TODO: submit attachment
    submit_form.submit()
    response = PyQuery(browser.contents)
    token_element = response("#feedback-token")
    token = None
    if token_element is not None:
        token = token_element.text()
    if not bool(token):
        errors.append(
            WatchdogError(
                subject="Couldn't get feedback token",
                message=u"The form submission was successful, but no feedback-token was given at %s"
                % browser.url,
            )
        )
    return token, now, errors


def fetch_test_submissions(config, target_token):
    """ fetch emails from IMAP server using the given configuration
        each email is parsed to see whether it matches a submission
        if so, its token is extracted and checked against the given
        one.
        if found, the email is deleted from the server and True is returned
    """
    from distutils import util
    use_ssl = bool(util.strtobool(config.get('imap_ssl', True)))
    server = IMAPClient(
        config["imap_host"], port=int(config.get("imap_port", 143)), use_uid=True, ssl=use_ssl,
    )
    server.login(config["imap_user"], config["imap_passwd"])
    server.select_folder("INBOX")
    candidates = server.fetch(
        server.search(criteria='NOT DELETED SUBJECT "Drop"'),
        ["BODY[HEADER.FIELDS (SUBJECT)]"],
    )
    success = False
    for imap_id, message in candidates.items():
        subject = message.get(b"BODY[HEADER.FIELDS (SUBJECT)]", "Subject: ")
        try:
            drop_id = find_drop_id.findall(subject.decode('utf-8'))[0]
        except IndexError:
            # ignore emails that are not test submissions
            continue
        if drop_id == target_token:
            server.delete_messages([imap_id])
            success = True
            log.info("Success! Found %s" % target_token)
            break
    server.logout()
    return success


def send_error_email(errors, config):
    if len(errors) == 0:
        return
    log.warning("Errors were found.")
    from pyramid_mailer import mailer_factory_from_settings
    from pyramid_mailer.message import Message
    from urllib.parse import urlparse

    mailer = mailer_factory_from_settings(config, prefix="smtp_")
    hostname = urlparse(config["app_url"]).hostname
    recipients = [
        recipient
        for recipient in config["notify_email"].split()
        if recipient
    ]
    body = "\n".join([str(error) for error in errors])
    message = Message(
        subject="[Briefkasten %s] Submission failure" % hostname,
        sender=config["the_sender"],
        recipients=recipients,
        body=body,
    )
    mailer.send_immediately(message, fail_silently=False)


def push_to_prometheus(errors, config):
    if len(errors) > 0:
        return
    log.info("No Errors were found, pushing success to prometheus/")
    registry = CollectorRegistry()
    gauge = Gauge(
        'job_last_briefkasten_watchdog_success_unixtime',
        'Last time a briefkasten watchdog job successfully finished',
        registry=registry)
    gauge.set_to_current_time()
    push_to_gateway(
        config['prometheus_push_gateway_url'],
        job="briefkasten_watchdog_{environment}".format(**config),
        registry=registry)


def default_config():
    return dict(
        app_url="http://localhost:6543/briefkasten/",
        max_process_secs=60,
        smtp_host="localhost",
        smtp_port=25,
        log_level="INFO",
        sleep_seconds=0,
        retry_seconds=20,
        max_attempts=3,
    )


def config_from_file(fs_config):
    parser = ConfigParser(allow_no_value=True)
    parser.read(fs_config)
    try:
        return parser.as_dict()["briefkasten"]
    except KeyError:
        return dict()


def config_from_env(prefix="BKWD_"):
    keys = [e for e in environ.keys() if e.startswith(prefix)]
    config = dict()
    for key in keys:
        target_key = key.split(prefix)[-1].lower()
        config[target_key] = environ[key]
    return config


def once(config):
    # perform test submission
    log.debug("Performing test submissions against {app_url}".format(**config))
    token, timestamp, errors = perform_submission(
        app_url=config["app_url"], testing_secret=config["testing_secret"]
    )
    log.info("Created drop with token %s" % token)
    # fetch submissions from mail server
    if token is not None:
        log.debug("Fetching previous submissions from IMAP server")
        attempts = 0
        while True:
            log.info("Waiting {retry_seconds} seconds".format(**config))
            sleep(int(config["retry_seconds"]))
            success = fetch_test_submissions(config, token)
            attempts += 1
            if success or attempts >= int(config['max_attempts']):
                break
            log.info("Retrying fetching %s" % token)

        # check for failed test submissions
        now = datetime.now()
        age = now - timestamp
        max_process_secs = int(config['max_process_secs'])
        if success and age.seconds > max_process_secs:
            errors.append(
                WatchdogError(
                    subject="Submission '%s' not received in time" % token,
                    message=u"The submission with token %s submitted on %s was received, but it took %d seconds instead of %d."
                    % (token, timestamp, age.seconds, max_process_secs),
                )
            )
        if not success:
            errors.append(
                WatchdogError(
                    subject="Submission '%s' not received" % token,
                    message=u"The submission with token %s which was submitted on %s was not received after %d seconds."
                    % (token, timestamp, max_process_secs),
                )
            )
    send_error_email(errors, config)
    push_to_prometheus(errors, config)
    if errors:
        log.error(errors)
    return errors


@click.command(help="Performs a test submission and checks it arrived")
@click.argument(
    "fs_config", required=False, default="watchdog.ini",
)
def main(fs_config=None):
    config = default_config()
    if fs_config is not None:
        fs_config = path.abspath(fs_config)
        config.update(config_from_file(fs_config))
    config.update(config_from_env())
    logging.basicConfig(
        stream=sys.stdout, level=getattr(logging, config["log_level"].upper())
    )

    once(config)


if __name__ == '__main__':
    main()
