"""
 *
 * This file is part of rasdaman community.
 *
 * Rasdaman community 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 3 of the License, or
 * (at your option) any later version.
 *
 * Rasdaman community 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 rasdaman community.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright 2003 - 2016 Peter Baumann / rasdaman GmbH.
 *
 * For more information please see <http://www.rasdaman.org>
 * or contact Peter Baumann via <baumann@rasdaman.com>.
 *
"""
# make sure all string literals are unicode
from __future__ import unicode_literals
from time import sleep
from os import popen
import os
from util.string_util import strip_whitespace
from util.distrowrapper import DistroWrapper, InitDTypes, DistroTypes
from util.file_util import get_values_for_property_keys
from util.log import log


def is_process_running(process_name):
    ps_output = popen("ps -Af").read()
    return process_name in ps_output[:]


class Service(object):
    """
    A service is a way to manage the lifecycle of rasdaman components, like the
    rasdaman database and petascope
    """
    def __init__(self, executor, service_name, process_name, custom_cmd=False):
        """
        Initialize
        """
        self.distro = DistroWrapper()
        self.executor = executor
        self.service_name = service_name
        self.process_name = process_name
        self.timeout = 30
        self.custom_cmd = custom_cmd or self.distro.get_initd_type() == InitDTypes.DOCKER

    def running(self):
        """
        Return True if the service is running, false otherwise.
        """
        return is_process_running(self.process_name)

    def available(self):
        """
        Return True if the service is running and is operationally available.
        """
        return self.running()

    def start(self):
        """
        Starts the service if not already running
        """
        if not self.running():
            try:
                if self.custom_cmd:
                    self.execute_custom_service_cmd('start')
                else:
                    out, err, rc = self.executor.executeSudo(
                        self.distro.get_service_start_cmd(self.service_name),
                        throw_on_error=False)
                    if rc != 0 and "systemctl status" in err:
                        svc = self.service_name + ".service"
                        show_status = ["systemctl", "status", svc]
                        self.executor.executeSudo(show_status, throw_on_error=False)
                        show_log = ["journalctl", "--no-pager", "-x", "-u", svc]
                        self.executor.executeSudo(show_log, throw_on_error=False)
                self.wait_for_service(False)
            except Exception:
                raise RuntimeError(self.service_name + " could not be started. " +
                                   "Please check the logs for more details.")

    def restart(self):
        """
        Restarts the service
        """
        try:
            if self.custom_cmd:
                self.stop()
                self.start()
            else:
                self.executor.executeSudo(self.distro.get_service_restart_cmd(self.service_name),
                                          throw_on_error=False)
        except Exception:
            raise RuntimeError(self.service_name + " could not be restarted. " +
                               "Please check the logs for more details.")

    def stop(self):
        """
        Stops the service if already running
        """
        if self.running():
            try:
                if self.custom_cmd:
                    self.execute_custom_service_cmd('stop')
                else:
                    self.executor.executeSudo(self.distro.get_service_stop_cmd(self.service_name),
                                              throw_on_error=False)
                self.wait_for_service(True)
            except Exception:
                raise RuntimeError(self.service_name + " could not be stopped. " +
                                   "Please check the logs for more details.")

    def enable(self):
        """
        Enable the service startup
        """
        if not self.custom_cmd:
            try:
                self.executor.executeSudo(self.distro.get_service_enable_cmd(self.service_name),
                                          throw_on_error=False)
                self.executor.executeSudo(self.distro.get_service_reload_cmd(self.service_name),
                                          throw_on_error=False)
            except Exception:
                log.warn(self.service_name + " could not be enabled to automatically start. " +
                         "Please check the logs for more details.")

    def disable(self):
        """
        Disable the service startup
        """
        if not self.custom_cmd:
            try:
                self.executor.executeSudo(self.distro.get_service_disable_cmd(self.service_name),
                                          throw_on_error=False)
                self.executor.executeSudo(self.distro.get_service_reload_cmd(self.service_name),
                                          throw_on_error=False)
            except Exception:
                log.warn(self.service_name + " could not be disabled to automatically start. " +
                         "Please check the logs for more details.")

    def wait_for_service(self, to_stop):
        """
        Wait (until a certain timeout, default 30 seconds), for a service to
        stop or to start.
        """
        to_start = not to_stop
        waiting = 0
        while waiting < self.timeout:
            if to_stop:
                if not self.running():
                    break
            elif to_start:
                if self.running():
                    break
            sleep(1)
            waiting += 1

    def execute_custom_service_cmd(self, cmd):
        """
        Circumvent systemd/sysv, and execute the start/stop scripts directly.
        This is necessary when installing in a Docker environment for example.
        """
        pass


