#!/usr/bin/env python3
import os
from common import auth

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def split_line(line, max_length=76):
    words = list(filter(None, map(str.strip, line.split(" "))))
    lines = []
    current_line = []
    current_line_length = 0
    for word in words:
        if (current_line and current_line_length + len(word) + 1 > max_length):
            lines.append(" ".join(current_line))
            current_line = []
            current_line_length = 0
        current_line.append(word)
        current_line_length += len(word) + 1
    if current_line:
        lines.append(" ".join(current_line))
    return lines


def prefix_lines(lines, prefix="#  "):
    return map(lambda line: "".join((prefix, line)), lines)


class ConfigEntry:
    def __init__(self, name, default, required=True, internal=True,
                 immutable=False, no_sane_default=False,
                 description=None, value=None):
        self.name = name
        self.default = default
        self.required = required
        self.internal = internal
        self.immutable = immutable
        self.no_sane_default = no_sane_default
        self.description = description
        self.value = value

    def extract(self, config):
        if self.immutable:
            self.value = self.default
            return
        if not hasattr(config, self.name) and self.required:
            raise ValueError(
                "Missing required config entry {}!".format(self.name))
        self.value = getattr(config, self.name, self.default)
        if self.no_sane_default and self.value == self.default:
            raise ValueError(
                "You have not configured {}, which is required!".format(
                    self.name))

    def get_example(self):
        if self.immutable:
            return []
        lines = []
        if self.description is not None:
            lines.extend(prefix_lines(split_line(self.description)))
        entry_line = "{} = {}".format(self.name, repr(self.default))
        if not self.required:
            entry_line = "# {}".format(entry_line)
        lines.append(entry_line)
        return lines


class ConfigSection:
    def __init__(self, name, entries, check, description=None,
                 deactivatable=False, active=None, imports=None):
        self.name = name
        self.entries = entries
        self.check = check
        self.description = description
        self.deactivatable = deactivatable
        self.active = active
        if isinstance(imports, str):
            imports = [imports]
        self.imports = imports

    def extract(self, config):
        if self.deactivatable:
            self.active = getattr(config, "{}_ACTIVE".format(self.name), False)
            if not self.active:
                return
        for entry in self.entries:
            entry.extract(config)

    def get_example(self):
        lines = []
        if self.imports:
            lines.extend(self.imports)
        header_line = (
            "# [{}]".format(self.name) +
            (" (optional)" if self.deactivatable else ""))
        lines.append(header_line)
        if self.description is not None:
            lines.extend(prefix_lines(split_line(self.description)))
            lines.append("")
        if self.deactivatable:
            lines.extend(prefix_lines(split_line("deactivate with")))
            lines.append("# {}_ACTIVE = False".format(self.name))
            lines.append("")
        for entry in self.entries:
            lines.extend(entry.get_example())
            lines.append("")
        lines.append("")
        return "\n".join(lines)

    def fill_modules(self, config, public_config):
        if self.deactivatable:
            setattr(config, "{}_ACTIVE".format(self.name), self.active)
            setattr(public_config, "{}_ACTIVE".format(self.name), self.active)
            if not self.active:
                return
        for entry in self.entries:
            setattr(config, entry.name, entry.value)
            if not entry.internal:
                setattr(public_config, entry.name, entry.value)


def check_choice(option, value, choices):
    if value not in choices:
        raise ValueError(
            "{} is not allowed choices! Should be one of {}, is {}".format(
                option, choices, value))


def check_database(
        SQLALCHEMY_DATABASE_URI,
        SQLALCHEMY_TRACK_MODIFICATIONS):
    # remove once sqlalchemy.database_exists works as intended
    return True
    from sqlalchemy_utils import database_exists
    from sqlalchemy import exc
    try:
        if not database_exists(SQLALCHEMY_DATABASE_URI):
            raise ValueError(
                "The database '{}' does not exist! "
                "Please configure it correctly or create it!".format(
                    SQLALCHEMY_DATABASE_URI))
    except (exc.NoSuchModuleError, exc.OperationalError) as error:
        raise ValueError(
            "The database uri '{}' does not state a valid "
            "database: {}".format(SQLALCHEMY_DATABASE_URI, error))


