#
# Functions to launch and test DBus session.
#
# Functions here are NOT RECOMMENDED to be called in modules. These functions
# could have unexpected behavior when called in modules and not in main
# anaconda application.
#
# Copyright (C) 2018
# Red Hat, Inc.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Author(s):  Jiri Konecny <jkonecny@redhat.com>
#
import os
from subprocess import TimeoutExpired

from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.util import startProgram
from pyanaconda.core.constants import ANACONDA_BUS_ADDR_FILE, ANACONDA_CONFIG_TMP, \
    ANACONDA_BUS_CONF_FILE, DBUS_ANACONDA_SESSION_ADDRESS
from pyanaconda.core.dbus import DBus
from dasbus.constants import DBUS_FLAG_NONE
from pyanaconda.modules.common.constants.services import BOSS

from pyanaconda.anaconda_loggers import get_anaconda_root_logger
from pyanaconda.modules.common.task import sync_run_task

log = get_anaconda_root_logger()

__all__ = ["AnacondaDBusLauncher"]


class AnacondaDBusLauncher(object):
    """Class for launching the Anaconda DBus modules."""

    DBUS_LAUNCH_BIN = "dbus-daemon"

    def __init__(self):
        self._dbus_daemon_process = None
        self._log_file = None
        self._bus_address = None

    @property
    def bus_address(self):
        """The address of the Anaconda DBus session."""
        return self._bus_address

    def start(self):
        """Start DBus modules.

        Start the DBus session, the boss and the kickstart modules.
        """
        self._write_temporary_config()

        self._start_dbus_session()
        self._set_environment()
        self._write_bus_address()

        self._start_boss()
        self._start_modules()

    def stop(self, timeout=20):
        """Stop the DBus modules.

        Stop the DBus session, the boss and the kickstart modules.

        :param timeout: seconds to the launcher timeout
        """
        self._stop_boss_and_modules()

        self._stop_dbus_session(timeout)
        self._remove_bus_address_file()

        self._remove_temporary_config()

    def _write_temporary_config(self):
        """Create the temporary config file."""
        dirname = os.path.dirname(ANACONDA_CONFIG_TMP)

        if not os.path.exists(dirname):
            os.makedirs(dirname)

        log.info("Configuration loaded from: %s", conf.get_sources())
        log.info("Writing the runtime configuration to: %s", ANACONDA_CONFIG_TMP)
        conf.write(ANACONDA_CONFIG_TMP)

    def _remove_temporary_config(self):
        """Remove the temporary config file."""
        if os.path.exists(ANACONDA_CONFIG_TMP):
            os.unlink(ANACONDA_CONFIG_TMP)

    def _start_dbus_session(self):
        """Start dbus session if not running already."""
        command = [
            self.DBUS_LAUNCH_BIN,
            '--print-address',
            "--syslog",
            "--config-file={}".format(ANACONDA_BUS_CONF_FILE)
        ]

        self._log_file = open('/tmp/dbus.log', 'a')
        self._dbus_daemon_process = startProgram(command, stderr=self._log_file, reset_lang=False)

        if self._dbus_daemon_process.poll() is not None:
            raise RuntimeError("DBus wasn't properly started!")

        address = self._dbus_daemon_process.stdout.readline().decode('utf-8').strip()

        if not address:
            raise RuntimeError("Unable to start DBus session!")

        self._bus_address = address

    def _stop_dbus_session(self, timeout):
        """Stop DBus service and clean bus address file."""
        if self._log_file:
            self._log_file.close()

        if not self._dbus_daemon_process:
            return

        self._dbus_daemon_process.terminate()

        try:
            self._dbus_daemon_process.wait(timeout)
        except TimeoutExpired:
            log.error("DBus daemon wasn't terminated kill it now")
            self._dbus_daemon_process.kill()

        ret_code = self._dbus_daemon_process.poll()

        if ret_code is None:
            log.error("DBus daemon can't be killed!")
        elif ret_code != 0:
            log.error("DBus daemon exited with error %s", ret_code)

    def _set_environment(self):
        """Set the environment variables."""
        # pylint: disable=environment-modify
        os.environ[DBUS_ANACONDA_SESSION_ADDRESS] = self._bus_address

    def _write_bus_address(self):
        """Write the bus address to a file."""
        file_name = ANACONDA_BUS_ADDR_FILE
        run_dir = os.path.dirname(file_name)

        if not os.path.exists(run_dir):
            os.mkdir(run_dir)

        with open(file_name, 'wt') as f:
            f.write(self._bus_address)

    def _remove_bus_address_file(self):
        """Remove the file with the bus address."""
        f = ANACONDA_BUS_ADDR_FILE
        if os.path.exists(f):
            os.unlink(f)

    def _start_boss(self):
        """Start the boss."""
        bus_proxy = DBus.proxy
        bus_proxy.StartServiceByName(BOSS.service_name, DBUS_FLAG_NONE)

    def _start_modules(self):
        """Start the kickstart modules."""
        boss_proxy = BOSS.get_proxy()
        task_path = boss_proxy.StartModulesWithTask()
        task_proxy = BOSS.get_proxy(task_path)
        sync_run_task(task_proxy)

    def _stop_boss_and_modules(self):
        """Stop the boss and the kickstart modules."""
        boss_proxy = BOSS.get_proxy()
        boss_proxy.Quit()
