#
# Copyright (c) 2009-2015, Mendix bv
# All Rights Reserved.
#
# http://www.mendix.com/
#

import yaml
import os
import sys
import pwd
import re
import copy

from .log import logger
from collections import defaultdict
from .version import MXVersion

# Use json if available. If not (python 2.5) we need to import the simplejson
# module instead, which has to be available.
try:
    import json
except ImportError:
    try:
        import simplejson as json
    except ImportError as ie:
        logger.critical(
            "Failed to import json as well as simplejson. If "
            "using python 2.5, you need to provide the simplejson "
            "module in your python library path."
        )
        raise


class M2EEConfig:
    def __init__(self, load_default_files=True, yaml_files=None, config=None):
        _yaml_files = []
        if load_default_files:
            _yaml_files.extend(find_yaml_files())
        if yaml_files:
            _yaml_files.extend(yaml_files)

        self._mtimes, self._conf = read_yaml_files(_yaml_files)

        if config:
            self._conf = merge_config(self._conf, config)

        self._all_systems_are_go = True

        self._check_appcontainer_config()
        self._check_runtime_config()
        self._conf["mxruntime"].setdefault(
            "BasePath", self._conf["m2ee"]["app_base"]
        )

        self._conf["mxruntime"].setdefault("DTAPMode", "P")

        self.fix_permissions()

        self._appcontainer_version = self._conf["m2ee"].get(
            "appcontainer_version", None
        )

        # >= 3.0: application information (e.g. runtime version)
        # if this file does not exist (i.e. < 3.0) try_load_json returns {}
        self._model_metadata = self._try_load_json(
            os.path.join(
                self._conf["m2ee"]["app_base"], "model", "metadata.json",
            )
        )

        self.runtime_version = self._lookup_runtime_version()

        self._conf["mxruntime"] = self._merge_microflow_constants()

        self._runtime_path = None
        if (
            not self._run_from_source
            or self._run_from_source == "appcontainer"
        ):
            if self.runtime_version is None:
                logger.info(
                    "Unable to look up mendix runtime files "
                    "because product version is yet unknown."
                )
                self._all_systems_are_go = False
            else:
                self._runtime_path = self.lookup_in_mxjar_repo(
                    str(self.runtime_version)
                )
                if self._runtime_path is None:
                    self._all_systems_are_go = False

        self._setup_classpath()

        if self._runtime_path and "RuntimePath" not in self._conf["mxruntime"]:
            runtimePath = os.path.join(self._runtime_path, "runtime")
            logger.debug(
                "Setting RuntimePath runtime config option to %s" % runtimePath
            )
            self._conf["mxruntime"]["RuntimePath"] = runtimePath

    def _setup_classpath(self):
        logger.debug("Determining classpath to be used...")

        classpath = []

        if self._run_from_source:
            logger.debug(
                "Building classpath to run hybrid appcontainer from " "source."
            )
            classpath = self._setup_classpath_from_source()
        elif self.use_hybrid_appcontainer() and self.runtime_version < 5:
            logger.debug(
                "Hybrid appcontainer from jars does not need a " "classpath."
            )
            self._appcontainer_jar = self._lookup_appcontainer_jar()
        elif not self._appcontainer_version or self.runtime_version >= 5:
            logger.debug(
                "Building classpath to run appcontainer/runtime from " "jars."
            )
            classpath = self._setup_classpath_runtime_binary()
            classpath.extend(self._setup_classpath_model())

        if "extend_classpath" in self._conf["m2ee"]:
            if isinstance(self._conf["m2ee"]["extend_classpath"], list):
                classpath.extend(self._conf["m2ee"]["extend_classpath"])
            else:
                logger.warn(
                    "extend_classpath option in m2ee section in "
                    "configuration is not a list"
                )

        self._classpath = ":".join(classpath)
        if self._classpath:
            logger.debug("Using classpath: %s" % self._classpath)
        else:
            logger.debug("No classpath will be used")

    def _merge_microflow_constants(self):
        """
        3.0: config.json "contains the configuration settings of the active
        configuration (in the Modeler) at the time of deployment." It also
        contains default values for microflow constants. D/T configuration is
        not stored in the mdp anymore, so for D/T we need to insert it into
        the configuration we read from yaml (yay!)
        { "Configuration": { "key": "value", ... }, "Constants": {
        "Module.Constant": "value", ... } } also... move the custom section
        into the MicroflowConstants runtime config option where 3.0 now
        expects them to be! yay... (when running 2.5, the MicroflowConstants
        part of runtime config will be sent using the old
        update_custom_configuration m2ee api call. Fun!
        """

        logger.debug("Merging microflow constants configuration...")

        config_json = {}
        if not self.get_dtap_mode()[0] in ("A", "P"):
            config_json_file = os.path.join(
                self._conf["m2ee"]["app_base"], "model", "config.json"
            )
            logger.trace(
                "In DTAPMode %s, so loading configuration from %s"
                % (self.get_dtap_mode(), config_json_file)
            )
            config_json = self._try_load_json(config_json_file)

        # figure out which constants to use
        merge_constants = {}
        if not self.get_dtap_mode()[0] in ("A", "P"):
            config_json_constants = config_json.get("Constants", {})
            logger.trace(
                "In DTAPMode %s, so using Constants from "
                "config.json: %s"
                % (self.get_dtap_mode(), config_json_constants)
            )
            merge_constants.update(config_json_constants)
        # custom yaml section can override defaults
        yaml_custom = self._conf.get("custom", {})
        if yaml_custom:
            logger.trace(
                "Using constants from custom config section: %s" % yaml_custom
            )
            merge_constants.update(yaml_custom)
        # 'MicroflowConstants' from runtime yaml section can override
        # default/custom
        yaml_mxruntime_mfconstants = self._conf["mxruntime"].get(
            "MicroflowConstants", {}
        )
        if yaml_mxruntime_mfconstants:
            logger.trace(
                "Using constants from mxruntime/MicroflowConstants: "
                "%s" % yaml_mxruntime_mfconstants
            )
            merge_constants.update(yaml_mxruntime_mfconstants)
        # merge all yaml runtime settings into config
        merge_config = {}
        if not self.get_dtap_mode()[0] in ("A", "P"):
            config_json_configuration = config_json.get("Configuration", {})
            logger.trace(
                "In DTAPMode %s, so seeding runtime configuration "
                "with Configuration from config.json: %s"
                % (self.get_dtap_mode(), config_json_configuration)
            )
            merge_config.update(config_json_configuration)
        merge_config.update(self._conf["mxruntime"])
        logger.trace(
            "Merging current mxruntime config into it... %s"
            % self._conf["mxruntime"]
        )
        # replace 'MicroflowConstants' with mfconstants we just figured out
        # before to prevent dict-deepmerge-problems
        merge_config["MicroflowConstants"] = merge_constants
        logger.trace(
            "Replacing 'MicroflowConstants' with constants we just "
            "figured out: %s" % merge_constants
        )
        # the merged result will be put back into self._conf['mxruntime']
        logger.debug("Merged runtime configuration: %s" % merge_config)
        return merge_config

    def _try_load_json(self, jsonfile):
        logger.debug("Loading json configuration from %s" % jsonfile)
        fd = None
        try:
            fd = open(jsonfile)
        except Exception as e:
            logger.debug(
                "Error reading configuration file %s: %s; "
                "ignoring..." % (jsonfile, e)
            )
            return {}

        config = None
        try:
            config = json.load(fd)
        except Exception as e:
            logger.error(
                "Error parsing configuration file %s: %s" % (jsonfile, e)
            )
            return {}

        logger.trace("contents read from %s: %s" % (jsonfile, config))
        return config

    def mtime_changed(self):
        for yamlfile, mtime in self._mtimes.items():
            if os.stat(yamlfile)[8] != mtime:
                return True
        return False

    def dump(self):
        print(yaml.dump(self._conf))

    def _check_appcontainer_config(self):
        # did we load any configuration at all?
        if not self._conf:
            logger.critical(
                "No configuration present. Please put a m2ee.yaml "
                "configuration file at the default location "
                "~/.m2ee/m2ee.yaml or specify an alternate "
                "configuration file using the -c option."
            )
            sys.exit(1)

        # TODO: better exceptions
        self._run_from_source = self._conf.get("mxnode", {}).get(
            "run_from_source", False
        )

        # mxnode
        if self._run_from_source:
            if not self._conf["mxnode"].get("source_workspace", None):
                logger.critical(
                    "Run from source was selected, but "
                    "source_workspace is not specified!"
                )
                sys.exit(1)
            if not self._conf["mxnode"].get("source_projects", None):
                logger.critical(
                    "Run from source was selected, but "
                    "source_projects is not specified!"
                )
                sys.exit(1)

        # m2ee
        for option in ["app_base", "admin_port", "admin_pass"]:
            if not self._conf["m2ee"].get(option, None):
                logger.critical(
                    "Option %s in configuration section m2ee is "
                    "not defined!" % option
                )
                sys.exit(1)

        # force admin_pass to a string, prevent TypeError when base64-ing it
        # before sending to m2ee api
        self._conf["m2ee"]["admin_pass"] = str(
            self._conf["m2ee"]["admin_pass"]
        )

        # Mendix >= 4.3: admin and runtime port only bind to localhost by
        # default
        self._conf["m2ee"]["admin_listen_addresses"] = self._conf["m2ee"].get(
            "admin_listen_addresses", ""
        )
        self._conf["m2ee"]["runtime_listen_addresses"] = self._conf[
            "m2ee"
        ].get("runtime_listen_addresses", "")

        # check admin_pass 1 or password... refuse to accept when users don't
        # change default passwords
        if (
            self._conf["m2ee"]["admin_pass"] == "1"
            or self._conf["m2ee"]["admin_pass"] == "password"
        ):
            logger.critical(
                "Using admin_pass '1' or 'password' is not "
                "allowed. Please put a long, random password into "
                "the admin_pass configuration option. At least "
                "change the default!"
            )
            sys.exit(1)

        # database_dump_path
        if "database_dump_path" not in self._conf["m2ee"]:
            self._conf["m2ee"]["database_dump_path"] = os.path.join(
                self._conf["m2ee"]["app_base"], "data", "database"
            )
        if not os.path.isdir(self._conf["m2ee"]["database_dump_path"]):
            logger.warn(
                "Database dump path %s is not a directory"
                % self._conf["m2ee"]["database_dump_path"]
            )

    def _check_runtime_config(self):
        self._run_from_source = self._conf.get("mxnode", {}).get(
            "run_from_source", False
        )

        if (
            not self._run_from_source
            or self._run_from_source == "appcontainer"
        ):
            # ensure mxjar_repo is a list, multiple locations are allowed for
            # searching
            if not self._conf.get("mxnode", {}).get("mxjar_repo", None):
                self._conf["mxnode"]["mxjar_repo"] = []
            elif not type(self._conf.get("mxnode", {})["mxjar_repo"]) == list:
                self._conf["mxnode"]["mxjar_repo"] = [
                    self._conf["mxnode"]["mxjar_repo"]
                ]
        # m2ee
        for option in ["app_name", "app_base", "runtime_port"]:
            if not self._conf["m2ee"].get(option, None):
                logger.warn(
                    "Option %s in configuration section m2ee is not "
                    "defined!" % option
                )
        # check some locations for existance and permissions
        basepath = self._conf["m2ee"]["app_base"]
        if not os.path.exists(basepath):
            logger.critical(
                "Application base directory %s does not exist!" % basepath
            )
            sys.exit(1)

        # model_upload_path
        if "model_upload_path" not in self._conf["m2ee"]:
            self._conf["m2ee"]["model_upload_path"] = os.path.join(
                self._conf["m2ee"]["app_base"], "data", "model-upload"
            )
        if not os.path.isdir(self._conf["m2ee"]["model_upload_path"]):
            logger.warn(
                "Model upload path %s is not a directory"
                % self._conf["m2ee"]["model_upload_path"]
            )

        # magically add app_base/runtimes to mxjar_repo when it's present
        magic_runtimes = os.path.join(
            self._conf["m2ee"]["app_base"], "runtimes"
        )
        if magic_runtimes not in self._conf["mxnode"][
            "mxjar_repo"
        ] and os.path.isdir(magic_runtimes):
            self._conf["mxnode"]["mxjar_repo"].insert(0, magic_runtimes)

    def fix_permissions(self):
        basepath = self._conf["m2ee"]["app_base"]
        for directory, mode in {
            "model": 0o0700,
            "web": 0o0755,
            "data": 0o0700,
        }.items():
            fullpath = os.path.join(basepath, directory)
            if not os.path.isdir(fullpath):
                logger.warn(
                    "Directory '%s' does not exist, unable to fixup permissions!"
                    % fullpath
                )
                continue
            try:
                if os.stat(fullpath).st_mode & 0xFFF != mode:
                    os.chmod(fullpath, mode)
                    logger.info(
                        "Fixing up permissions of directory '%s' "
                        "with mode %s" % (directory, oct(mode)[-3:])
                    )
            except Exception as e:
                logger.error(
                    "Unable to fixup permissions of directory '%s' "
                    "with mode %s: %s, Ignoring."
                    % (directory, oct(mode)[-3:], e)
                )

    def get_felix_config_file(self):
        return self._conf["m2ee"].get(
            "felix_config_file",
            os.path.join(
                self._conf["m2ee"]["app_base"],
                "model",
                "felixconfig.properties",
            ),
        )

    def write_felix_config(self):
        felix_config_file = self.get_felix_config_file()
        felix_config_path = os.path.dirname(felix_config_file)
        if not os.access(felix_config_path, os.W_OK):
            logger.critical(
                "felix_config_file is not in a writable "
                "location: %s" % felix_config_path
            )
            return False

        project_bundles_path = os.path.join(
            self._conf["m2ee"]["app_base"], "model", "bundles"
        )
        osgi_storage_path = os.path.join(
            self._conf["m2ee"]["app_base"], "data", "tmp", "felixcache"
        )
        felix_template_file = os.path.join(
            self._runtime_path, "runtime", "felixconfig.properties.template"
        )
        if os.path.exists(felix_template_file):
            logger.debug(
                "writing felix configuration template from %s "
                "to %s" % (felix_template_file, felix_config_file)
            )
            try:
                input_file = open(felix_template_file)
                template = input_file.read()
            except IOError as e:
                logger.error(
                    "felix configuration template could not be " "read: %s", e
                )
                return False
            try:
                output_file = open(felix_config_file, "w")
                render = template.format(
                    ProjectBundlesDir=project_bundles_path,
                    InstallDir=self._runtime_path,
                    FrameworkStorage=osgi_storage_path,
                )
                output_file.write(render)
            except IOError as e:
                logger.error(
                    "felix configuration file could not be " "written: %s", e
                )
                return False
        else:
            logger.error(
                "felix configuration template is not a readable "
                "file: %s" % felix_template_file
            )
            return False
        return True

    def get_app_name(self):
        return self._conf["m2ee"]["app_name"]

    def get_app_base(self):
        return self._conf["m2ee"]["app_base"]

    def get_default_dotm2ee_directory(self):
        dotm2ee = os.path.join(pwd.getpwuid(os.getuid())[5], ".m2ee")
        if not os.path.isdir(dotm2ee):
            try:
                os.mkdir(dotm2ee)
            except OSError as e:
                logger.debug("Got %s: %s" % (type(e), e))
                import traceback

                logger.debug(traceback.format_exc())
                logger.critical(
                    "Directory %s does not exist, and cannot be " "created!"
                )
                logger.critical(
                    "If you do not want to use .m2ee in your home "
                    "directory, you have to specify pidfile, "
                    "munin -> config_cache in your configuration "
                    "file"
                )
                sys.exit(1)

        return dotm2ee

    def get_runtime_blocking_connector(self):
        return self._conf["m2ee"].get("runtime_blocking_connector", False)

    def get_symlink_mxclientsystem(self):
        return self._conf["m2ee"].get("symlink_mxclientsystem", True)

    def get_post_unpack_hook(self):
        return self._conf["m2ee"].get("post_unpack_hook", False)

    def get_public_webroot_path(self):
        return self._conf["mxruntime"].get(
            "PublicWebrootPath",
            os.path.join(self._conf["m2ee"]["app_base"], "web"),
        )

    def get_real_mxclientsystem_path(self):
        if "MxClientSystemPath" in self._conf["mxruntime"]:
            return self._conf["mxruntime"].get("MxClientSystemPath")
        else:
            return os.path.join(
                self._runtime_path, "runtime", "mxclientsystem"
            )

    def get_mimetypes(self):
        return self._conf["mimetypes"]

    def all_systems_are_go(self):
        return self._all_systems_are_go

    def get_java_env(self):
        env = {}

        preserve_environment = self._conf["m2ee"].get(
            "preserve_environment", False
        )
        if preserve_environment is True:
            env = os.environ.copy()
        elif preserve_environment is False:
            pass
        elif type(preserve_environment) == list:
            for varname in preserve_environment:
                if varname in os.environ:
                    env[varname] = os.environ[varname]
                else:
                    logger.warn(
                        "preserve_environment variable %s is not "
                        "present in os.environ" % varname
                    )
        else:
            logger.warn("preserve_environment is not a boolean or list")

        custom_environment = self._conf["m2ee"].get("custom_environment", {})
        if custom_environment is not None:
            if type(custom_environment) == dict:
                env.update(custom_environment)
            else:
                logger.warn(
                    "custom_environment option in m2ee section in "
                    "configuration is not a dictionary"
                )

        env.update(
            {
                "M2EE_ADMIN_PORT": str(self._conf["m2ee"]["admin_port"]),
                "M2EE_ADMIN_PASS": str(self._conf["m2ee"]["admin_pass"]),
                # only has effect with Mendix >= 4.3, but include anyway as
                # it does not break earlier versions
                "M2EE_ADMIN_LISTEN_ADDRESSES": str(
                    self._conf["m2ee"]["admin_listen_addresses"]
                ),
                "M2EE_RUNTIME_LISTEN_ADDRESSES": str(
                    self._conf["m2ee"]["runtime_listen_addresses"]
                ),
            }
        )

        # only add RUNTIME environment variables when using default
        # appcontainer from runtime distro
        if not self._appcontainer_version and self.runtime_version < 5:
            env["M2EE_RUNTIME_PORT"] = str(self._conf["m2ee"]["runtime_port"])
            if "runtime_blocking_connector" in self._conf["m2ee"]:
                env["M2EE_RUNTIME_BLOCKING_CONNECTOR"] = str(
                    self._conf["m2ee"]["runtime_blocking_connector"]
                )

        if "monitoring_pass" in self._conf["m2ee"]:
            env["M2EE_MONITORING_PASS"] = str(
                self._conf["m2ee"]["monitoring_pass"]
            )

        return env

    def get_java_cmd(self):
        """
        Build complete JVM startup command line
        """
        cmd = []
        cmd.append(self._conf["m2ee"].get("javabin", "java"))

        if "javaopts" in self._conf["m2ee"]:
            if isinstance(self._conf["m2ee"]["javaopts"], list):
                cmd.extend(self._conf["m2ee"]["javaopts"])
            else:
                logger.warn(
                    "javaopts option in m2ee section in configuration "
                    "is not a list"
                )
        if self.runtime_version >= 7:
            cmd.extend(
                [
                    "-DMX_INSTALL_PATH=%s" % self._runtime_path,
                    "-jar",
                    os.path.join(
                        self._runtime_path,
                        "runtime/launcher/runtimelauncher.jar",
                    ),
                    self.get_app_base(),
                ]
            )
        elif self._classpath:
            cmd.extend(["-cp", self._classpath])

            if self.runtime_version >= 5:
                cmd.append(
                    "-Dfelix.config.properties=file:%s"
                    % self.get_felix_config_file()
                )

            cmd.append(self._get_appcontainer_mainclass())
        elif self._appcontainer_version:
            cmd.extend(["-jar", self._appcontainer_jar])
        else:
            logger.critical("Unable to determine JVM startup parameters.")
            return None

        return cmd

    def _lookup_appcontainer_jar(self):
        if self._appcontainer_version is None:
            # this probably means a bug in this program
            logger.critical(
                "Trying to look up appcontainer jar, but "
                "_appcontainer_version is not defined."
            )
            self._all_systems_are_go = False
            return ""

        appcontainer_path = self.lookup_in_mxjar_repo(
            "appcontainer-%s" % self._appcontainer_version
        )
        if appcontainer_path is None:
            logger.critical(
                "AppContainer not found for version %s"
                % self._appcontainer_version
            )
            self._all_systems_are_go = False
            return ""

        return os.path.join(appcontainer_path, "appcontainer.jar")

    def get_admin_port(self):
        return self._conf["m2ee"]["admin_port"]

    def get_admin_pass(self):
        return self._conf["m2ee"]["admin_pass"]

    def get_xmpp_credentials(self):
        if "xmpp" in self._conf["m2ee"]:
            if isinstance(self._conf["m2ee"]["xmpp"], dict):
                return self._conf["m2ee"]["xmpp"]
            else:
                logger.warn(
                    "xmpp option in m2ee section in configuration is "
                    "not a dictionary"
                )
        return None

    def get_runtime_port(self):
        return self._conf["m2ee"]["runtime_port"]

    def get_runtime_listen_addresses(self):
        return self._conf["m2ee"].get("runtime_listen_addresses", "")

    def get_pidfile(self):
        return self._conf["m2ee"].get(
            "pidfile",
            os.path.join(self.get_default_dotm2ee_directory(), "m2ee.pid"),
        )

    def get_logfile(self):
        return self._conf["m2ee"].get("logfile", None)

    def get_runtime_config(self):
        return self._conf["mxruntime"]

    def get_logging_config(self):
        return self._conf["logging"]

    def get_jetty_options(self):
        jetty_opts = copy.deepcopy(self._conf["m2ee"].get("jetty"))
        if jetty_opts is None:
            jetty_opts = {}
        if self.get_runtime_version() >= 5:
            jetty_opts["use_blocking_connector"] = jetty_opts.get(
                "use_blocking_connector", self.get_runtime_blocking_connector()
            )
        return jetty_opts

    def get_munin_options(self):
        return self._conf["m2ee"].get("munin", {})

    def get_dtap_mode(self):
        return self._conf["mxruntime"]["DTAPMode"].upper()

    def allow_destroy_db(self):
        return self._conf["m2ee"].get("allow_destroy_db", True)

    def is_using_postgresql(self):
        databasetype = self._conf["mxruntime"].get("DatabaseType", None)
        return (
            isinstance(databasetype, str)
            and databasetype.lower() == "postgresql"
        )

    def get_pg_environment(self):
        if not self.is_using_postgresql():
            logger.warn("Only PostgreSQL databases are supported right now.")
        # rip additional :port from hostName, but allow occurrence of plain
        # ipv6 address between []-brackets (simply assume [ipv6::] when ']' is
        # found in string (also see JDBCDataStoreConfiguration in MxRuntime)
        host = self._conf["mxruntime"]["DatabaseHost"]
        port = "5432"
        ipv6end = host.rfind("]")
        lastcolon = host.rfind(":")
        if ipv6end != -1 and lastcolon > ipv6end:
            # "]" found and ":" exists after the "]"
            port = host[lastcolon + 1 :]
            host = host[1:ipv6end]
        elif ipv6end != -1:
            # "]" found but no ":" exists after the "]"
            host = host[1:ipv6end]
        elif ipv6end == -1 and lastcolon != -1:
            # no "]" found and ":" exists, simply split on ":"
            port = host[lastcolon + 1 :]
            host = host[:lastcolon]

        # TODO: sanity checks
        pg_env = {
            "PGHOST": host,
            "PGPORT": port,
            "PGUSER": self._conf["mxruntime"]["DatabaseUserName"],
            "PGPASSWORD": self._conf["mxruntime"]["DatabasePassword"],
            "PGDATABASE": self._conf["mxruntime"]["DatabaseName"],
        }
        logger.trace("PostgreSQL environment variables: %s" % str(pg_env))
        return pg_env

    def get_psql_binary(self):
        return self._conf["mxnode"].get("psql", "psql")

    def get_pg_dump_binary(self):
        return self._conf["mxnode"].get("pg_dump", "pg_dump")

    def get_pg_restore_binary(self):
        return self._conf["mxnode"].get("pg_restore", "pg_restore")

    def get_first_writable_mxjar_repo(self):
        repos = self._conf["mxnode"]["mxjar_repo"]
        logger.debug("Searching for writeable mxjar repos... in %s" % repos)
        repos = [repo for repo in repos if os.access(repo, os.W_OK)]
        if len(repos) > 0:
            found = repos[0]
            logger.debug("Found writable mxjar location: %s" % found)
            return found
        else:
            logger.debug("No writable mxjar location found")
            return None

    def get_runtime_download_url(self, version):
        url = self._conf["mxnode"].get(
            "download_runtime_url", "https://download.mendix.com/runtimes/"
        )
        if url[-1] != "/":
            url += "/"
        url += "mendix-%s.tar.gz" % version
        return url

    def get_database_dump_path(self):
        return self._conf["m2ee"]["database_dump_path"]

    def get_model_upload_path(self):
        return self._conf["m2ee"]["model_upload_path"]

    def get_appcontainer_version(self):
        return self._appcontainer_version

    def use_hybrid_appcontainer(self):
        return self._appcontainer_version is not None

    def get_runtime_version(self):
        return self.runtime_version

    def get_classpath(self):
        return self._classpath

    def _get_appcontainer_mainclass(self):
        if self.runtime_version // 2.5:
            return "com.mendix.m2ee.server.M2EE"
        if self.runtime_version // 3 or self.runtime_version // 4:
            if self.use_hybrid_appcontainer():
                return "com.mendix.m2ee.AppContainer"
            return "com.mendix.m2ee.server.HttpAdminAppContainer"
        if self.runtime_version >= 5:
            return "org.apache.felix.main.Main"

        raise Exception(
            "Trying to determine appcontainer main class for "
            "runtime version %s. Please report this as a bug."
            % self.runtime_version
        )

    def _setup_classpath_from_source(self):
        # when running from source, grab eclipse projects:
        logger.debug("Running from source.")
        classpath = []

        wsp = self._conf["mxnode"]["source_workspace"]
        for proj in self._conf["mxnode"]["source_projects"]:
            classpath.append(os.path.join(wsp, proj, "bin"))
            libdir = os.path.join(wsp, proj, "lib")
            if os.path.isdir(libdir):
                classpath.append(os.path.join(libdir, "*"))

        return classpath

    def _setup_classpath_runtime_binary(self):
        """
        Returns the location of the mendix runtime files and the
        java classpath or None if the classpath cannot be determined
        (i.e. the Mendix Runtime is not available on this system)
        """

        logger.debug("Running from binary distribution.")
        classpath = []

        if not self._runtime_path:
            logger.debug(
                "runtime_path is empty, no classpath can be " "determined"
            )
            return []

        if self.runtime_version < 5:
            classpath.extend(
                [
                    os.path.join(self._runtime_path, "server", "*"),
                    os.path.join(self._runtime_path, "server", "lib", "*"),
                    os.path.join(self._runtime_path, "runtime", "*"),
                    os.path.join(self._runtime_path, "runtime", "lib", "*"),
                ]
            )
        elif self.runtime_version >= 5:
            classpath.extend(
                [
                    os.path.join(
                        self._runtime_path,
                        "runtime",
                        "felix",
                        "bin",
                        "felix.jar",
                    ),
                    os.path.join(
                        self._runtime_path,
                        "runtime",
                        "lib",
                        "com.mendix.xml-apis-1.4.1.jar",
                    ),
                ]
            )

        return classpath

    def _setup_classpath_model(self):

        classpath = []

        if self.runtime_version < 5:
            # put model lib into classpath
            model_lib = os.path.join(
                self._conf["m2ee"]["app_base"], "model", "lib"
            )
            if os.path.isdir(model_lib):
                # put all jars into classpath
                classpath.append(os.path.join(model_lib, "userlib", "*"))
                # put all directories as themselves into classpath
                classpath.extend(
                    [
                        os.path.join(model_lib, name)
                        for name in os.listdir(model_lib)
                        if os.path.isdir(os.path.join(model_lib, name))
                    ]
                )
            else:
                logger.info(
                    "No current unpacked application model is available. "
                    "Use the unpack command to unpack a mendix deployment "
                    "archive from %s" % self._conf["m2ee"]["model_upload_path"]
                )

        return classpath

    def _lookup_runtime_version(self):
        logger.debug("Determining runtime version to be used...")

        # force to a specific version
        if self._conf["m2ee"].get("runtime_version", None):
            logger.debug(
                "Runtime version forced to %s in configuration"
                % self._conf["m2ee"]["runtime_version"]
            )
            return MXVersion(self._conf["m2ee"]["runtime_version"])

        # 3.0 has runtime version in metadata.json
        if "RuntimeVersion" in self._model_metadata:
            logger.debug(
                "MxRuntime version listed in model metadata: %s"
                % self._model_metadata["RuntimeVersion"]
            )
            return MXVersion(self._model_metadata["RuntimeVersion"])

        # else, 2.5: try to read from model.mdp using sqlite
        import sqlite3

        model_mdp = os.path.join(
            self._conf["m2ee"]["app_base"], "model", "model.mdp"
        )
        if not os.path.isfile(model_mdp):
            logger.debug(
                "Mendix 2.5? No, %s is not a file! Giving up now..."
                % model_mdp
            )
            return None
        version = None
        try:
            conn = sqlite3.connect(model_mdp)
            c = conn.cursor()
            c.execute("SELECT _ProductVersion FROM _MetaData LIMIT 1;")
            version = c.fetchone()[0]
            c.close()
            conn.close()
        except sqlite3.Error as e:
            logger.error(
                "An error occured while trying to read mendix "
                "version number from model.mdp: %s" % e
            )
            return None

        # hack: force convert sqlite string to ascii, this prevents syslog from
        # stumbling over it because a BOM will appear which messes up syslog
        # <U+FEFF><183>m2ee: (bofht) DEBUG - MxRuntime version listed in
        # application model file: 2.5.3 also see
        # http://en.wikipedia.org/wiki/Byte_order_mark
        version = version.encode("ascii", "ignore")
        # TODO: is this only syslog cosmetics, or does splitting syslog into
        # files based on progname break here? needs a bit of testing...

        if not re.match(r"^[\w.-]+$", version):
            logger.error(
                "Invalid version number in model.mdp: %s (not a "
                "release build?)" % version
            )
            return None

        logger.debug(
            "MxRuntime version listed in application model file: %s" % version
        )

        return MXVersion(version)

    def lookup_in_mxjar_repo(self, dirname):
        logger.debug("Searching for %s in mxjar repo locations..." % dirname)
        path = None
        for repo in self._conf["mxnode"]["mxjar_repo"]:
            try_path = os.path.join(repo, dirname)
            if os.path.isdir(try_path):
                path = try_path
                logger.debug("Using: %s" % path)
                break

        return path

    def get_runtime_path(self):
        return self._runtime_path

    def has_database_password(self):
        return "DatabasePassword" in self._conf["mxruntime"]

    def _warn_constants(self):
        if "Constants" not in self._model_metadata:
            return
        if "MicroflowConstants" not in self._conf["mxruntime"]:
            return

        model_constants = [
            constant["Name"] for constant in self._model_metadata["Constants"]
        ]
        yaml_constants = list(
            self._conf["mxruntime"]["MicroflowConstants"].keys()
        )

        missing = [m for m in model_constants if m not in yaml_constants]
        if missing:
            logger.warn("Constants not defined:")
            for constant in missing:
                logger.warn("- %s" % constant)

        obsolete = [m for m in yaml_constants if m not in model_constants]
        if obsolete:
            logger.info("Constants defined but not needed by application:")
            for constant in obsolete:
                logger.info("- %s" % constant)