def check_security(SECRET_KEY, SECURITY_KEY, SESSION_PROTECTION, SESSION_COOKIE_SECURE, SESSION_COOKIE_HTTPONLY, SESSION_COOKIE_SAMESITE):
    MIN_KEY_LENGTH = 20
    if len(SECRET_KEY) < MIN_KEY_LENGTH:
        raise ValueError(
            "Insufficient length of SECRET_KEY, should be at least {}!".format(
                MIN_KEY_LENGTH))
    if len(SECURITY_KEY) < MIN_KEY_LENGTH:
        raise ValueError(
            "Insufficient length of SECURITY_KEY, should be at "
            "least {}!".format(
                MIN_KEY_LENGTH))
    check_choice("SESSION_PROTECTION", SESSION_PROTECTION, ["strong", "none"])
    check_choice("SESSION_COOKIE_SECURE", SESSION_COOKIE_SECURE, [True, False])
    check_choice("SESSION_COOKIE_HTTPONLY", SESSION_COOKIE_HTTPONLY, [True, False])
    check_choice("SESSION_COOKIE_SAMESITE", SESSION_COOKIE_SAMESITE, ["Lax", "Strict"])


def check_server_name(SERVER_NAME, PREFERRED_URL_SCHEME, CDN_URL, PERMITTED_METADATA_DOMAINS):
    # todo: check ip address and server name
    check_choice(
        "PREFERRED_URL_SCHEME", PREFERRED_URL_SCHEME,
        ["http", "https"])


def check_debug(DEBUG):
    if DEBUG:
        logger.warning("DEBUG mode is activated!")


def check_celery(
        CELERY_BROKER_URL, CELERY_TASK_SERIALIZER, CELERY_ACCEPT_CONTENT):
    pass
    # todo: check broker url
    check_choice(
        "CELERY_TASK_SERIALIZER", CELERY_TASK_SERIALIZER,
        ["pickle"])
    check_choice(
        "CELERY_ACCEPT_CONTENT", CELERY_ACCEPT_CONTENT,
        [["pickle"]])


def check_sentry(SENTRY_DSN):
    pass


def check_authentication(AUTH_MAX_DURATION, AUTH_BACKENDS):
    if AUTH_MAX_DURATION <= 0:
        raise ValueError(
            "AUTH_MAX_DURATION should be positive, is {}!".format(
                AUTH_MAX_DURATION))
    if not AUTH_BACKENDS:
        raise ValueError("No authentication backends have been configured!")


def check_error_report_context(ERROR_CONTEXT_LINES):
    if ERROR_CONTEXT_LINES < 0:
        raise ValueError(
            "ERROR_CONTEXT_LINES should be positive, is {}!".format(
                ERROR_CONTEXT_LINES))


def check_pagination(PAGE_LENGTH, PAGE_DIFF):
    if PAGE_LENGTH <= 0:
        raise ValueError(
            "PAGE_LENGTH should be positive, is {}!".format(
                PAGE_LENGTH))
    if PAGE_DIFF < 0:
        raise ValueError(
            "PAGE_DIFF should be positive, is {}!".format(
                PAGE_DIFF))


def check_index_page(
        MAX_INDEX_DAYS, MAX_PAST_INDEX_DAYS,
        MAX_PAST_INDEX_DAYS_BEFORE_REMINDER):
    if MAX_INDEX_DAYS < 0:
        raise ValueError(
            "MAX_INDEX_DAYS should be positive, is {}!".format(
                MAX_INDEX_DAYS))
    if MAX_PAST_INDEX_DAYS < 0:
        raise ValueError(
            "MAX_PAST_INDEX_DAYS should be positive, is {}!".format(
                MAX_PAST_INDEX_DAYS))
    if MAX_PAST_INDEX_DAYS_BEFORE_REMINDER < 0:
        raise ValueError(
            "MAX_PAST_INDEX_DAYS_BEFORE_REMINDER should be "
            "positive, is {}!".format(
                MAX_PAST_INDEX_DAYS_BEFORE_REMINDER))


def check_admin_data(ADMIN_MAIL, ADMIN_GROUP):
    if not ADMIN_MAIL:
        raise ValueError("No admin mail address given!")
    if not ADMIN_GROUP:
        raise ValueError("No admin group given!")


def check_parser(PARSER_LAZY, FUZZY_MIN_SCORE, PRIVATE_KEYWORDS):
    if PARSER_LAZY:
        logger.warning(
            "Parser lazy mode is activated, this is not meant or useful "
            "for production operation!")
    if not 0 <= FUZZY_MIN_SCORE <= 100:
        raise ValueError(
            "FUZZY_MIN_SCORE should be a percentage from 0 to 100")
    for keyword in PRIVATE_KEYWORDS:
        if not keyword:
            raise ValueError("Invalid private keyword given: {}".format(
                keyword))


