"""
Wrapper module for at(1)

Also, a 'tag' feature has been added to more
easily tag jobs.

:platform:      linux,openbsd,freebsd

.. versionchanged:: 2017.7.0
"""

import datetime
import re
import time

import salt.utils.data
import salt.utils.path
import salt.utils.platform

# pylint: enable=import-error,redefined-builtin
from salt.exceptions import CommandNotFoundError

# pylint: disable=import-error,redefined-builtin

# OS Families that should work (Ubuntu and Debian are the default)
# TODO: Refactor some of this module to remove the checks for binaries

# Tested on OpenBSD 5.0
BSD = ("OpenBSD", "FreeBSD")

__virtualname__ = "at"


def __virtual__():
    """
    Most everything has the ability to support at(1)
    """
    if salt.utils.platform.is_windows() or salt.utils.platform.is_sunos():
        return (False, "The at module could not be loaded: unsupported platform")
    if salt.utils.path.which("at") is None:
        return (False, "The at module could not be loaded: at command not found")
    return __virtualname__


def _cmd(binary, *args):
    """
    Wrapper to run at(1) or return None.
    """
    binary = salt.utils.path.which(binary)
    if not binary:
        raise CommandNotFoundError(f"{binary}: command not found")
    cmd = [binary] + list(args)
    return __salt__["cmd.run_stdout"]([binary] + list(args), python_shell=False)


def atq(tag=None):
    """
    List all queued and running jobs or only those with
    an optional 'tag'.

    CLI Example:

    .. code-block:: bash

        salt '*' at.atq
        salt '*' at.atq [tag]
        salt '*' at.atq [job number]
    """
    jobs = []

    # Shim to produce output similar to what __virtual__() should do
    # but __salt__ isn't available in __virtual__()
    # Tested on CentOS 5.8
    if __grains__["os_family"] == "RedHat":
        output = _cmd("at", "-l")
    else:
        output = _cmd("atq")

    if output is None:
        return "'at.atq' is not available."

    # No jobs so return
    if output == "":
        return {"jobs": jobs}

    # Jobs created with at.at() will use the following
    # comment to denote a tagged job.
    job_kw_regex = re.compile(r"^### SALT: (\w+)")

    # Split each job into a dictionary and handle
    # pulling out tags or only listing jobs with a certain
    # tag
    for line in output.splitlines():
        job_tag = ""

        # Redhat/CentOS
        if __grains__["os_family"] == "RedHat":
            job, spec = line.split("\t")
            specs = spec.split()
        elif __grains__["os"] == "OpenBSD":
            if line.startswith(" Rank"):
                continue
            else:
                tmp = line.split()
                timestr = " ".join(tmp[1:5])
                job = tmp[6]
                specs = (
                    datetime.datetime(*(time.strptime(timestr, "%b %d, %Y %H:%M")[0:5]))
                    .isoformat()
                    .split("T")
                )
                specs.append(tmp[7])
                specs.append(tmp[5])
        elif __grains__["os"] == "FreeBSD":
            if line.startswith("Date"):
                continue
            else:
                tmp = line.split()
                timestr = " ".join(tmp[1:6])
                job = tmp[8]
                specs = (
                    datetime.datetime(
                        *(time.strptime(timestr, "%b %d %H:%M:%S %Z %Y")[0:5])
                    )
                    .isoformat()
                    .split("T")
                )
                specs.append(tmp[7])
                specs.append(tmp[6])

        else:
            job, spec = line.split("\t")
            tmp = spec.split()
            timestr = " ".join(tmp[0:5])
            specs = (
                datetime.datetime(*(time.strptime(timestr)[0:5])).isoformat().split("T")
            )
            specs.append(tmp[5])
            specs.append(tmp[6])

        # Search for any tags
        atc_out = _cmd("at", "-c", job)
        for line in atc_out.splitlines():
            tmp = job_kw_regex.match(line)
            if tmp:
                job_tag = tmp.groups()[0]

        if __grains__["os"] in BSD:
            job = str(job)
        else:
            job = int(job)

        # If a tag is supplied, only list jobs with that tag
        if tag:
            # TODO: Looks like there is a difference between salt and salt-call
            # If I don't wrap job in an int(), it fails on salt but works on
            # salt-call. With the int(), it fails with salt-call but not salt.
            if tag == job_tag or tag == job:
                jobs.append(
                    {
                        "job": job,
                        "date": specs[0],
                        "time": specs[1],
                        "queue": specs[2],
                        "user": specs[3],
                        "tag": job_tag,
                    }
                )
        else:
            jobs.append(
                {
                    "job": job,
                    "date": specs[0],
                    "time": specs[1],
                    "queue": specs[2],
                    "user": specs[3],
                    "tag": job_tag,
                }
            )

    return {"jobs": jobs}


