# Copyright (c) 2019 Red Hat, Inc.
#
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.
from __future__ import absolute_import

import os
import subprocess
import sys

from oslo_log import log

import tobiko
from tobiko.shell.sh import _io
from tobiko.shell.sh import _exception
from tobiko.shell.sh import _execute
from tobiko.shell.sh import _path
from tobiko.shell.sh import _process


LOG = log.getLogger(__name__)


def local_execute(command, environment=None, timeout: tobiko.Seconds = None,
                  shell=None, stdin=None, stdout=None, stderr=None,
                  expect_exit_status=0, **kwargs):
    """Execute command on local host using local shell"""
    process = local_process(command=command,
                            environment=environment,
                            timeout=timeout,
                            shell=shell,
                            stdin=stdin,
                            stdout=stdout,
                            stderr=stderr,
                            **kwargs)
    return _execute.execute_process(process=process,
                                    stdin=stdin,
                                    expect_exit_status=expect_exit_status)


def local_process(command, environment=None, current_dir=None,
                  timeout: tobiko.Seconds = None, shell=None, stdin=None,
                  stdout=None, stderr=True, sudo=None, network_namespace=None):
    return LocalShellProcessFixture(
        command=command, environment=environment, current_dir=current_dir,
        timeout=timeout, shell=shell, stdin=stdin, stdout=stdout,
        stderr=stderr, sudo=sudo, network_namespace=network_namespace)


class LocalExecutePathFixture(_path.ExecutePathFixture):

    executable_dirs = [os.path.realpath(os.path.join(d, 'bin'))
                       for d in [sys.prefix, sys.exec_prefix]]
    environ = dict(os.environ)


class LocalShellProcessFixture(_process.ShellProcessFixture):

    path_execute = tobiko.required_fixture(LocalExecutePathFixture)
    default_shell = True

    def create_process(self):
        tobiko.setup_fixture(self.path_execute)
        parameters = self.parameters
        args = self.command
        stdin = parameters.stdin and subprocess.PIPE or None
        stdout = parameters.stdout and subprocess.PIPE or None
        stderr = parameters.stderr and subprocess.PIPE or None
        env = merge_dictionaries(os.environ, parameters.environment)
        try:
            return subprocess.Popen(args=args,
                                    bufsize=parameters.buffer_size,
                                    shell=False,
                                    universal_newlines=False,
                                    env=env,
                                    cwd=parameters.current_dir,
                                    stdin=stdin,
                                    stdout=stdout,
                                    stderr=stderr)
        except FileNotFoundError as ex:
            LOG.debug(f"Error executing local command: args={args}")
            raise _exception.ShellCommandFailed(command=self.command,
                                                exit_status=-1,
                                                stdin=parameters.stdin,
                                                stdout=None,
                                                stderr=str(ex)) from ex

    def setup_stdin(self):
        self.stdin = _io.ShellStdin(delegate=self.process.stdin,
                                    buffer_size=self.parameters.buffer_size)

    def setup_stdout(self):
        self.stdout = _io.ShellStdout(delegate=self.process.stdout,
                                      buffer_size=self.parameters.buffer_size)

    def setup_stderr(self):
        self.stderr = _io.ShellStderr(delegate=self.process.stderr,
                                      buffer_size=self.parameters.buffer_size)

    def poll_exit_status(self):
        return self.process.poll()

    def _get_exit_status(self, timeout: tobiko.Seconds):
        try:
            exit_status = self.process.wait(timeout=timeout)
        except subprocess.TimeoutExpired:
            LOG.exception("Failed waiting for subprocess termination")
            return None
        else:
            assert exit_status is not None
            return exit_status

    def kill(self, sudo=False):
        process = self.process
        LOG.debug('Killing local process: %r', self.command)
        try:
            if sudo:
                _execute.execute(f"kill '{self.pid}'", sudo=True)
            else:
                process.kill()
        except Exception:
            LOG.exception('Failed killing local process: %r (PID=%r)',
                          self.command, self.pid)
        else:
            LOG.debug('Local process killed: %r (PID=%r)', self.command,
                      self.pid)

    @property
    def pid(self):
        process = self.process
        return process and process.pid or None


def merge_dictionaries(*dictionaries):
    merged = {}
    for d in dictionaries:
        if d:
            merged.update(d)
    return merged
