"""
Module for managing the Salt schedule on a minion

Requires that python-dateutil is installed on the minion.

.. versionadded:: 2014.7.0

"""

import copy as pycopy
import datetime
import logging
import os
from collections import OrderedDict

import yaml

import salt.utils.event
import salt.utils.files
import salt.utils.yaml

try:
    import dateutil.parser as dateutil_parser

    _WHEN_SUPPORTED = True
    _RANGE_SUPPORTED = True
except ImportError:
    _WHEN_SUPPORTED = False
    _RANGE_SUPPORTED = False

__proxyenabled__ = ["*"]

log = logging.getLogger(__name__)

__func_alias__ = {"list_": "list", "reload_": "reload"}

SCHEDULE_CONF = [
    "name",
    "maxrunning",
    "function",
    "splay",
    "range",
    "when",
    "once",
    "once_fmt",
    "returner",
    "jid_include",
    "args",
    "kwargs",
    "_seconds",
    "seconds",
    "minutes",
    "hours",
    "days",
    "enabled",
    "return_job",
    "metadata",
    "cron",
    "until",
    "after",
    "return_config",
    "return_kwargs",
    "run_on_start",
    "skip_during_range",
    "run_after_skip_range",
]


def _get_schedule_config_file():
    """
    Return the minion schedule configuration file
    """
    config_dir = __opts__.get("conf_dir", None)
    if config_dir is None and "conf_file" in __opts__:
        config_dir = os.path.dirname(__opts__["conf_file"])
    if config_dir is None:
        config_dir = salt.syspaths.CONFIG_DIR

    minion_d_dir = os.path.join(
        config_dir,
        os.path.dirname(
            __opts__.get(
                "default_include",
                salt.config.DEFAULT_MINION_OPTS["default_include"],
            )
        ),
    )

    if not os.path.isdir(config_dir):
        os.makedirs(config_dir)

    if not os.path.isdir(minion_d_dir):
        os.makedirs(minion_d_dir)

    return os.path.join(minion_d_dir, "_schedule.conf")


def list_(
    show_all=False, show_disabled=True, where=None, return_yaml=True, offline=False
):
    """
    List the jobs currently scheduled on the minion

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.list

        # Show all jobs including hidden internal jobs
        salt '*' schedule.list show_all=True

        # Hide disabled jobs from list of jobs
        salt '*' schedule.list show_disabled=False

    """

    def _get_saved():
        schedule = {}
        schedule_config = _get_schedule_config_file()
        if os.path.exists(schedule_config):
            with salt.utils.files.fopen(schedule_config) as fp_:
                schedule_yaml = fp_.read()
                if schedule_yaml:
                    schedule_contents = yaml.safe_load(schedule_yaml)
                    schedule = schedule_contents.get("schedule", {})
        return schedule

    schedule = {}
    if offline:
        schedule = _get_saved()
        saved_schedule = pycopy.deepcopy(schedule)
    else:
        try:
            with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
                res = __salt__["event.fire"](
                    {"func": "list", "where": where}, "manage_schedule"
                )
                if res:
                    event_ret = event_bus.get_event(
                        tag="/salt/minion/minion_schedule_list_complete", wait=30
                    )
                    if event_ret and event_ret["complete"]:
                        schedule = event_ret["schedule"]
        except KeyError:
            # Effectively a no-op, since we can't really return without an event system
            ret = {}
            ret["comment"] = "Event module not available. Schedule list failed."
            ret["result"] = True
            log.debug("Event module not available. Schedule list failed.")
            return ret

        saved_schedule = _get_saved()

    _hidden = ["enabled", "skip_function", "skip_during_range"]
    for job in list(schedule.keys()):  # iterate over a copy since we will mutate it
        if job in _hidden:
            continue

        # Default jobs added by salt begin with __
        # by default hide them unless show_all is True.
        if job.startswith("__") and not show_all:
            del schedule[job]
            continue

        # if enabled is not included in the job,
        # assume job is enabled.
        if "enabled" not in schedule[job]:
            schedule[job]["enabled"] = True

        for item in pycopy.copy(schedule[job]):
            if item not in SCHEDULE_CONF:
                del schedule[job][item]
                continue
            if schedule[job][item] is None:
                del schedule[job][item]
                continue
            if schedule[job][item] == "true":
                schedule[job][item] = True
            if schedule[job][item] == "false":
                schedule[job][item] = False

        # if the job is disabled and show_disabled is False, skip job
        if not show_disabled and not schedule[job]["enabled"]:
            del schedule[job]
            continue

        if "_seconds" in schedule[job]:
            # remove _seconds from the listing
            del schedule[job]["_seconds"]

    if return_yaml:
        # Indicate whether the scheduled job is saved
        # to the minion configuration.
        for item in schedule:
            if isinstance(schedule[item], dict):
                if item in saved_schedule:
                    schedule[item]["saved"] = True
                else:
                    schedule[item]["saved"] = False
        tmp = {"schedule": schedule}
        return salt.utils.yaml.safe_dump(tmp, default_flow_style=False)
    else:
        return schedule