def check_rendering(
        FONTS, DOCUMENTS_PATH, LATEX_BULLETPOINTS, HTML_LEVEL_OFFSET,
        LATEX_LOCAL_TEMPLATES, LATEX_LOGO_TEMPLATE, LATEX_GEOMETRY,
        LATEX_PAGESTYLE, LATEX_HEADER_FOOTER, LATEX_ADDITIONAL_PACKAGES,
        LATEX_TEMPLATES):
    for key in ("main", "roman", "sans", "mono"):
        if key not in FONTS:
            raise ValueError("No font for type {} given!".format(key))
        try:
            path = FONTS[key]["path"]
            extension = FONTS[key]["extension"]
            for face in ("regular", "bold", "italic", "bolditalic"):
                if face not in FONTS[key]:
                    raise ValueError("Missing fontface {} for {} font!".format(
                        face, key))
                filepath = os.path.join(
                    path, "".join((FONTS[key][face], extension)))
                if not os.path.exists(filepath):
                    raise ValueError(
                        "Font {} does not exist in the filesystem! "
                        "Expected it at {}".format(
                            FONTS[key][face], filepath))
        except KeyError:
            raise ValueError(
                "Missing path information in font specification "
                "{key}: ".format(key=key))
    if not os.path.isdir(DOCUMENTS_PATH):
        raise ValueError(
            "Document savedir does not exist! Should be at {}.".format(
                DOCUMENTS_PATH))
    if not LATEX_BULLETPOINTS:
        raise ValueError("LATEX_BULLETPOINTS is empty.")
    if not 1 <= HTML_LEVEL_OFFSET <= 4:
        raise ValueError(
            "HTML_LEVEL_OFFSET should be from 1 to 4, but is {}".format(
                HTML_LEVEL_OFFSET))
    # todo: check templates stuff


def check_mail(MAIL_FROM, MAIL_HOST, MAIL_USER, MAIL_PASSWORD,
               MAIL_USE_TLS, MAIL_USE_STARTTLS):
    if MAIL_USE_TLS and MAIL_USE_STARTTLS:
        raise ValueError(
            "Both TLS and STARTTLS are set for Mail! Please set only one.")
    from utils import MailManager
    import socket
    import smtplib
    import ssl
    mail_config = Config()
    mail_config.MAIL_ACTIVE = True
    mail_config.MAIL_FROM = MAIL_FROM
    mail_config.MAIL_HOST = MAIL_HOST
    mail_config.MAIL_USER = MAIL_USER
    mail_config.MAIL_PASSWORD = MAIL_PASSWORD
    mail_config.MAIL_USE_TLS = MAIL_USE_TLS
    mail_config.MAIL_USE_STARTTLS = MAIL_USE_STARTTLS
    mail_manager = MailManager(mail_config)
    try:
        if not mail_manager.check():
            raise ValueError("The mail connection is improperly configured.")
    except (ConnectionRefusedError, socket.gaierror) as error:
        raise ValueError("Mail Cannot connect to the server: {}".format(error))
    except smtplib.SMTPAuthenticationError as error:
        raise ValueError("Mail Cannot authenticate: {}".format(error))
    except (ssl.SSLError, smtplib.SMTPNotSupportedError) as error:
        raise ValueError("TLS for Mail does not work: {}".format(error))
    except Exception as error:
        raise ValueError(
            "Testing the mail connection failed: {}".format(error))


