"""
Module to provide information about minions and handle killing and restarting
minions
"""

import datetime
import logging
import os
import sys
import time

import salt.key
import salt.utils.data
import salt.utils.path
import salt.utils.systemd
from salt.exceptions import CommandExecutionError

log = logging.getLogger(__name__)

# Don't shadow built-ins.
__func_alias__ = {"list_": "list"}


def _is_systemd_system():
    """
    Check if the system uses systemd
    """
    is_linux = __grains__.get("kernel") == "Linux"
    is_booted = salt.utils.systemd.booted(__context__)
    if is_linux and is_booted:
        return True
    return False


def _is_windows_system():
    """
    Check if the system is Windows
    """
    return __grains__.get("kernel") == "Windows"


def _schedule_retry_systemd(retry_delay):
    """
    Schedule a retry for the minion restart on systemd systems
    """
    if not _is_systemd_system():
        return False

    # systemd-run --on-active=180 bash -c "systemctl is-active salt-minion || systemctl start salt-minion"
    cmd = [
        salt.utils.path.which("systemd-run"),
        f"--on-active={retry_delay}",
        "/bin/sh",
        "-c",
        "systemctl is-active salt-minion || systemctl start salt-minion",
    ]

    out = __salt__["cmd.run_all"](cmd, python_shell=False)
    if out["retcode"] != 0:
        raise CommandExecutionError(out["stderr"])
    return out


def _schedule_retry_windows(retry_delay):
    """
    Schedule a retry for the minion restart on Windows systems
    """
    if not _is_windows_system():
        return False

    when = datetime.datetime.now() + datetime.timedelta(seconds=retry_delay)
    cmd = salt.utils.path.which("powershell.exe")
    args = "-ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden -Command \"if ((Get-Service -Name salt-minion).Status -ne 'Running') { Start-Service -Name salt-minion }\""
    out = __salt__["task.create_task"](
        name="retry-minion-restart",
        user_name="System",
        force=True,
        action_type="Execute",
        cmd=cmd,
        arguments=args,
        trigger_type="Once",
        start_date=when.strftime("%Y-%m-%d"),
        start_time=when.strftime("%H:%M:%S"),
    )
    if not out:
        raise CommandExecutionError("Failed to add retry task")
    return out


def _schedule_retry(retry_delay):
    """
    Schedule a retry for the minion restart

    """

    try:
        int(retry_delay)
    except (ValueError, TypeError):
        raise CommandExecutionError(
            "Invalid retry_delay value: {}. Must be a number of seconds.".format(
                retry_delay
            )
        )
    if _is_systemd_system():
        return _schedule_retry_systemd(retry_delay)
    elif _is_windows_system():
        return _schedule_retry_windows(retry_delay)
    return False


def _restart_service(
    service_name, schedule_retry, retry_delay, comment, no_block=False
):
    """
    Helper function to restart a service with optional retry scheduling
    """
    ret = {"service_restart": {}}
    schedule_retry_failed = False

    try:
        if schedule_retry:
            ret["service_restart"]["schedule_retry"] = {}

            schedule_retry_failed = True
            sched = _schedule_retry(retry_delay)
            schedule_retry_failed = False

            if _is_systemd_system():
                ret["service_restart"]["schedule_retry"]["detail"] = sched.get(
                    "stderr", ""
                )
            ret["service_restart"]["schedule_retry"]["delay"] = retry_delay
            comment.append(
                f"Scheduled retry for minion restart in {retry_delay} seconds"
            )

        # Restart the service
        if no_block:
            ret["service_restart"]["result"] = __salt__["service.restart"](
                service_name, no_block=True
            )
        else:
            ret["service_restart"]["result"] = __salt__["service.restart"](service_name)
            if not ret["service_restart"]["result"]:
                raise CommandExecutionError(f"Failed to restart {service_name} service")

        comment.append("Service restart successful")
        ret["retcode"] = 0

    except CommandExecutionError as e:
        comment.append(
            "Adding scheduled retry failed"
            if schedule_retry_failed
            else "Service restart failed"
        )
        ret["service_restart"]["result"] = False
        ret["service_restart"]["stderr"] = str(e)
        ret["retcode"] = salt.defaults.exitcodes.EX_SOFTWARE

    return ret