def is_enabled(name=None):
    """
    List a Job only if its enabled

    If job is not specified, indicate
    if the scheduler is enabled or disabled.

    .. versionadded:: 2015.5.3

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.is_enabled name=job_name
        salt '*' schedule.is_enabled
    """

    current_schedule = __salt__["schedule.list"](show_all=False, return_yaml=False)
    if not name:
        return current_schedule.get("enabled", True)
    else:
        if name in current_schedule:
            return current_schedule[name]
        else:
            return {}


def purge(**kwargs):
    """
    Purge all the jobs currently scheduled on the minion

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.purge

        # Purge jobs on Salt minion
        salt '*' schedule.purge

    """

    ret = {"comment": [], "changes": {}, "result": True}

    current_schedule = list_(
        show_all=True, return_yaml=False, offline=kwargs.get("offline")
    )
    for name in pycopy.deepcopy(current_schedule):
        if name == "enabled":
            continue
        if name.startswith("__"):
            continue

        if "test" in kwargs and kwargs["test"]:
            ret["result"] = True
            ret["comment"].append(f"Job: {name} would be deleted from schedule.")
        else:
            if kwargs.get("offline"):
                del current_schedule[name]

                ret["comment"].append(f"Deleted job: {name} from schedule.")
                ret["changes"][name] = "removed"

            else:
                persist = kwargs.get("persist", True)
                try:
                    with salt.utils.event.get_event(
                        "minion", opts=__opts__
                    ) as event_bus:
                        res = __salt__["event.fire"](
                            {"name": name, "func": "delete", "persist": persist},
                            "manage_schedule",
                        )
                        if res:
                            event_ret = event_bus.get_event(
                                tag="/salt/minion/minion_schedule_delete_complete",
                                wait=30,
                            )
                            if event_ret and event_ret["complete"]:
                                _schedule_ret = event_ret["schedule"]
                                if name not in _schedule_ret:
                                    ret["result"] = True
                                    ret["changes"][name] = "removed"
                                    ret["comment"].append(
                                        f"Deleted job: {name} from schedule."
                                    )
                                else:
                                    ret["comment"].append(
                                        "Failed to delete job {} from schedule.".format(
                                            name
                                        )
                                    )
                                    ret["result"] = True

                except KeyError:
                    # Effectively a no-op, since we can't really return without an event system
                    ret["comment"] = "Event module not available. Schedule add failed."
                    ret["result"] = True

    # wait until the end to write file in offline mode
    if kwargs.get("offline"):
        schedule_conf = _get_schedule_config_file()

        try:
            with salt.utils.files.fopen(schedule_conf, "wb+") as fp_:
                fp_.write(
                    salt.utils.stringutils.to_bytes(
                        salt.utils.yaml.safe_dump({"schedule": current_schedule})
                    )
                )
        except OSError:
            log.error(
                "Failed to persist the updated schedule",
                exc_info_on_loglevel=logging.DEBUG,
            )

    return ret


def delete(name, **kwargs):
    """
    Delete a job from the minion's schedule

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.delete job1

        # Delete job on Salt minion when the Salt minion is not running
        salt '*' schedule.delete job1

    """

    ret = {
        "comment": f"Failed to delete job {name} from schedule.",
        "result": False,
        "changes": {},
    }

    if not name:
        ret["comment"] = "Job name is required."

    if "test" in kwargs and kwargs["test"]:
        ret["comment"] = f"Job: {name} would be deleted from schedule."
        ret["result"] = True
    else:
        if kwargs.get("offline"):
            current_schedule = list_(
                show_all=True,
                where="opts",
                return_yaml=False,
                offline=kwargs.get("offline"),
            )

            del current_schedule[name]

            schedule_conf = _get_schedule_config_file()

            try:
                with salt.utils.files.fopen(schedule_conf, "wb+") as fp_:
                    fp_.write(
                        salt.utils.stringutils.to_bytes(
                            salt.utils.yaml.safe_dump({"schedule": current_schedule})
                        )
                    )
            except OSError:
                log.error(
                    "Failed to persist the updated schedule",
                    exc_info_on_loglevel=logging.DEBUG,
                )

            ret["result"] = True
            ret["comment"] = f"Deleted Job {name} from schedule."
            ret["changes"][name] = "removed"
        else:
            persist = kwargs.get("persist", True)

            if name in list_(
                show_all=True,
                where="opts",
                return_yaml=False,
                offline=kwargs.get("offline"),
            ):
                event_data = {"name": name, "func": "delete", "persist": persist}
            elif name in list_(
                show_all=True,
                where="pillar",
                return_yaml=False,
                offline=kwargs.get("offline"),
            ):
                event_data = {
                    "name": name,
                    "where": "pillar",
                    "func": "delete",
                    "persist": False,
                }
            else:
                ret["comment"] = f"Job {name} does not exist."
                return ret

            try:
                with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
                    res = __salt__["event.fire"](event_data, "manage_schedule")
                    if res:
                        event_ret = event_bus.get_event(
                            tag="/salt/minion/minion_schedule_delete_complete",
                            wait=30,
                        )
                        if event_ret and event_ret["complete"]:
                            schedule = event_ret["schedule"]
                            if name not in schedule:
                                ret["result"] = True
                                ret["comment"] = "Deleted Job {} from schedule.".format(
                                    name
                                )
                                ret["changes"][name] = "removed"
                            else:
                                ret["comment"] = (
                                    "Failed to delete job {} from schedule.".format(
                                        name
                                    )
                                )
                            return ret
            except KeyError:
                # Effectively a no-op, since we can't really return without an event system
                ret["comment"] = "Event module not available. Schedule add failed."
    return ret