def check_printing(PRINTING_SERVER, PRINTING_USER, PRINTING_PRINTERS):
    LPR_PATH = "/usr/bin/lpr"
    LPSTAT_PATH = "/usr/bin/lpstat"
    LPOPTIONS_PATH = "/usr/bin/lpoptions"
    for path in [LPR_PATH, LPSTAT_PATH, LPOPTIONS_PATH]:
        if not os.path.exists(path):
            raise ValueError(
                "{} is not installed! It is required for printing".format(
                    path))
    import subprocess as sp
    list_printers_command = [LPSTAT_PATH, "-h", PRINTING_SERVER, "-p"]
    raw_printers = sp.check_output(list_printers_command).decode("utf-8")
    printers = [
        line.split(" ")[1].lower()
        for line in raw_printers.splitlines()
        if not line.startswith(" ")
    ]
    configured_printers = [key.lower() for key in PRINTING_PRINTERS]
    missing_printers = set(configured_printers) - set(printers)
    if missing_printers:
        raise ValueError(
            "Some configured printers are not available: {}, "
            "available are: {}".format(
                missing_printers, printers))

    def _strip_star(value):
        if value.startswith("*"):
            return value[1:]
        return value
    for printer in configured_printers:
        options_command = [
            LPOPTIONS_PATH, "-h", PRINTING_SERVER, "-p", printer, "-l"]
        raw_options = sp.check_output(options_command).decode("utf-8")
        available_options = {}
        for line in raw_options.splitlines():
            name_part, value_part = line.split(":")
            values = list(map(_strip_star, value_part.strip().split(" ")))
            for name in name_part.split("/"):
                available_options[name] = values
        for option in PRINTING_PRINTERS[printer]:
            name, value = option.split("=")
            if name not in available_options:
                logger.warning(
                    "Printer {} has unknown option {} set!".format(
                        printer, name))
            elif value not in available_options[name]:
                logger.warning(
                    "Printer {} has the invalid value {} specified "
                    "for option {}! Valid values are {}.".format(
                        printer, value, name, available_options[name]))


def check_etherpad(ETHERPAD_URL, ETHERPAD_API_URL, ETHERPAD_APIKEY, EMPTY_ETHERPAD):
    import requests
    try:
        answer = requests.get(ETHERPAD_URL)
        if answer.status_code != 200:
            raise ValueError(
                "The etherpad does not return 200 OK at {}".format(
                    ETHERPAD_URL))
    except requests.exceptions.ConnectionError as error:
        raise ValueError("Cannot connect to the etherpad at {}: {}".format(
            ETHERPAD_URL, error))


def check_wiki(
        WIKI_TYPE, WIKI_API_URL, WIKI_ANONYMOUS,
        WIKI_USER, WIKI_PASSWORD, WIKI_DOMAIN):
    check_choice("WIKI_TYPE", WIKI_TYPE, ["MEDIAWIKI", "DOKUWIKI"])
    # todo: check the connection


def check_calendar(
        CALENDAR_URL, CALENDAR_DEFAULT_DURATION, CALENDAR_MAX_REQUESTS):
    from calendarpush import Client, CalendarException
    try:
        client = Client(url=CALENDAR_URL)
        client.get_calendars()
    except (KeyError, CalendarException) as error:
        raise ValueError("Cannot connect to the calendar at {}!".format(
            CALENDAR_URL))


def check_timezone(CALENDAR_TIMEZONE_MAP):
    pass