def list_():
    """
    Return a list of accepted, denied, unaccepted and rejected keys.
    This is the same output as `salt-key -L`

    CLI Example:

    .. code-block:: bash

        salt 'master' minion.list
    """
    pki_dir = __salt__["config.get"]("pki_dir", "")

    # We have to replace the minion/master directories
    pki_dir = pki_dir.replace("minion", "master")

    # The source code below is (nearly) a copy of salt.key.Key.list_keys
    key_dirs = _check_minions_directories(pki_dir)

    ret = {}

    for dir_ in key_dirs:
        ret[os.path.basename(dir_)] = []
        try:
            for fn_ in salt.utils.data.sorted_ignorecase(os.listdir(dir_)):
                if not fn_.startswith("."):
                    if os.path.isfile(os.path.join(dir_, fn_)):
                        ret[os.path.basename(dir_)].append(fn_)
        except OSError:
            # key dir kind is not created yet, just skip
            continue

    return ret


def _check_minions_directories(pki_dir):
    """
    Return the minion keys directory paths.

    This function is a copy of salt.key.Key._check_minions_directories.
    """
    minions_accepted = os.path.join(pki_dir, salt.key.Key.ACC)
    minions_pre = os.path.join(pki_dir, salt.key.Key.PEND)
    minions_rejected = os.path.join(pki_dir, salt.key.Key.REJ)
    minions_denied = os.path.join(pki_dir, salt.key.Key.DEN)

    return minions_accepted, minions_pre, minions_rejected, minions_denied


def kill(timeout=15):
    """
    Kill the salt minion.

    timeout
        int seconds to wait for the minion to die.

    If you have a monitor that restarts ``salt-minion`` when it dies then this is
    a great way to restart after a minion upgrade.

    CLI Example:

    .. code-block:: bash

        salt minion[12] minion.kill

        minion1:
            ----------
            killed:
                7874
            retcode:
                0
        minion2:
            ----------
            killed:
                29071
            retcode:
                0

    The result of the salt command shows the process ID of the minions and the
    results of a kill signal to the minion in as the ``retcode`` value: ``0``
    is success, anything else is a failure.
    """

    ret = {
        "killed": None,
        "retcode": 1,
    }
    comment = []
    pid = __grains__.get("pid")
    if not pid:
        comment.append('Unable to find "pid" in grains')
        ret["retcode"] = salt.defaults.exitcodes.EX_SOFTWARE
    else:
        if "ps.kill_pid" not in __salt__:
            comment.append("Missing command: ps.kill_pid")
            ret["retcode"] = salt.defaults.exitcodes.EX_SOFTWARE
        else:
            # The retcode status comes from the first kill signal
            ret["retcode"] = int(not __salt__["ps.kill_pid"](pid))

            # If the signal was successfully delivered then wait for the
            # process to die - check by sending signals until signal delivery
            # fails.
            if ret["retcode"]:
                comment.append("ps.kill_pid failed")
            else:
                for _ in range(timeout):
                    time.sleep(1)
                    signaled = __salt__["ps.kill_pid"](pid)
                    if not signaled:
                        ret["killed"] = pid
                        break
                else:
                    # The process did not exit before the timeout
                    comment.append("Timed out waiting for minion to exit")
                    ret["retcode"] = salt.defaults.exitcodes.EX_TEMPFAIL

    if comment:
        ret["comment"] = comment
    return ret