def build_schedule_item(name, **kwargs):
    """
    Build a schedule job

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.build_schedule_item job1 function='test.ping' seconds=3600
    """

    ret = {"comment": [], "result": True}

    if not name:
        ret["comment"] = "Job name is required."
        ret["result"] = False
        return ret

    schedule = {}
    schedule[name] = OrderedDict()
    schedule[name]["function"] = kwargs["function"]

    time_conflict = False
    for item in ["seconds", "minutes", "hours", "days"]:
        if item in kwargs and "when" in kwargs:
            time_conflict = True

        if item in kwargs and "cron" in kwargs:
            time_conflict = True

    if time_conflict:
        ret["result"] = False
        ret["comment"] = (
            'Unable to use "seconds", "minutes", "hours", or "days" with "when" or'
            ' "cron" options.'
        )
        return ret

    if "when" in kwargs and "cron" in kwargs:
        ret["result"] = False
        ret["comment"] = 'Unable to use "when" and "cron" options together.  Ignoring.'
        return ret

    for item in ["seconds", "minutes", "hours", "days"]:
        if item in kwargs:
            schedule[name][item] = kwargs[item]

    if "return_job" in kwargs:
        schedule[name]["return_job"] = kwargs["return_job"]

    if "metadata" in kwargs:
        schedule[name]["metadata"] = kwargs["metadata"]

    if "job_args" in kwargs:
        if isinstance(kwargs["job_args"], list):
            schedule[name]["args"] = kwargs["job_args"]
        else:
            ret["result"] = False
            ret["comment"] = "job_args is not a list. please correct and try again."
            return ret

    if "job_kwargs" in kwargs:
        if isinstance(kwargs["job_kwargs"], dict):
            schedule[name]["kwargs"] = kwargs["job_kwargs"]
        else:
            ret["result"] = False
            ret["comment"] = "job_kwargs is not a dict. please correct and try again."
            return ret

    if "maxrunning" in kwargs:
        schedule[name]["maxrunning"] = kwargs["maxrunning"]
    else:
        schedule[name]["maxrunning"] = 1

    if "name" in kwargs:
        schedule[name]["name"] = kwargs["name"]
    else:
        schedule[name]["name"] = name

    if "enabled" in kwargs:
        schedule[name]["enabled"] = kwargs["enabled"]
    else:
        schedule[name]["enabled"] = True

    schedule[name]["jid_include"] = kwargs.get("jid_include", True)

    if "splay" in kwargs:
        if isinstance(kwargs["splay"], dict):
            # Ensure ordering of start and end arguments
            schedule[name]["splay"] = OrderedDict()
            schedule[name]["splay"]["start"] = kwargs["splay"]["start"]
            schedule[name]["splay"]["end"] = kwargs["splay"]["end"]
        else:
            schedule[name]["splay"] = kwargs["splay"]

    if "when" in kwargs:
        if not _WHEN_SUPPORTED:
            ret["result"] = False
            ret["comment"] = 'Missing dateutil.parser, "when" is unavailable.'
            return ret
        else:
            validate_when = kwargs["when"]
            if not isinstance(validate_when, list):
                validate_when = [validate_when]
            for _when in validate_when:
                try:
                    dateutil_parser.parse(_when)
                except ValueError:
                    ret["result"] = False
                    ret["comment"] = 'Schedule item {} for "when" in invalid.'.format(
                        _when
                    )
                    return ret

    for item in [
        "range",
        "when",
        "once",
        "once_fmt",
        "cron",
        "returner",
        "after",
        "return_config",
        "return_kwargs",
        "until",
        "run_on_start",
        "skip_during_range",
    ]:
        if item in kwargs:
            schedule[name][item] = kwargs[item]

    return schedule[name]