CONFIG_SECTIONS = [
    ConfigSection(
        name="Database",
        entries=[
            ConfigEntry(
                name="SQLALCHEMY_DATABASE_URI",
                default="engine://user:password@host/database",
                required=True, internal=True, no_sane_default=True,
                description=(
                    "Database connection string. See "
                    "http://docs.sqlalchemy.org/en/latest/core/engines.html "
                    "for details. SQLite does not work with Alembic "
                    "migrations.")),
            ConfigEntry(
                name="SQLALCHEMY_TRACK_MODIFICATIONS",
                default=False,
                required=False, internal=True, immutable=True,
                description="Necessary option. Do not change."),
        ],
        check=check_database,
        description="Settings for SQLAlchemy"),
    ConfigSection(
        name="Security",
        entries=[
            ConfigEntry(
                name="SECRET_KEY",
                default=os.urandom(128),
                required=True, internal=True,
                description="Secret random key used for session security."),
            ConfigEntry(
                name="SECURITY_KEY",
                default=os.urandom(128),
                required=True, internal=True,
                description="Secret random key used for user sessions."),
            ConfigEntry(
                name="SESSION_PROTECTION",
                default="strong",
                required=False, internal=True, immutable=True,
                description="Flask setting for sessions. Do not change."),
            ConfigEntry(
                name="SESSION_COOKIE_SECURE",
                default=True,
                required=False, internal=True, immutable=True,
                description="Flask setting for cookies. Do not change."),
            ConfigEntry(
                name="SESSION_COOKIE_HTTPONLY",
                default=True,
                required=False, internal=True, immutable=True,
                description="Flask setting for cookies. Do not change."),
            ConfigEntry(
                name="SESSION_COOKIE_SAMESITE",
                default="Strict",
                required=False, internal=True, immutable=True,
                description="Flask setting for cookies. Do not change."),
        ],
        check=check_security,
        description="Secret keys and random strings"),
    ConfigSection(
        name="URL Root",
        entries=[
            ConfigEntry(
                name="SERVER_NAME",
                default="protokolle.example.com",
                required=True, internal=False, no_sane_default=True,
                description="Domain on which this website is hosted"),
            ConfigEntry(
                name="PREFERRED_URL_SCHEME",
                default="https",
                required=False, internal=False,
                description="Protocol used by this website. "
                            "Either 'http' or 'https'."),
            ConfigEntry(
                name="CDN_URL",
                default=None,
                required=False, internal=False,
                description="URL to get bootstrap and jQuery from."),
            ConfigEntry(
                name="PERMITTED_METADATA_DOMAINS",
                default=[],
                required=False, internal=False,
                description="Domains allowed to be linked to in protocol metadata (e.g. location)."),
        ],
        check=check_server_name,
        description="Where is the website hosted"),
    ConfigSection(
        name="Debug",
        entries=[
            ConfigEntry(
                name="DEBUG",
                default=False,
                required=False, internal=True,
                description="Activate debug mode"),
        ],
        check=check_debug,
        description="Debug mode. Do not set in production."),
    ConfigSection(
        name="Celery",
        entries=[
            ConfigEntry(
                name="CELERY_BROKER_URL",
                default="redis://localhost:6379/0",
                required=True, internal=True,
                description=(
                    "Connection to the celery broker. See http://docs.celery"
                    "project.org/en/latest/userguide/configuration.html")),
            ConfigEntry(
                name="CELERY_TASK_SERIALIZER",
                default="pickle",
                required=False, internal=True, immutable=True,
                description="How celery serializes tasks. Do not change."),
            ConfigEntry(
                name="CELERY_ACCEPT_CONTENT",
                default=['pickle'],
                required=False, internal=True, immutable=True,
                description="How celery deserializes tasks. Do not change."),
        ],
        check=check_celery,
        description="Settings for the task scheduler."),
    ConfigSection(
        name="Sentry",
        entries=[
            ConfigEntry(
                name="SENTRY_DSN",
                default=None,
                required=False, internal=True,
                description=(
                    "Connection string for sentry. See "
                    "https://docs.sentry.io/quickstart/#configure-the-dsn")),
        ],
        check=check_sentry,
        description="Connection information for sentry exception reporting."),
    ConfigSection(
        name="Authentication",
        entries=[
            ConfigEntry(
                name="AUTH_MAX_DURATION",
                default=300,
                required=False, internal=False,
                description="Time in seconds for which non-permanent "
                            "sessions remain valid"),
            ConfigEntry(
                name="AUTH_BACKENDS",
                default=[
                    auth.StaticUserManager((("user", "password", ["group"]),)),
                ],
                required=True, internal=True, no_sane_default=True,
                description="Active Authentication backends"),
        ],
        check=check_authentication,
        description="User Authentication backend settings.",
        imports="from common import auth"),
    ConfigSection(
        name="Error Report Context",
        entries=[
            ConfigEntry(
                name="ERROR_CONTEXT_LINES",
                default=3,
                required=False, internal=False,
                description=(
                    "Number of lines before and after an error to include "
                    "in the error description")),
        ],
        check=check_error_report_context,
        description="Compiling error context settings"),
    ConfigSection(
        name="Pagination",
        entries=[
            ConfigEntry(
                name="PAGE_LENGTH",
                default=20,
                required=False, internal=False,
                description="Default number of entries per page"),
            ConfigEntry(
                name="PAGE_DIFF",
                default=3,
                required=False, internal=False,
                description=(
                    "Number of pages before and after the current one "
                    "with a direct link.")),
        ],
        check=check_pagination,
        description="Pagination settings, used for list pages"),
    ConfigSection(
        name="Index Page",
        entries=[
            ConfigEntry(
                name="MAX_INDEX_DAYS",
                default=14,
                required=False, internal=False,
                description="Next days to list upcoming meetings on"),
            ConfigEntry(
                name="MAX_PAST_INDEX_DAYS",
                default=2,
                required=False, internal=False,
                description="Past days to list unfinished meetings on"),
            ConfigEntry(
                name="MAX_PAST_INDEX_DAYS_BEFORE_REMINDER",
                default=14,
                required=False, internal=True,
                description=(
                    "If a meeting is unfinished this many days after its "
                    "date, a reminder is sent by mail")),
        ],
        check=check_index_page,
        description="Settings for what to show on the index page"),
    ConfigSection(
        name="Admin data",
        entries=[
            ConfigEntry(
                name="ADMIN_MAIL",
                default="admin@example.com",
                required=True, internal=False, no_sane_default=True,
                description="Mail address to tell users to "
                            "contact in case of errors"),
            ConfigEntry(
                name="ADMIN_GROUP",
                default="admin",
                required=True, internal=False,
                description="Users with this group are admins and are "
                            "allowed to do and see everything."),
        ],
        check=check_admin_data,
        description="Settings for who is an admin and how to contact them"),
    ConfigSection(
        name="Parser",
        entries=[
            ConfigEntry(
                name="PARSER_LAZY",
                default=False,
                required=False, internal=False,
                description="Do not enforce some parser policies."),
            ConfigEntry(
                name="FUZZY_MIN_SCORE",
                default=90,
                required=False, internal=False,
                description="Todos with at least this equality score are "
                            "considered equal whe importing old protocols."),
            ConfigEntry(
                name="PRIVATE_KEYWORDS",
                default=["private", "internal", "privat", "intern"],
                required=False, internal=False,
                description="Keywords indicating private protocol parts"),
        ],
        check=check_parser,
        description="Settings for the protocol syntax parser"),
    ConfigSection(
        name="Rendering",
        entries=[
            ConfigEntry(
                name="FONTS",
                default={
                    "main": {
                        "extension": ".otf",
                        "path": "/usr/share/fonts/gsfonts/",
                        "regular": "NimbusSans-Regular",
                        "bold": "NimbusSans-Bold",
                        "italic": "NimbusSans-Italic",
                        "bolditalic": "NimbusSans-BoldItalic"
                    },
                    "roman": {
                        "extension": ".otf",
                        "path": "/usr/share/fonts/gsfonts/",
                        "regular": "NimbusRoman-Regular",
                        "bold": "NimbusRoman-Bold",
                        "italic": "NimbusRoman-Italic",
                        "bolditalic": "NimbusRoman-BoldItalic"
                    },
                    "sans": {
                        "extension": ".otf",
                        "path": "/usr/share/fonts/gsfonts/",
                        "regular": "NimbusSans-Regular",
                        "bold": "NimbusSans-Bold",
                        "italic": "NimbusSans-Italic",
                        "bolditalic": "NimbusSans-BoldItalic"
                    },
                    "mono": {
                        "extension": ".otf",
                        "path": "/usr/share/fonts/gsfonts/",
                        "regular": "NimbusMonoPS-Regular",
                        "bold": "NimbusMonoPS-Bold",
                        "italic": "NimbusMonoPS-Italic",
                        "bolditalic": "NimbusMonoPS-BoldItalic"
                    }
                },
                required=True, internal=False,
                description="fonts for xelatex"),
            ConfigEntry(
                name="DOCUMENTS_PATH",
                default="documents",
                required=False, internal=True,
                description="Path to the directory to save protocols in. "
                            "Write access is necessary."),
            ConfigEntry(
                name="LATEX_BULLETPOINTS",
                default=[
                    r"\textbullet", r"\normalfont \bfseries \textendash",
                    r"\textasteriskcentered", r"\textperiodcentered"],
                required=False, internal=False,
                description="list of bulletpoints to use in latex"),
            ConfigEntry(
                name="HTML_LEVEL_OFFSET",
                default=3,
                required=False, internal=False,
                description="Header level at which to start with "
                            "HTML headlines"),
            ConfigEntry(
                name="LATEX_LOCAL_TEMPLATES",
                default=None,
                required=False, internal=False,
                description="path to additional jinja2 templates"),
            ConfigEntry(
                name="LATEX_LOGO_TEMPLATE",
                default=None,
                required=False, internal=False,
                description="template to include at the top of protocols"),
            ConfigEntry(
                name="LATEX_GEOMETRY",
                default="vmargin=1.5cm,hmargin={1.5cm,1.2cm},"
                        "bindingoffset=8mm",
                required=False, internal=False,
                description="custom latex page geometry"),
            ConfigEntry(
                name="LATEX_PAGESTYLE",
                default=None,
                required=False, internal=False,
                description="custom latex pagestyle, e.g. 'fancy'"),
            ConfigEntry(
                name="LATEX_HEADER_FOOTER",
                default=False,
                required=False, internal=False,
                description="Include a header and footer in protocols"),
            ConfigEntry(
                name="LATEX_ADDITIONAL_PACKAGES",
                default=None,
                required=False, internal=False,
                description="Include additional latex packages in protocols"),
            ConfigEntry(
                name="LATEX_TEMPLATES",
                default=None,
                required=False, internal=False,
                description=(
                    "define multiple LaTeX-templates to use with a each "
                    "protocol type individually overriding the general LATEX "
                    "options the LATEX_LOCAL_TEMPLATES parameter is need to "
                    "provide the path for the templates each template must "
                    "be placed in an individual folder named by its ID in "
                    "LATEX_TEMPLATES and must contain the provided template "
                    "files: e.g. the files for the template 'yourtemplate' "
                    "need to be in the folder named 'yourtemplate', and the "
                    "templates provides the files: 'protokoll2.cls' (class), "
                    "'protocol.tex' (protocol), 'decision.tex' (decision) and "
                    "'asta-logo.tex'")),
        ],
        check=check_rendering,
        description="Settings for rendering protocols to pdf, html, etc."),
    ConfigSection(
        name="MAIL",
        entries=[
            ConfigEntry(
                name="MAIL_FROM",
                default="protokolle@example.com",
                required=True, internal=False, no_sane_default=True,
                description="Mail sender address"),
            ConfigEntry(
                name="MAIL_HOST",
                default="mail.example.com:465",
                required=True, internal=True, no_sane_default=True,
                description="SMTP Mail server address with port"),
            ConfigEntry(
                name="MAIL_USER",
                default="",
                required=False, internal=True,
                description="Mail server login user. "
                            "Empty for no authentication."),
            ConfigEntry(
                name="MAIL_PASSWORD",
                default="",
                required=False, internal=True,
                description="Mail server login password. "
                            "Empty for no authentication."),
            ConfigEntry(
                name="MAIL_USE_TLS",
                default=True,
                required=False, internal=True,
                description="Use SMPTS (not STARTTLS). Should match port."),
            ConfigEntry(
                name="MAIL_USE_STARTTLS",
                default=False,
                required=False, internal=True,
                description="Use SMTP with STARTTLS. Should match port."),

        ],
        check=check_mail,
        deactivatable=True,
        description="Mail server connection"),
    ConfigSection(
        name="PRINTING",
        entries=[
            ConfigEntry(
                name="PRINTING_SERVER",
                default="printsrv.example.com:631",
                required=True, internal=True, no_sane_default=True,
                description="CUPS printserver connection"),
            ConfigEntry(
                name="PRINTING_USER",
                default="protocols",
                required=False, internal=True,
                description="CUPS user for print jobs"),
            ConfigEntry(
                name="PRINTING_PRINTERS",
                default={
                    "example_printer":
                        ["Duplex=DuplexNoTumble", "option2=value"],
                    "other_printer": ["list", "of", "options"],
                },
                required=True, internal=False, no_sane_default=True,
                description="printers with corresponding options"),
        ],
        check=check_printing,
        deactivatable=True,
        description="CUPS printing settings"),
    ConfigSection(
        name="ETHERPAD",
        entries=[
            ConfigEntry(
                name="ETHERPAD_URL",
                default="https://example.com/etherpad",
                required=True, internal=False, no_sane_default=True,
                description=(
                    "URL of the etherpad installation. "
                    "Do not include the '/p'!")),
            ConfigEntry(
                name="ETHERPAD_API_URL",
                default="https://example.com/etherpad/api",
                required=False, internal=True, no_sane_default=True,
                description=("URL of the etherpad API. "
                             "Usually ETHERPAD_URL + /api")),
            ConfigEntry(
                name="ETHERPAD_APIKEY",
                default="abc123",
                required=True, internal=True, no_sane_default=True,
                description="Key to access the etherpad API"),
            ConfigEntry(
                name="EMPTY_ETHERPAD",
                default="\n".join([
                    "Welcome to Etherpad!",
                    "",
                    "This pad text is synchronized as you type, so that "
                    "everyone viewing this page sees the same text. This "
                    "allows you to collaborate seamlessly on documents!",
                    "",
                    "Get involved with Etherpad at https://etherpad.org",
                    ""
                ]),
                required=False, internal=False,
                description="The content a new etherpad contains."),
        ],
        check=check_etherpad,
        deactivatable=True,
        description="Etherpad settings"),
    ConfigSection(
        name="WIKI",
        entries=[
            ConfigEntry(
                name="WIKI_TYPE",
                default="MEDIAWIKI",
                required=True, internal=False,
                description="'MEDIAWIKI' or 'DOKUWIKI'"),
            ConfigEntry(
                name="WIKI_API_URL",
                default="https://wiki.example.com/wiki/api.php",
                required=True, internal=True, no_sane_default=True,
                description=(
                    "API Endpoint for Mediawiki, "
                    "'https://user:password@wiki.example.com/lib/exe/"
                    "xmlrpc.php' for Dokuwiki")),
            ConfigEntry(
                name="WIKI_ANONYMOUS",
                default=False,
                required=False, internal=True,
                description="Skip login (only for Mediawiki)"),
            ConfigEntry(
                name="WIKI_USER",
                default=None,
                required=False, internal=True,
                description="Login user (only for Mediawiki)"),
            ConfigEntry(
                name="WIKI_PASSWORD",
                default=None,
                required=False, internal=True,
                description="Login password (only for Mediawiki)"),
            ConfigEntry(
                name="WIKI_DOMAIN",
                default=None,
                required=False, internal=True,
                description="Login domain (only for Mediawiki)"),
        ],
        check=check_wiki,
        deactivatable=True,
        description="Mediawiki or Dokuwiki settings"),
    ConfigSection(
        name="CALENDAR",
        entries=[
            ConfigEntry(
                name="CALENDAR_URL",
                default="https://user:password@calendar.example.com/dav/",
                required=True, internal=True, no_sane_default=True,
                description="CalDAV server URL"),
            ConfigEntry(
                name="CALENDAR_DEFAULT_DURATION",
                default=3,
                required=False, internal=False,
                description="Default meeting length in hours"),
            ConfigEntry(
                name="CALENDAR_MAX_REQUESTS",
                default=10,
                required=False, internal=False,
                description=(
                    "Number of retries before giving a connection attempt up. "
                    "Some CalDAV servers reply randomly with errors.")),
        ],
        check=check_calendar,
        deactivatable=True,
        description="CalDAV settings"),
    ConfigSection(
        name="TIMEZONE",
        entries=[
            ConfigEntry(
                name="CALENDAR_TIMEZONE_MAP",
                default={
                    "CET": "Europe/Berlin",
                    "CEST": "Europe/Berline",
                },
                required=False, internal=False,
                description="Timezone abbreviation map. Add as needed."),
        ],
        check=check_timezone,
        description="Settings for translating timezone information."),
]