def find_yaml_files():
    yaml_files = []
    # don't add deprecated m2eerc-file if yaml is present
    # (if both exist, probably one is a symlink to the other...)
    if os.path.isfile("/etc/m2ee/m2ee.yaml"):
        yaml_files.append("/etc/m2ee/m2ee.yaml")
    elif os.path.isfile("/etc/m2ee/m2eerc"):
        yaml_files.append("/etc/m2ee/m2eerc")

    homedir = pwd.getpwuid(os.getuid())[5]
    if os.path.isfile(os.path.join(homedir, ".m2ee/m2ee.yaml")):
        yaml_files.append(os.path.join(homedir, ".m2ee/m2ee.yaml"))
    elif os.path.isfile(os.path.join(homedir, ".m2eerc")):
        yaml_files.append(os.path.join(homedir, ".m2eerc"))
    return yaml_files


def read_yaml_files(yaml_files):
    config = defaultdict(dict)
    yaml_mtimes = {}

    for yaml_file in yaml_files:
        additional_config = load_config(yaml_file)
        config = merge_config(config, additional_config)
        yaml_mtimes[yaml_file] = os.stat(yaml_file)[8]

    return (yaml_mtimes, config)


def load_config(yaml_file):
    logger.debug("Loading configuration from %s" % yaml_file)
    fd = None
    try:
        fd = open(yaml_file)
    except Exception as e:
        logger.error(
            "Error reading configuration file %s, ignoring..." % yaml_file
        )
        return

    try:
        return yaml.load(fd, Loader=yaml.FullLoader)
    except Exception as e:
        logger.error(
            "Error parsing configuration file %s: %s" % (yaml_file, e)
        )
        return


def merge_config(initial_config, additional_config):
    result = copy.deepcopy(initial_config)
    if additional_config is None:
        return result
    additional_config = copy.deepcopy(additional_config)
    if initial_config is None:
        return additional_config

    for section in set(
        list(initial_config.keys()) + list(additional_config.keys())
    ):
        if section in initial_config:
            if section in additional_config:
                if isinstance(additional_config[section], dict):
                    result[section] = merge_config(
                        initial_config[section], additional_config[section]
                    )
                elif isinstance(additional_config[section], list):
                    result[section] = (
                        initial_config[section] + additional_config[section]
                    )
                else:
                    result[section] = additional_config[section]
        else:
            result[section] = additional_config[section]

    return result


if __name__ == "__main__":
    config = M2EEConfig(sys.argv[1:])
    config.dump()