def atrm(*args):
    """
    Remove jobs from the queue.

    CLI Example:

    .. code-block:: bash

        salt '*' at.atrm <jobid> <jobid> .. <jobid>
        salt '*' at.atrm all
        salt '*' at.atrm all [tag]
    """

    # Need to do this here also since we use atq()
    if not salt.utils.path.which("at"):
        return "'at.atrm' is not available."

    if not args:
        return {"jobs": {"removed": [], "tag": None}}

    # Convert all to strings
    args = salt.utils.data.stringify(args)

    if args[0] == "all":
        if len(args) > 1:
            opts = list(list(map(str, [j["job"] for j in atq(args[1])["jobs"]])))
            ret = {"jobs": {"removed": opts, "tag": args[1]}}
        else:
            opts = list(list(map(str, [j["job"] for j in atq()["jobs"]])))
            ret = {"jobs": {"removed": opts, "tag": None}}
    else:
        opts = list(
            list(
                map(
                    str,
                    [i["job"] for i in atq()["jobs"] if str(i["job"]) in args],
                )
            )
        )
        ret = {"jobs": {"removed": opts, "tag": None}}

    # Shim to produce output similar to what __virtual__() should do
    # but __salt__ isn't available in __virtual__()
    output = _cmd("at", "-d", " ".join(opts))
    if output is None:
        return "'at.atrm' is not available."

    return ret


def at(*args, **kwargs):  # pylint: disable=C0103
    """
    Add a job to the queue.

    The 'timespec' follows the format documented in the
    at(1) manpage.

    CLI Example:

    .. code-block:: bash

        salt '*' at.at <timespec> <cmd> [tag=<tag>] [runas=<user>]
        salt '*' at.at 12:05am '/sbin/reboot' tag=reboot
        salt '*' at.at '3:05am +3 days' 'bin/myscript' tag=nightly runas=jim
        salt '*' at.at '"22:02"' 'bin/myscript' tag=nightly runas=jim
    """

    if len(args) < 2:
        return {"jobs": []}

    # Shim to produce output similar to what __virtual__() should do
    # but __salt__ isn't available in __virtual__()
    binary = salt.utils.path.which("at")
    if not binary:
        return "'at.at' is not available."

    if "tag" in kwargs:
        stdin = "### SALT: {}\n{}".format(kwargs["tag"], " ".join(args[1:]))
    else:
        stdin = " ".join(args[1:])
    # Ensure the command is terminated with a newline. Distro-patched at
    # (Fedora/RHEL, BZ 486844) appends its job delimiter immediately after the
    # last stdin byte; without a trailing newline the marker concatenates onto
    # the final command and produces a job that never executes.
    stdin += "\n"
    cmd = [binary, args[0]]

    cmd_kwargs = {"stdin": stdin, "python_shell": False}
    if "runas" in kwargs:
        cmd_kwargs["runas"] = kwargs["runas"]
    output = __salt__["cmd.run"](cmd, **cmd_kwargs)

    if output is None:
        return "'at.at' is not available."

    if output.endswith("Garbled time"):
        return {"jobs": [], "error": "invalid timespec"}

    if output.startswith("warning: commands"):
        output = output.splitlines()[1]

    if output.startswith("commands will be executed"):
        output = output.splitlines()[1]

    output = output.split()[1]

    if __grains__["os"] in BSD:
        return atq(str(output))
    else:
        return atq(int(output))


def atc(jobid):
    """
    Print the at(1) script that will run for the passed job
    id. This is mostly for debugging so the output will
    just be text.

    CLI Example:

    .. code-block:: bash

        salt '*' at.atc <jobid>
    """
    # Shim to produce output similar to what __virtual__() should do
    # but __salt__ isn't available in __virtual__()
    output = _cmd("at", "-c", str(jobid))

    if output is None:
        return "'at.atc' is not available."
    elif output == "":
        return {"error": f"invalid job id '{jobid}'"}

    return output


def _atq(**kwargs):
    """
    Return match jobs list
    """

    jobs = []

    runas = kwargs.get("runas", None)
    tag = kwargs.get("tag", None)
    hour = kwargs.get("hour", None)
    minute = kwargs.get("minute", None)
    day = kwargs.get("day", None)
    month = kwargs.get("month", None)
    year = kwargs.get("year", None)
    if year and len(str(year)) == 2:
        year = f"20{year}"

    jobinfo = atq()["jobs"]
    if not jobinfo:
        return {"jobs": jobs}

    for job in jobinfo:

        if not runas:
            pass
        elif runas == job["user"]:
            pass
        else:
            continue

        if not tag:
            pass
        elif tag == job["tag"]:
            pass
        else:
            continue

        if not hour:
            pass
        elif f"{int(hour):02d}" == job["time"].split(":")[0]:
            pass
        else:
            continue

        if not minute:
            pass
        elif f"{int(minute):02d}" == job["time"].split(":")[1]:
            pass
        else:
            continue

        if not day:
            pass
        elif f"{int(day):02d}" == job["date"].split("-")[2]:
            pass
        else:
            continue

        if not month:
            pass
        elif f"{int(month):02d}" == job["date"].split("-")[1]:
            pass
        else:
            continue

        if not year:
            pass
        elif year == job["date"].split("-")[0]:
            pass
        else:
            continue

        jobs.append(job)

    if not jobs:
        note = "No match jobs or time format error"
        return {"jobs": jobs, "note": note}

    return {"jobs": jobs}


def jobcheck(**kwargs):
    """
    Check the job from queue.
    The kwargs dict include 'hour minute day month year tag runas'
    Other parameters will be ignored.

    CLI Example:

    .. code-block:: bash

        salt '*' at.jobcheck runas=jam day=13
        salt '*' at.jobcheck day=13 month=12 year=13 tag=rose
    """

    if not kwargs:
        return {"error": "You have given a condition"}

    return _atq(**kwargs)