class RasdamanLocalService(Service):
    def __init__(self, executor):
        """
        A local rasdaman service manager using start/stop_rasdaman.sh
        """
        super(RasdamanLocalService, self).__init__(executor, "rasdaman", "rasmgr", True)
        self.bin_path = ""
        self.is_running = None

    def __update_running(self):
        self.is_running = super(RasdamanLocalService, self).running()

    def __check_rasql_running(self):
        test_query = "SELECT C FROM RAS_COLLECTIONNAMES AS C"
        _,_,rc = self.executor.execute([self.bin_path + "/rasql", "-q", test_query],
                                       throw_on_error=False, warn_on_error=True, timeout_sec=3)
        return rc == 0

    def execute_custom_service_cmd(self, cmd):
        self.executor.execute([self.bin_path + cmd + "_rasdaman.sh"], no_wait=True)

    def stop(self):
        super(RasdamanLocalService, self).stop()
        sleep(1)
        self.__update_running()

    def start(self):
        super(RasdamanLocalService, self).start()
        sleep(5)
        self.__update_running()

    def available(self):
        return self.running() and self.__check_rasql_running()

    def running(self):
        if self.is_running is None:
            self.__update_running()
        return self.is_running

    @staticmethod
    def get_rasadmin_user(petascope_properties_file):
        """
        @return user,password pair corresponding to the rasdaman_admin_user
        and rasdaman_admin_pass keys in petascope_properties_file.
        """
        values = get_values_for_property_keys(petascope_properties_file,
                                              ["rasdaman_admin_user",
                                               "rasdaman_admin_pass"])
        if len(values) == 2:
            return values[0], values[1]
        else:
            return None, None


class RasdamanService(Service):
    def __init__(self, executor):
        """
        The rasdaman service manager
        """
        super(RasdamanService, self).__init__(executor, "rasdaman", "rasmgr")


class TomcatService(Service):

    def __init__(self, executor):
        """
        The petascope service manager
        """
        super(TomcatService, self).__init__(executor, "tomcat7", "tomcat")
        self.service_name = self.distro.get_tomcat_name()

    def execute_custom_service_cmd(self, cmd):
        self.executor.executeSudo(['/usr/libexec/tomcat/server', cmd], no_wait=True)

    def restart(self):
        super(TomcatService, self).restart()

    def start(self):
        super(TomcatService, self).start()

    def stop(self):
        super(TomcatService, self).stop()

    def uninstall_war_file(self, war_file_name, webapps_dir=None):
        """Remove war_file_name (e.g. def.war) from webapps_dir."""
        if webapps_dir is None:
            webapps_dir = self.distro.get_tomcat_path() + "/webapps"
        if os.path.exists(webapps_dir):
            to_remove = [war_file_name]
            if '.' in war_file_name:
                # remove dir as well
                to_remove.append(war_file_name.split('.')[0])
            for p in to_remove:
                path = webapps_dir + "/" + p
                if os.path.exists(path):
                    log.info("Migration: removing " + path)
                    self.executor.executeSudo(['rm', '-rf', path])

    def install_war_file(self, war_file, webapps_dir=None):
        if os.path.exists(war_file):
            if webapps_dir is None:
                webapps_dir = self.distro.get_tomcat_path() + "/webapps"
            war_file_name = os.path.basename(war_file)
            if os.path.exists(webapps_dir):
                log.info("Deploying {} in {}...".format(war_file_name, webapps_dir))
                target_file = webapps_dir + "/" + war_file_name

                # make a backup
                if os.path.exists(target_file):
                    self.executor.executeSudo(["cp", "-a", target_file, target_file + ".bak"], throw_on_error=False)

                # initialize secoredb directory
                secoredb_dir = webapps_dir + "/secoredb"
                if not os.path.exists(secoredb_dir):
                    self.executor.executeSudo(['mkdir', '-p', secoredb_dir])
                    self.executor.executeSudo(['chmod', '777', secoredb_dir])

                # copy $RMANHOME/share/rasdaman/rasdaman.war to webapps dir
                _, _, rc = self.executor.executeSudo(["cp", war_file, webapps_dir], throw_on_error=False)
                if rc == 0:
                    # remove the existing unpacked directory as it seems like
                    # sometimes the new WAR file is not unpacked fully by Tomcat
                    war_dir = webapps_dir + "/" + war_file_name.split(".")[0]
                    self.executor.executeSudo(['rm', '-rf', war_dir])
                    sleep(20) # wait to app to start
                    log.info("{} deployed successfully.".format(war_file_name))
                else:
                    log.warn("Failed copying war file.")
            else:
                log.warn("Cannot deploy {}, Tomcat webapps directory '{}' not found.".format(war_file_name, webapps_dir))