def add(name, **kwargs):
    """
    Add a job to the schedule

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.add job1 function='test.ping' seconds=3600
        # If function have some arguments, use job_args
        salt '*' schedule.add job2 function='cmd.run' job_args="['date >> /tmp/date.log']" seconds=60

        # Add job to Salt minion when the Salt minion is not running
        salt '*' schedule.add job1 function='test.ping' seconds=3600 offline=True

    """

    ret = {
        "comment": f"Failed to add job {name} to schedule.",
        "result": False,
        "changes": {},
    }
    current_schedule = list_(
        show_all=True, return_yaml=False, offline=kwargs.get("offline")
    )

    if name in current_schedule:
        ret["comment"] = f"Job {name} already exists in schedule."
        ret["result"] = False
        return ret

    if not name:
        ret["comment"] = "Job name is required."
        ret["result"] = False

    time_conflict = False
    for item in ["seconds", "minutes", "hours", "days"]:
        if item in kwargs and "when" in kwargs:
            time_conflict = True
        if item in kwargs and "cron" in kwargs:
            time_conflict = True

    if time_conflict:
        ret["comment"] = (
            'Error: Unable to use "seconds", "minutes", "hours", or "days" with "when"'
            ' or "cron" options.'
        )
        return ret

    if "when" in kwargs and "cron" in kwargs:
        ret["comment"] = 'Unable to use "when" and "cron" options together.  Ignoring.'
        return ret

    persist = kwargs.get("persist", True)

    _new = build_schedule_item(name, **kwargs)
    if "result" in _new and not _new["result"]:
        return _new

    schedule_data = {}
    schedule_data[name] = _new

    if "test" in kwargs and kwargs["test"]:
        ret["comment"] = f"Job: {name} would be added to schedule."
        ret["result"] = True
    else:
        if kwargs.get("offline"):
            current_schedule.update(schedule_data)

            schedule_conf = _get_schedule_config_file()

            try:
                with salt.utils.files.fopen(schedule_conf, "wb+") as fp_:
                    fp_.write(
                        salt.utils.stringutils.to_bytes(
                            salt.utils.yaml.safe_dump({"schedule": current_schedule})
                        )
                    )
            except OSError:
                log.error(
                    "Failed to persist the updated schedule",
                    exc_info_on_loglevel=logging.DEBUG,
                )

            ret["result"] = True
            ret["comment"] = f"Added job: {name} to schedule."
            ret["changes"][name] = "added"
        else:
            try:
                with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
                    res = __salt__["event.fire"](
                        {
                            "name": name,
                            "schedule": schedule_data,
                            "func": "add",
                            "persist": persist,
                        },
                        "manage_schedule",
                    )
                    if res:
                        event_ret = event_bus.get_event(
                            tag="/salt/minion/minion_schedule_add_complete",
                            wait=30,
                        )
                        if event_ret and event_ret["complete"]:
                            schedule = event_ret["schedule"]
                            if name in schedule:
                                ret["result"] = True
                                ret["comment"] = "Added job: {} to schedule.".format(
                                    name
                                )
                                ret["changes"][name] = "added"
                                return ret
            except KeyError:
                # Effectively a no-op, since we can't really return without an event system
                ret["comment"] = "Event module not available. Schedule add failed."
    return ret


def modify(name, **kwargs):
    """
    Modify an existing job in the schedule

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.modify job1 function='test.ping' seconds=3600

        # Modify job on Salt minion when the Salt minion is not running
        salt '*' schedule.modify job1 function='test.ping' seconds=3600 offline=True

    """

    ret = {"comment": "", "changes": {}, "result": True}

    time_conflict = False
    for item in ["seconds", "minutes", "hours", "days"]:
        if item in kwargs and "when" in kwargs:
            time_conflict = True

        if item in kwargs and "cron" in kwargs:
            time_conflict = True

    if time_conflict:
        ret["result"] = False
        ret["comment"] = (
            'Error: Unable to use "seconds", "minutes", "hours", or "days" with "when"'
            " option."
        )
        return ret

    if "when" in kwargs and "cron" in kwargs:
        ret["result"] = False
        ret["comment"] = 'Unable to use "when" and "cron" options together.  Ignoring.'
        return ret

    current_schedule = list_(
        show_all=True, return_yaml=False, offline=kwargs.get("offline")
    )

    if name not in current_schedule:
        ret["comment"] = f"Job {name} does not exist in schedule."
        ret["result"] = False
        return ret

    _current = current_schedule[name]

    if "function" not in kwargs:
        kwargs["function"] = _current.get("function")

    # Remove the auto generated _seconds value
    if "_seconds" in _current:
        _current["seconds"] = _current.pop("_seconds")

    # Copy _current _new, then update values from kwargs
    _new = build_schedule_item(name, **kwargs)

    # Remove test from kwargs, it's not a valid schedule option
    _new.pop("test", None)

    if "result" in _new and not _new["result"]:
        return _new

    if _new == _current:
        ret["comment"] = f"Job {name} in correct state"
        return ret

    ret["changes"][name] = {
        "old": OrderedDict(_current),
        "new": OrderedDict(_new),
    }

    if "test" in kwargs and kwargs["test"]:
        ret["comment"] = f"Job: {name} would be modified in schedule."
    else:
        if kwargs.get("offline"):
            current_schedule[name].update(_new)

            schedule_conf = _get_schedule_config_file()

            try:
                with salt.utils.files.fopen(schedule_conf, "wb+") as fp_:
                    fp_.write(
                        salt.utils.stringutils.to_bytes(
                            salt.utils.yaml.safe_dump({"schedule": current_schedule})
                        )
                    )
            except OSError:
                log.error(
                    "Failed to persist the updated schedule",
                    exc_info_on_loglevel=logging.DEBUG,
                )

            ret["result"] = True
            ret["comment"] = f"Modified job: {name} in schedule."

        else:
            persist = kwargs.get("persist", True)
            if name in list_(show_all=True, where="opts", return_yaml=False):
                event_data = {
                    "name": name,
                    "schedule": _new,
                    "func": "modify",
                    "persist": persist,
                }
            elif name in list_(show_all=True, where="pillar", return_yaml=False):
                event_data = {
                    "name": name,
                    "schedule": _new,
                    "where": "pillar",
                    "func": "modify",
                    "persist": False,
                }

            out = __salt__["event.fire"](event_data, "manage_schedule")
            if out:
                ret["comment"] = f"Modified job: {name} in schedule."
            else:
                ret["comment"] = f"Failed to modify job {name} in schedule."
                ret["result"] = False
    return ret


