"""
The Base Application class for ipyparallel apps
"""
import logging
import os
import re
import sys

import traitlets
from IPython.core.application import BaseIPythonApplication
from IPython.core.application import base_aliases as base_ip_aliases
from IPython.core.application import base_flags as base_ip_flags
from IPython.utils.path import expand_path
from jupyter_client.session import Session
from tornado.ioloop import IOLoop
from traitlets import Bool, Instance, Unicode, default, observe
from traitlets.config.application import LevelFormatter, catch_config_error

from .._version import __version__

# FIXME: CUnicode is needed for cli parsing
# with traitlets 4
# bump when we require traitlets 5, which requires Python 3.7
if int(traitlets.__version__.split(".", 1)[0]) < 5:
    from traitlets import CUnicode
else:
    # don't need CUnicode with traitlets 4
    CUnicode = Unicode

# -----------------------------------------------------------------------------
# Module errors
# -----------------------------------------------------------------------------


class PIDFileError(Exception):
    pass


# -----------------------------------------------------------------------------
# Main application
# -----------------------------------------------------------------------------
base_aliases = {}
base_aliases.update(base_ip_aliases)
base_aliases.update(
    {
        'work-dir': 'BaseParallelApplication.work_dir',
        'log-to-file': 'BaseParallelApplication.log_to_file',
        'clean-logs': 'BaseParallelApplication.clean_logs',
        'log-url': 'BaseParallelApplication.log_url',
        'cluster-id': 'BaseParallelApplication.cluster_id',
    }
)

base_flags = {
    'log-to-file': (
        {'BaseParallelApplication': {'log_to_file': True}},
        "send log output to a file",
    )
}
base_flags.update(base_ip_flags)


class BaseParallelApplication(BaseIPythonApplication):
    """The base Application for ipyparallel apps

    Primary extensions to BaseIPythonApplication:

    * work_dir
    * remote logging via pyzmq
    * IOLoop instance
    """

    version = __version__

    _deprecated_classes = None

    def init_crash_handler(self):
        # disable crash handler from IPython
        pass

    def _log_level_default(self):
        # temporarily override default_log_level to INFO
        return logging.INFO

    def _log_format_default(self):
        """override default log format to include time"""
        return "%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"

    work_dir = Unicode(
        os.getcwd(), config=True, help='Set the working dir for the process.'
    )

    @observe('work_dir')
    def _work_dir_changed(self, change):
        self.work_dir = str(expand_path(change['new']))

    log_to_file = Bool(config=True, help="whether to log to a file")

    clean_logs = Bool(
        False, config=True, help="whether to cleanup old logfiles before starting"
    )

    log_url = Unicode(
        '', config=True, help="The ZMQ URL of the iplogger to aggregate logging."
    )

    cluster_id = CUnicode(
        '',
        config=True,
        help="""String id to add to runtime files, to prevent name collisions when
        using multiple clusters with a single profile simultaneously.

        When set, files will be named like: 'ipcontroller-<cluster_id>-engine.json'

        Since this is text inserted into filenames, typical recommendations apply:
        Simple character strings are ideal, and spaces are not recommended (but should
        generally work).
        """,
    )

    loop = Instance(IOLoop)

    def _loop_default(self):
        return IOLoop.current()

    session = Instance(Session)

    @default("session")
    def _default_session(self):
        return Session(parent=self)

    aliases = base_aliases
    flags = base_flags

    @catch_config_error
    def initialize(self, argv=None):
        """initialize the app"""
        self.init_config_from_env()
        super().initialize(argv)
        self.init_deprecated_config()
        self.to_work_dir()
        self.reinit_logging()

    def init_config_from_env(self):
        """Load any configuration from environment variables"""
        if "IPP_SESSION_KEY" in os.environ:
            self.config.Session.key = os.environ["IPP_SESSION_KEY"].encode("ascii")
        if "IPP_CLUSTER_ID" in os.environ:
            self.cluster_id = os.environ["IPP_CLUSTER_ID"]
        if "IPP_PROFILE_DIR" in os.environ:
            self.config.ProfileDir.location = os.environ["IPP_PROFILE_DIR"]

    def init_deprecated_config(self):
        if not self._deprecated_classes:
            return
        deprecated_config_found = False
        new_classname = self.__class__.__name__
        for deprecated_classname in self._deprecated_classes:
            if deprecated_classname in self.config:
                cfg = self.config[deprecated_classname]
                new_config = self.config[new_classname]
                for key, deprecated_value in list(cfg.items()):
                    if key in new_config:
                        new_value = new_config[key]
                        if new_value != deprecated_value:
                            self.log.warning(
                                f"Ignoring c.{deprecated_classname}.{key} = {deprecated_value}, overridden by c.{new_classname}.{key} = {new_value}"
                            )
                    else:
                        self.log.warning(
                            f"c.{deprecated_classname}.{key} is deprecated in ipyparallel 7, use c.{new_classname}.{key} = {deprecated_value}"
                        )
                        new_config[key] = deprecated_value
                        cfg.pop(key)
                        deprecated_config_found = True

        if deprecated_config_found:
            # reload config
            self.update_config(self.config)

    def to_work_dir(self):
        wd = self.work_dir
        if wd != os.getcwd():
            os.chdir(wd)
            self.log.info("Changing to working dir: %s" % wd)
        # This is the working dir by now.
        sys.path.insert(0, '')

    def reinit_logging(self):
        # Remove old log files
        log_dir = self.profile_dir.log_dir
        if self.clean_logs:
            for f in os.listdir(log_dir):
                if re.match(r'%s-\d+\.(log|err|out)' % self.name, f):
                    try:
                        os.remove(os.path.join(log_dir, f))
                    except OSError:
                        # probably just conflict from sibling process
                        # already removing it
                        pass
        if self.log_to_file:
            # Start logging to the new log file
            log_filename = f"{self.name}-{self.cluster_id}-{os.getpid()}.log"
            logfile = os.path.join(log_dir, log_filename)
            if sys.__stderr__:
                print(f"Sending logs to {logfile}", file=sys.__stderr__)
            open_log_file = open(logfile, 'w')
        else:
            open_log_file = None
        if open_log_file is not None:
            while self.log.handlers:
                self.log.removeHandler(self.log.handlers[0])
            self._log_handler = logging.StreamHandler(open_log_file)
            self.log.addHandler(self._log_handler)
        else:
            self._log_handler = self.log.handlers[0]
        # Add timestamps to log format:
        self._log_formatter = LevelFormatter(self.log_format, datefmt=self.log_datefmt)
        self._log_handler.setFormatter(self._log_formatter)
        # do not propagate log messages to root logger
        # ipcluster app will sometimes print duplicate messages during shutdown
        # if this is 1 (default):
        self.log.propagate = False