def restart(systemd=True, win_service=True, schedule_retry=False, retry_delay=180):
    """
    Restart the salt minion.

    The method to restart the minion will be chosen as follows:

        If ``minion_restart_command`` is set in the minion configuration then
        the command specified will be used to restart the minion.

        If the minion is running as a systemd service then the minion will be
        restarted using the systemd_service module, unless ``systemd`` is
        set to ``False``

        If the minion is running as a Windows service then the minion will be
        restarted using the win_service module, unless ``win_service`` is
        set to ``False``

        If the salt-minion process is running in daemon mode (the ``-d``
        argument is present in ``argv``) then the minion will be killed and
        restarted using the same command line arguments, if possible.

        If the salt-minion process is running in the foreground (the ``-d``
        argument is not present in ``argv``) then the minion will be killed but
        not restarted. This behavior is intended for minion processes that are
        managed by a process supervisor.

    systemd
        If set to ``False`` then systemd will not be used to restart the minion.
        Defaults to ``True``.

    win_service
        If set to ``False`` then the Windows service manager will not be used to
        restart the minion. Defaults to ``True``.

    schedule_retry
        If set to ``True`` then a scheduled job will be added to start the
        minion if it has failed to restart after the retry_delay

    retry_delay
        The amount of time to wait before attempting to start the minion if it
        has failed to restart. Defaults to 180 seconds.

    CLI Examples:

    .. code-block:: bash

        salt minion[12] minion.restart

        minion1:
        ----------
            comment:
                - Using systemctl to restart salt-minion
                - Service restart successful
            killed:
                None
            restart:
                ----------
            retcode:
                0
            service_restart:
                ----------
                result:
                    True
        minion2:
            ----------
            comment:
                - Using windows service manager to restart salt-minion
                - Service restart successful
            killed:
                None
            restart:
                ----------
            retcode:
                0
            service_restart:
                ----------
                result:
                    True

    The result shows that ``minion1`` was restarted using systemd and
    ``minion2`` was restarted using the Windows service manager. The
    ``service_restart`` field indicates the result of the service restart
    operation. The ``killed`` field is ``None`` because the minion was restarted
    using the service manager and not by killing the process. The ``restart``
    field is empty because the minion was restarted using the service manager
    and not by running the command line arguments of the minion process.

    .. code-block:: bash

        salt minion[12] minion.restart

        minion1:
            ----------
            comment:
                - Restart using process argv:
                -     /home/omniture/install/bin/salt-minion
                -     -d
                -     -c
                -     /home/omniture/install/etc/salt
            killed:
                10070
            restart:
                ----------
                stderr:
                stdout:
            retcode:
                0
        minion2:
            ----------
            comment:
                - Using configuration minion_restart_command:
                -     /home/omniture/install/bin/salt-minion
                -     --not-an-option
                -     -d
                -     -c
                -     /home/omniture/install/etc/salt
                - Restart failed
            killed:
                10896
            restart:
                ----------
                stderr:
                    Usage: salt-minion

                    salt-minion: error: no such option: --not-an-option
                stdout:
            retcode:
                64

    The result of the command shows the process ID of ``minion1`` that is
    shutdown (killed) and the results of the restart.  If there is a failure
    in the restart it will be reflected in a non-zero ``retcode`` and possibly
    output in the ``stderr`` and/or ``stdout`` values along with addition
    information in the ``comment`` field as is demonstrated with ``minion2``.
    """

    should_kill = True
    should_restart = True
    comment = []
    ret = {
        "killed": None,
        "restart": {},
        "service_restart": {},
        "retcode": 0,
    }

    restart_cmd = __salt__["config.get"]("minion_restart_command")
    if restart_cmd:
        comment.append("Using configuration minion_restart_command:")
        comment.extend([f"    {arg}" for arg in restart_cmd])
    elif systemd and _is_systemd_system():
        # If we are using systemd then we will restart the minion using
        # service.restart (systemd_service.restart)
        comment.append("Using systemctl to restart salt-minion")
        should_kill = False
        should_restart = False
    elif win_service and _is_windows_system():
        # If we are on Windows then we will restart the minion using
        # service.restart (win_service.restart)
        comment.append("Using windows service manager to restart salt-minion")
        should_kill = False
        should_restart = False
    else:
        if "-d" in sys.argv:
            restart_cmd = sys.argv
            comment.append("Restart using process argv:")
            comment.extend([f"    {arg}" for arg in restart_cmd])
        else:
            should_restart = False
            comment.append(
                "Not running in daemon mode - will not restart process after killing"
            )

    if should_kill:
        ret.update(kill())
        if "comment" in ret and ret["comment"]:
            if isinstance(ret["comment"], str):
                comment.append(ret["comment"])
            else:
                comment.extend(ret["comment"])
        if ret["retcode"]:
            comment.append("Kill failed - not restarting")
            should_restart = False

    if should_restart:
        ret["restart"] = __salt__["cmd.run_all"](restart_cmd, env=os.environ)
        # Do not want to mislead users to think that the returned PID from
        # cmd.run_all() is the PID of the new salt minion - just delete the
        # returned PID.
        if "pid" in ret["restart"]:
            del ret["restart"]["pid"]
        if ret["restart"].get("retcode", None):
            comment.append("Restart failed")
            ret["retcode"] = ret["restart"]["retcode"]
        if "retcode" in ret["restart"]:
            # Just want a single retcode
            del ret["restart"]["retcode"]

    if not restart_cmd:
        if systemd and _is_systemd_system():
            service_result = _restart_service(
                "salt-minion", schedule_retry, retry_delay, comment, no_block=True
            )
            ret.update(service_result)
        elif win_service and _is_windows_system():
            service_result = _restart_service(
                "salt-minion", schedule_retry, retry_delay, comment
            )
            ret.update(service_result)

    if comment:
        ret["comment"] = comment

    return ret