class Config:
    def __init__(self, data=None):
        if data is None:
            data = {}
        object.__setattr__(self, "data", data)

    def __setattr__(self, key, value):
        self.data[key] = value

    def __getattr__(self, key):
        try:
            return self.data[key]
        except KeyError:
            raise AttributeError

    def __dir__(self):
        return self.data.keys()


def import_config(sections=CONFIG_SECTIONS):
    import config
    for section in sections:
        section.extract(config)
    internal_config, public_config = Config(), Config()
    for section in sections:
        section.fill_modules(internal_config, public_config)
    return internal_config, public_config


def check_config(sections=CONFIG_SECTIONS):
    import config
    successful = True
    for section in sections:
        section.extract(config)
        if not section.deactivatable or section.active:
            logger.info("Checking {}".format(section.name))
            try:
                section.check(
                    **{entry.name: entry.value for entry in section.entries})
            except ValueError as error:
                logger.error(error.args[0])
                successful = False
    return successful


def write_example_config(
        sections=CONFIG_SECTIONS, filename="config.py.example"):
    example_config = "\n".join([
        section.get_example()
        for section in sections
    ])
    with open(filename, "w") as example_config_file:
        example_config_file.write(example_config)


if __name__ == "__main__":
    import argparse
    import sys
    parser = argparse.ArgumentParser(
        description="Create and check the config")
    subparsers = parser.add_subparsers(dest="command")
    subparsers.required = True

    check_parser = subparsers.add_parser("check")
    check_parser.add_argument(
        "--log-level", choices=["error", "warning", "info", "debug"])

    create_parser = subparsers.add_parser("create")
    create_parser.add_argument("--filename", default="config.py.example")

    show_parser = subparsers.add_parser("show")

    arguments = parser.parse_args()

    if arguments.command == "check":
        if arguments.log_level:
            logger.setLevel(arguments.log_level.upper())
        sys.exit(not check_config())
    elif arguments.command == "create":
        filename = arguments.filename
        write_example_config(filename=filename)
        print("An example config has been generated at {}. "
              "Please change the requried entries.".format(filename))
    elif arguments.command == "show":
        import pprint
        internal_config, public_config = import_config()
        print("The complete config is:")
        pprint.pprint(internal_config.data)
        print("The public part is:")
        pprint.pprint(public_config.data)