"""
 *
 * 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
import os
import pwd
import subprocess
import time
from threading import Timer

from config_manager import ConfigManager
from util.log import log
from util.string_util import get_timestamp, binary_to_string, strip_whitespace


class UserInfo:
    def __init__(self, name):
        """
        Creates a user information record from the name
        :param str name: the username
        """
        pw_record = pwd.getpwnam(name)
        self.user_name = pw_record.pw_name
        self.user_home_dir = pw_record.pw_dir
        self.user_uid = pw_record.pw_uid
        self.user_gid = pw_record.pw_gid


def get_env_for_user(userinfo, cwd, env=None):
    """
    Gets the environment for a specific user
    :param UserInfo userinfo:the user information
    :param str cwd: the cwd
    :param dict env: an existing environment or none
    """
    if env is None:
        env = os.environ.copy()
    env['HOME'] = userinfo.user_home_dir
    env['LOGNAME'] = userinfo.user_name
    env['PWD'] = cwd
    env['USER'] = userinfo.user_name
    return env


def demote_user_callback(user_info):
    """
    Generates a callback to be used once the process forks
    :param UserInfo user_info: the user information
    :rtype: function
    """

    def result():
        os.setgid(user_info.user_gid)
        os.setuid(user_info.user_uid)

    return result


def kill_proc(proc, timed_out):
    timed_out["value"] = True
    proc.kill()


class ExecutionError(Exception):
    def __init__(self, command, stdout, stderr, return_code):
        """
        Class to describe an execution error
        :param str command: the command that was tried
        :param str stdout: the std output
        :param str stderr: the std error
        :param int return_code: the return code
        """
        self.command = binary_to_string(command)
        self.return_code = return_code
        self.stdout = strip_whitespace(binary_to_string(stdout))
        self.stderr = strip_whitespace(binary_to_string(stderr))

    def __str__(self):
        sep = "+-----------------------------------------------------------------------------------------"
        ret = "\n" + sep + "\n"
        ret +=  "+   command: " + self.command + "\n"
        ret += "+ exit code: " + str(self.return_code) + "\n"
        if len(self.stdout) > 0:
            ret += "+    stdout:\n" + self.stdout + "\n"
        if len(self.stderr) > 0:
            ret += "+    stderr:\n" + self.stderr + "\n"
        ret += sep + "\n"
        return ret


class Executor:
    def __init__(self, historic):
        """
        Class to execute shell commands
        :param History historic: the history object to save the commands
        """
        self.historic = historic

    def execute(self, command, working_dir=None, stdin=None, user=None, env=None,
                throw_on_error=True, no_wait=False, shell=False, retry=0,
                timeout_sec=-1, warn_on_error=False):
        """
        Executes the command in the given working directory
        :param list[str] command: the command as a list of words, e.g.
         "make install" => ["make", "install"]
        :param str working_dir: the path to the working dir, defaults to cwd
        :param str stdin: a string to be given as the stdin input
        :param str user: a username to execute the command. This is only
         possible if the executing user has sudo access; defaults to
         ConfigManager.default_user user
        :param dict env: a environment dict as returned by os.environ.copy()
        :param bool throw_on_error: set to false to prevent the executor from
         throwing exceptions when the return code is different than 0
        :param bool no_wait: if set to true it will not wait for the return code
         and stdout. This is especially important when executing pg_ctl CMD for
         example, which for whatever reason blocks on Docker when trying to get
         the stdout of it.
        :param bool shell: to run the command through a shell
        :param int retry: number of times to retry a command before throwing an
         exception on error
        :param int timeout_sec: timeout in seconds before terminating the command
        :param bool warn_on_error: log a warning in case the return code of
        command is not 0.
        :return: returns stdout, stderr, exit code
        :rtype: (str, str, int)
        """
        start = time.time()
        start_time = get_timestamp(start)
        if command is None or command == []:
            return "", "", 0
        if user is None:
            user = ConfigManager.default_user
        stdout, stderr, return_code = "", "", -1
        cmd_string = " ".join(command) if type(command) == list else command
        cmd_string = binary_to_string(cmd_string)
        try:
            # log.debug("Executing command with user " + user + ": " + cmd_string)
            userinfo = UserInfo(user)
            if working_dir is None:
                working_dir = os.getcwd()
            environment = get_env_for_user(userinfo, working_dir, env)
            preexec_fn = demote_user_callback(userinfo)

            while retry >= 0 and return_code != 0:
                proc = subprocess.Popen(command,
                                        preexec_fn=preexec_fn,
                                        cwd=working_dir,
                                        stdin=subprocess.PIPE,
                                        stdout=subprocess.PIPE,
                                        stderr=subprocess.PIPE,
                                        env=environment,
                                        shell=shell)
                if no_wait:
                    break

                # execute command, with or without a timeout
                if timeout_sec > 0:
                    # will be set to True if the process timed out
                    timed_out = {"value": False}
                    timer = Timer(timeout_sec, kill_proc, [proc, timed_out])
                    try:
                        # start a timer that will kill the process if it isn't
                        # cancelled below before the process returns
                        timer.start()
                        stdout, stderr = proc.communicate(input=stdin)
                        stdout = binary_to_string(stdout)
                        stderr = binary_to_string(stderr)
                    finally:
                        timer.cancel()

                    if timed_out["value"]:
                        stderr += "\nCommand execution timed out after " + str(timeout_sec) + " seconds."
                    else:
                        return_code = proc.returncode
                else:
                    stdout, stderr = proc.communicate(input=stdin)
                    stdout = binary_to_string(stdout)
                    stderr = binary_to_string(stderr)
                    return_code = proc.returncode

                if return_code != 0:
                    if throw_on_error and retry == 0:
                        raise ExecutionError(cmd_string, stdout, stderr, return_code)
                    retry = retry - 1
                    if retry >= 0:
                        log.info("retrying command...")
                else:
                    break
        except OSError as e:
            if e.errno == 1 and e.strerror == "Operation not permitted":
                log.error("OS error: " + str(e) + "; most likely you need to execute with sudo/root user.")
            else:
                log.error("OS error: " + str(e))
            if throw_on_error:
                raise ExecutionError(cmd_string, stdout, e.strerror, e.errno)
        except ExecutionError as e:
            raise e
        except Exception as e:
            log.error("Failed executing command (" + cmd_string + "): " + str(e))
            if throw_on_error:
                raise ExecutionError(cmd_string, stdout, str(e), return_code)
        finally:
            end = time.time()
            self.historic.add_to_history(cmd_string, stdout, stderr, return_code,
                                         user, start_time, end - start)

        if warn_on_error and return_code != 0:
            log.warn("command failed: {}\nexit code: {}\nstdout: {}\nstderr: {}".format(
                cmd_string, return_code, stdout, stderr))
        return stdout, stderr, return_code

    def executeSudo(self, command, working_dir=None, stdin=None, env=None,
                    throw_on_error=True, no_wait=False, shell=False, retry=0,
                    timeout_sec=-1, warn_on_error=False):
        """
        Executes the command as the root user. See the execute method for
        parameter and result documentation.
        """
        return self.execute(command, working_dir, stdin, "root", env,
                            throw_on_error, no_wait, shell, retry, timeout_sec,
                            warn_on_error)

    def executeWithNewShell(self, command, working_dir=None, stdin=None, user=None,
                            env=None, throw_on_error=True, no_wait=False,
                            shell=False, retry=0, timeout_sec=-1, warn_on_error=False):
        """
        Executes the command in a reloaded shell. You will have to escape your
         arguments manually. This is useful for the moments where you need to
         simulate a logout / login, e.g. after adding a new group to some user.
        :param list[str] command: the command as a list of words, e.g.
         "make install" => ["make", "install"]
        :param str working_dir: the path to the working dir, defaults to cwd
        :param str stdin: a string to be given as the stdin input
        :param str user: a username to execute the command. This is only
         possible if the executing user has sudo access; defaults to
         ConfigManager.default_user user
        :param dict env: a environment dict as returned by os.environ.copy()
        :param bool throw_on_error: set to false to prevent the executor from
         throwing exceptions when the return code is different than 0
        :param bool no_wait: if set to true it will not wait for the return code
         and stdout
        :param bool shell: to run the command through a shell
        :param int retry: number of times to retry a command before throwing an
         exception on error
        :return: returns stdout and stderr
        :rtype: (str,str, int)
        """
        if user is None:
            user = ConfigManager.default_user
        command = ["sudo", "su", user, "-c", " ".join(command)]
        return self.executeSudo(command, working_dir, stdin, env, throw_on_error,
                                no_wait, shell, retry, timeout_sec, warn_on_error)