def run_job(name, force=False):
    """
    Run a scheduled job on the minion immediately

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.run_job job1

        salt '*' schedule.run_job job1 force=True
        Force the job to run even if it is disabled.
    """

    ret = {"comment": [], "result": True}

    if not name:
        ret["comment"] = "Job name is required."
        ret["result"] = False

    schedule = list_(show_all=True, return_yaml=False)
    if name in schedule:
        data = schedule[name]
        if "enabled" in data and not data["enabled"] and not force:
            ret["comment"] = f"Job {name} is disabled."
        else:
            out = __salt__["event.fire"](
                {"name": name, "func": "run_job"}, "manage_schedule"
            )
            if out:
                ret["comment"] = f"Scheduling Job {name} on minion."
            else:
                ret["comment"] = f"Failed to run job {name} on minion."
                ret["result"] = False
    else:
        ret["comment"] = f"Job {name} does not exist."
        ret["result"] = False
    return ret


def enable_job(name, **kwargs):
    """
    Enable a job in the minion's schedule

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.enable_job job1
    """

    ret = {"comment": [], "result": True, "changes": {}}

    if not name:
        ret["comment"] = "Job name is required."
        ret["result"] = False

    if "test" in __opts__ and __opts__["test"]:
        ret["comment"] = f"Job: {name} would be enabled in schedule."
    else:
        persist = kwargs.get("persist", True)

        if name in list_(show_all=True, where="opts", return_yaml=False):
            event_data = {"name": name, "func": "enable_job", "persist": persist}
        elif name in list_(show_all=True, where="pillar", return_yaml=False):
            event_data = {
                "name": name,
                "where": "pillar",
                "func": "enable_job",
                "persist": False,
            }
        else:
            ret["comment"] = f"Job {name} does not exist."
            ret["result"] = False
            return ret

        try:
            with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
                res = __salt__["event.fire"](event_data, "manage_schedule")
                if res:
                    event_ret = event_bus.get_event(
                        tag="/salt/minion/minion_schedule_enabled_job_complete",
                        wait=30,
                    )
                    if event_ret and event_ret["complete"]:
                        schedule = event_ret["schedule"]
                        # check item exists in schedule and is enabled
                        if name in schedule and schedule[name]["enabled"]:
                            ret["result"] = True
                            ret["comment"] = f"Enabled Job {name} in schedule."
                            ret["changes"][name] = "enabled"
                        else:
                            ret["result"] = False
                            ret["comment"] = f"Failed to enable job {name} in schedule."
                        return ret
        except KeyError:
            # Effectively a no-op, since we can't really return without an event system
            ret["comment"] = "Event module not available. Schedule enable job failed."
    return ret


def disable_job(name, **kwargs):
    """
    Disable a job in the minion's schedule

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.disable_job job1
    """

    ret = {"comment": [], "result": True, "changes": {}}

    if not name:
        ret["comment"] = "Job name is required."
        ret["result"] = False

    if "test" in kwargs and kwargs["test"]:
        ret["comment"] = f"Job: {name} would be disabled in schedule."
    else:
        persist = kwargs.get("persist", True)

        if name in list_(show_all=True, where="opts", return_yaml=False):
            event_data = {"name": name, "func": "disable_job", "persist": persist}
        elif name in list_(show_all=True, where="pillar"):
            event_data = {
                "name": name,
                "where": "pillar",
                "func": "disable_job",
                "persist": False,
            }
        else:
            ret["comment"] = f"Job {name} does not exist."
            ret["result"] = False
            return ret

        try:
            with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
                res = __salt__["event.fire"](event_data, "manage_schedule")
                if res:
                    event_ret = event_bus.get_event(
                        tag="/salt/minion/minion_schedule_disabled_job_complete",
                        wait=30,
                    )
                    if event_ret and event_ret["complete"]:
                        schedule = event_ret["schedule"]
                        # check item exists in schedule and is enabled
                        if name in schedule and not schedule[name]["enabled"]:
                            ret["result"] = True
                            ret["comment"] = f"Disabled Job {name} in schedule."
                            ret["changes"][name] = "disabled"
                        else:
                            ret["result"] = False
                            ret["comment"] = (
                                f"Failed to disable job {name} in schedule."
                            )
                        return ret
        except KeyError:
            # Effectively a no-op, since we can't really return without an event system
            ret["comment"] = "Event module not available. Schedule enable job failed."
    return ret