class PostgresService(Service):
    def __init__(self, executor):
        """
        The postgres service manager
        """
        super(PostgresService, self).__init__(executor, "postgresql", "postgres")

    def execute_custom_service_cmd(self, cmd):
        # Ubuntu in docker
        if self.distro.get_distro_type() == DistroTypes.UBUNTU:
            self.executor.execute(['/etc/init.d/postgresql', cmd],
                                  '/tmp', user='root', no_wait=False)
        else:
            # All other cases (like CentOS)
            self.executor.execute(['/usr/bin/pg_ctl', cmd, '-D', '/var/lib/pgsql/data/',
                                  '-s', '-o', '"-p 5432"', '-w', '-t', '30', '-m', 'fast'],
                                  '/tmp', user='postgres', no_wait=True)

    def restart(self):
        if self.custom_cmd:
            self.execute_custom_service_cmd('restart')
        else:
            super(PostgresService, self).restart()

    def initdb(self):
        """
        Initialize $PGDATA if necessary.
        """
        if self.custom_cmd:
            self.executor.execute(['initdb', '/var/lib/pgsql/data/'],
                                  user='postgres', throw_on_error=False)
        else:
            self.executor.executeSudo(["postgresql-setup", "initdb"],
                                      throw_on_error=False)
        self.start()

    def get_data_dir(self):
        """
        Returns $PGDATA
        """
        if not self.running():
            self.start()
        out, _, rc = self.executor.execute(['psql', '-t', '-P', 'format=unaligned', '-c', 'SHOW data_directory'],
                                           '/tmp', user='postgres', throw_on_error=False)
        if len(out) > 0 and rc == 0:
            return strip_whitespace(out)
        else:
            return None

    def get_hba_conf_path(self):
        """
        Returns the absolute path of the pg_hba.conf file
        """
        if not self.running():
            self.start()
        out, _, rc = self.executor.execute(['psql', '-t', '-P', 'format=unaligned', '-c', 'SHOW hba_file'],
                                           '/tmp', user='postgres', throw_on_error=False)
        if len(out) > 0 and rc == 0:
            return strip_whitespace(out)
        else:
            return '/var/lib/pgsql/data/pg_hba.conf'

    def user_exists(self, user):
        """
        Checks if the given user is a valid postgres user
        :param str user: the user to check for
        :rtype: bool
        """
        flag = "rasdaman_user_exists"
        query = "SELECT '" + flag + "' FROM pg_roles WHERE rolname='" + user + "'"
        out, _, _ = self.executor.execute(["psql", "-c", query], user="postgres",
                                          throw_on_error=False)

        if type(out) == bytes:
            out = out.decode('utf-8')

        return flag in out

    def user_can_login(self, user, passwd, database):
        """
        Check if the given user/passwd credentials are valid
        """
        query = "SELECT 1"
        if not self.database_exists(database):
            database = "template1"
        conn = "dbname={} user={} password={}".format(database, user, passwd)
        out, _, rc = self.executor.execute(["psql", "-c", query, conn],
                                          throw_on_error=False)
        return rc == 0

    def database_exists(self, database):
        """
        Checks if the given database exists
        :param str database: the database name
        :rtype: bool
        """
        flag = "db_exists"
        query = "SELECT '" + flag + "' FROM pg_database WHERE datname='" + database + "'"
        out, _, _ = self.executor.execute(["psql", "-c", query], user="postgres",
                                          throw_on_error=False)
        return flag in out