def save(**kwargs):
    """
    Save all scheduled jobs on the minion

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.save
    """

    ret = {"comment": [], "result": True}

    if "test" in kwargs and kwargs["test"]:
        ret["comment"] = "Schedule would be saved."
    else:
        try:
            with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
                res = __salt__["event.fire"](
                    {"func": "save_schedule"}, "manage_schedule"
                )
                if res:
                    event_ret = event_bus.get_event(
                        tag="/salt/minion/minion_schedule_saved",
                        wait=30,
                    )
                    if event_ret and event_ret["complete"]:
                        ret["result"] = True
                        ret["comment"] = "Schedule (non-pillar items) saved."
                    else:
                        ret["result"] = False
                        ret["comment"] = "Failed to save schedule."
        except KeyError:
            # Effectively a no-op, since we can't really return without an event system
            ret["comment"] = "Event module not available. Schedule save failed."
    return ret


def enable(**kwargs):
    """
    Enable all scheduled jobs on the minion

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.enable
    """

    ret = {"comment": [], "changes": {}, "result": True}

    if "test" in kwargs and kwargs["test"]:
        ret["comment"] = "Schedule would be enabled."
    else:
        persist = kwargs.get("persist", True)

        try:
            with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
                res = __salt__["event.fire"](
                    {"func": "enable", "persist": persist}, "manage_schedule"
                )
                if res:
                    event_ret = event_bus.get_event(
                        tag="/salt/minion/minion_schedule_enabled_complete",
                        wait=30,
                    )
                    if event_ret and event_ret["complete"]:
                        schedule = event_ret["schedule"]
                        if "enabled" in schedule and schedule["enabled"]:
                            ret["result"] = True
                            ret["comment"] = "Enabled schedule on minion."
                            ret["changes"]["schedule"] = "enabled"
                        else:
                            ret["result"] = False
                            ret["comment"] = "Failed to enable schedule on minion."
                        return ret
        except KeyError:
            # Effectively a no-op, since we can't really return without an event system
            ret["comment"] = "Event module not available. Schedule enable job failed."
    return ret


def disable(**kwargs):
    """
    Disable all scheduled jobs on the minion

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.disable
    """

    ret = {"comment": [], "changes": {}, "result": True}

    if "test" in kwargs and kwargs["test"]:
        ret["comment"] = "Schedule would be disabled."
    else:
        persist = kwargs.get("persist", True)

        try:
            with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
                res = __salt__["event.fire"](
                    {"func": "disable", "persist": persist}, "manage_schedule"
                )
                if res:
                    event_ret = event_bus.get_event(
                        tag="/salt/minion/minion_schedule_disabled_complete",
                        wait=30,
                    )
                    if event_ret and event_ret["complete"]:
                        schedule = event_ret["schedule"]
                        if "enabled" in schedule and not schedule["enabled"]:
                            ret["result"] = True
                            ret["comment"] = "Disabled schedule on minion."
                            ret["changes"]["schedule"] = "disabled"
                        else:
                            ret["result"] = False
                            ret["comment"] = "Failed to disable schedule on minion."
                        return ret
        except KeyError:
            # Effectively a no-op, since we can't really return without an event system
            ret["comment"] = "Event module not available. Schedule disable job failed."
    return ret


def reload_():
    """
    Reload saved scheduled jobs on the minion

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.reload
    """

    ret = {"comment": [], "result": True}

    # If there a schedule defined in pillar, refresh it.
    if "schedule" in __pillar__:
        out = __salt__["event.fire"]({}, "pillar_refresh")
        if out:
            ret["comment"].append("Reloaded schedule from pillar on minion.")
        else:
            ret["comment"].append("Failed to reload schedule from pillar on minion.")
            ret["result"] = False

    # move this file into an configurable opt
    sfn = "{}/{}/schedule.conf".format(
        __opts__["config_dir"], os.path.dirname(__opts__["default_include"])
    )
    if os.path.isfile(sfn):
        with salt.utils.files.fopen(sfn, "rb") as fp_:
            try:
                schedule = salt.utils.yaml.safe_load(fp_)
            except salt.utils.yaml.YAMLError as exc:
                ret["comment"].append(f"Unable to read existing schedule file: {exc}")

        if schedule:
            if "schedule" in schedule and schedule["schedule"]:
                out = __salt__["event.fire"](
                    {"func": "reload", "schedule": schedule}, "manage_schedule"
                )
                if out:
                    ret["comment"].append(
                        "Reloaded schedule on minion from schedule.conf."
                    )
                else:
                    ret["comment"].append(
                        "Failed to reload schedule on minion from schedule.conf."
                    )
                    ret["result"] = False
            else:
                ret["comment"].append(
                    "Failed to reload schedule on minion.  Saved file is empty or"
                    " invalid."
                )
                ret["result"] = False
        else:
            ret["comment"].append(
                "Failed to reload schedule on minion.  Saved file is empty or invalid."
            )
            ret["result"] = False
    return ret


def move(name, target, **kwargs):
    """
    Move scheduled job to another minion or minions.

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.move jobname target
    """

    ret = {"comment": [], "result": True}

    if not name:
        ret["comment"] = "Job name is required."
        ret["result"] = False

    if "test" in kwargs and kwargs["test"]:
        ret["comment"] = f"Job: {name} would be moved from schedule."
    else:
        opts_schedule = list_(show_all=True, where="opts", return_yaml=False)
        pillar_schedule = list_(show_all=True, where="pillar", return_yaml=False)

        if name in opts_schedule:
            schedule_data = opts_schedule[name]
            where = None
        elif name in pillar_schedule:
            schedule_data = pillar_schedule[name]
            where = "pillar"
        else:
            ret["comment"] = f"Job {name} does not exist."
            ret["result"] = False
            return ret

        schedule_opts = []
        for key, value in schedule_data.items():
            temp = f"{key}={value}"
            schedule_opts.append(temp)
        response = __salt__["publish.publish"](target, "schedule.add", schedule_opts)

        # Get errors and list of affeced minions
        errors = []
        minions = []
        for minion in response:
            minions.append(minion)
            if not response[minion]:
                errors.append(minion)

        # parse response
        if not response:
            ret["comment"] = "no servers answered the published schedule.add command"
            return ret
        elif errors:
            ret["comment"] = "the following minions return False"
            ret["minions"] = errors
            return ret
        else:
            delete(name, where=where)
            ret["result"] = True
            ret["comment"] = f"Moved Job {name} from schedule."
            ret["minions"] = minions
            return ret
    return ret


def copy(name, target, **kwargs):
    """
    Copy scheduled job to another minion or minions.

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.copy jobname target
    """

    ret = {"comment": [], "result": True}

    if not name:
        ret["comment"] = "Job name is required."
        ret["result"] = False

    if "test" in kwargs and kwargs["test"]:
        ret["comment"] = f"Job: {name} would be copied from schedule."
    else:
        opts_schedule = list_(show_all=True, where="opts", return_yaml=False)
        pillar_schedule = list_(show_all=True, where="pillar", return_yaml=False)

        if name in opts_schedule:
            schedule_data = opts_schedule[name]
        elif name in pillar_schedule:
            schedule_data = pillar_schedule[name]
        else:
            ret["comment"] = f"Job {name} does not exist."
            ret["result"] = False
            return ret

        schedule_opts = []
        for key, value in schedule_data.items():
            temp = f"{key}={value}"
            schedule_opts.append(temp)
        response = __salt__["publish.publish"](target, "schedule.add", schedule_opts)

        # Get errors and list of affeced minions
        errors = []
        minions = []
        for minion in response:
            minions.append(minion)
            if not response[minion]:
                errors.append(minion)

        # parse response
        if not response:
            ret["comment"] = "no servers answered the published schedule.add command"
            return ret
        elif errors:
            ret["comment"] = "the following minions return False"
            ret["minions"] = errors
            return ret
        else:
            ret["result"] = True
            ret["comment"] = f"Copied Job {name} from schedule to minion(s)."
            ret["minions"] = minions
            return ret
    return ret


def postpone_job(name, current_time, new_time, **kwargs):
    """
    Postpone a job in the minion's schedule

    Current time and new time should be in date string format,
    default value is %Y-%m-%dT%H:%M:%S.

    .. versionadded:: 2018.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.postpone_job job current_time new_time

        salt '*' schedule.postpone_job job current_time new_time time_fmt='%Y-%m-%dT%H:%M:%S'
    """

    time_fmt = kwargs.get("time_fmt") or "%Y-%m-%dT%H:%M:%S"
    ret = {"comment": [], "result": True}

    if not name:
        ret["comment"] = "Job name is required."
        ret["result"] = False
        return ret

    if not current_time:
        ret["comment"] = "Job current time is required."
        ret["result"] = False
        return ret
    else:
        try:
            # Validate date string
            datetime.datetime.strptime(current_time, time_fmt)
        except (TypeError, ValueError):
            log.error("Date string could not be parsed: %s, %s", new_time, time_fmt)

            ret["comment"] = "Date string could not be parsed."
            ret["result"] = False
            return ret

    if not new_time:
        ret["comment"] = "Job new_time is required."
        ret["result"] = False
        return ret
    else:
        try:
            # Validate date string
            datetime.datetime.strptime(new_time, time_fmt)
        except (TypeError, ValueError):
            log.error("Date string could not be parsed: %s, %s", new_time, time_fmt)

            ret["comment"] = "Date string could not be parsed."
            ret["result"] = False
            return ret

    if "test" in __opts__ and __opts__["test"]:
        ret["comment"] = f"Job: {name} would be postponed in schedule."
    else:

        if name in list_(show_all=True, where="opts", return_yaml=False):
            event_data = {
                "name": name,
                "time": current_time,
                "new_time": new_time,
                "time_fmt": time_fmt,
                "func": "postpone_job",
            }
        elif name in list_(show_all=True, where="pillar", return_yaml=False):
            event_data = {
                "name": name,
                "time": current_time,
                "new_time": new_time,
                "time_fmt": time_fmt,
                "where": "pillar",
                "func": "postpone_job",
            }
        else:
            ret["comment"] = f"Job {name} does not exist."
            ret["result"] = False
            return ret

        try:
            with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
                res = __salt__["event.fire"](event_data, "manage_schedule")
                if res:
                    event_ret = event_bus.get_event(
                        tag="/salt/minion/minion_schedule_postpone_job_complete",
                        wait=30,
                    )
                    if event_ret and event_ret["complete"]:
                        schedule = event_ret["schedule"]
                        # check item exists in schedule and is enabled
                        if name in schedule and schedule[name]["enabled"]:
                            ret["result"] = True
                            ret["comment"] = "Postponed Job {} in schedule.".format(
                                name
                            )
                        else:
                            ret["result"] = False
                            ret["comment"] = (
                                f"Failed to postpone job {name} in schedule."
                            )
                        return ret
        except KeyError:
            # Effectively a no-op, since we can't really return without an event system
            ret["comment"] = "Event module not available. Schedule postpone job failed."
    return ret


def skip_job(name, current_time, **kwargs):
    """
    Skip a job in the minion's schedule at specified time.

    Time to skip should be specified as date string format,
    default value is %Y-%m-%dT%H:%M:%S.

    .. versionadded:: 2018.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.skip_job job time
    """
    time_fmt = kwargs.get("time_fmt") or "%Y-%m-%dT%H:%M:%S"

    ret = {"comment": [], "result": True}

    if not name:
        ret["comment"] = "Job name is required."
        ret["result"] = False

    if not current_time:
        ret["comment"] = "Job time is required."
        ret["result"] = False
    else:
        # Validate date string
        try:
            datetime.datetime.strptime(current_time, time_fmt)
        except (TypeError, ValueError):
            log.error("Date string could not be parsed: %s, %s", current_time, time_fmt)

            ret["comment"] = "Date string could not be parsed."
            ret["result"] = False
            return ret

    if "test" in __opts__ and __opts__["test"]:
        ret["comment"] = f"Job: {name} would be skipped in schedule."
    else:

        if name in list_(show_all=True, where="opts", return_yaml=False):
            event_data = {
                "name": name,
                "time": current_time,
                "time_fmt": time_fmt,
                "func": "skip_job",
            }
        elif name in list_(show_all=True, where="pillar", return_yaml=False):
            event_data = {
                "name": name,
                "time": current_time,
                "time_fmt": time_fmt,
                "where": "pillar",
                "func": "skip_job",
            }
        else:
            ret["comment"] = f"Job {name} does not exist."
            ret["result"] = False
            return ret

        try:
            with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
                res = __salt__["event.fire"](event_data, "manage_schedule")
                if res:
                    event_ret = event_bus.get_event(
                        tag="/salt/minion/minion_schedule_skip_job_complete",
                        wait=30,
                    )
                    if event_ret and event_ret["complete"]:
                        schedule = event_ret["schedule"]
                        # check item exists in schedule and is enabled
                        if name in schedule and schedule[name]["enabled"]:
                            ret["result"] = True
                            ret["comment"] = "Added Skip Job {} in schedule.".format(
                                name
                            )
                        else:
                            ret["result"] = False
                            ret["comment"] = f"Failed to skip job {name} in schedule."
                        return ret
        except KeyError:
            # Effectively a no-op, since we can't really return without an event system
            ret["comment"] = "Event module not available. Schedule skip job failed."
    return ret


def show_next_fire_time(name, **kwargs):
    """
    Show the next fire time for scheduled job

    .. versionadded:: 2018.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.show_next_fire_time job_name

    """

    ret = {"result": True}

    if not name:
        ret["comment"] = "Job name is required."
        ret["result"] = False

    try:
        event_data = {"name": name, "func": "get_next_fire_time"}
        with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
            res = __salt__["event.fire"](event_data, "manage_schedule")
            if res:
                event_ret = event_bus.get_event(
                    tag="/salt/minion/minion_schedule_next_fire_time_complete",
                    wait=30,
                )
    except KeyError:
        # Effectively a no-op, since we can't really return without an event system
        ret = {}
        ret["comment"] = (
            "Event module not available. Schedule show next fire time failed."
        )
        ret["result"] = True
        return ret

    if "next_fire_time" in event_ret:
        ret["next_fire_time"] = event_ret["next_fire_time"]
    else:
        ret["comment"] = "next fire time not available."
    return ret


def job_status(name, time_fmt="%Y-%m-%dT%H:%M:%S"):
    """
    Show the information for a particular job.

    CLI Example:

    .. code-block:: bash

        salt '*' schedule.job_status job_name

    """

    def convert_datetime_objects_in_dict_to_string(data_dict, time_fmt):
        return {
            key: (
                value.strftime(time_fmt)
                if isinstance(value, datetime.datetime)
                else value
            )
            for key, value in data_dict.items()
        }

    schedule = {}
    try:
        with salt.utils.event.get_event("minion", opts=__opts__) as event_bus:
            res = __salt__["event.fire"](
                {"func": "job_status", "name": name, "fire_event": True},
                "manage_schedule",
            )
            if res:
                event_ret = event_bus.get_event(
                    tag="/salt/minion/minion_schedule_job_status_complete", wait=30
                )
                data = event_ret.get("data", {})
                return convert_datetime_objects_in_dict_to_string(data, time_fmt)
    except KeyError:
        # Effectively a no-op, since we can't really return without an event system
        ret = {}
        ret["comment"] = "Event module not available. Schedule list failed."
        ret["result"] = True
        log.debug("Event module not available. Schedule list failed.")
        return ret
