"""
Module for managing Solaris logadm based log rotations.
"""

import logging
import shlex

import salt.utils.args
import salt.utils.decorators as decorators
import salt.utils.files
import salt.utils.stringutils

log = logging.getLogger(__name__)
default_conf = "/etc/logadm.conf"
option_toggles = {
    "-c": "copy",
    "-l": "localtime",
    "-N": "skip_missing",
}
option_flags = {
    "-A": "age",
    "-C": "count",
    "-a": "post_command",
    "-b": "pre_command",
    "-e": "mail_addr",
    "-E": "expire_command",
    "-g": "group",
    "-m": "mode",
    "-M": "rename_command",
    "-o": "owner",
    "-p": "period",
    "-P": "timestmp",
    "-R": "old_created_command",
    "-s": "size",
    "-S": "max_size",
    "-t": "template",
    "-T": "old_pattern",
    "-w": "entryname",
    "-z": "compress_count",
}


def __virtual__():
    """
    Only work on Solaris based systems
    """
    if "Solaris" in __grains__["os_family"]:
        return True
    return (
        False,
        "The logadm execution module cannot be loaded: only available on Solaris.",
    )


def _arg2opt(arg):
    """
    Turn a pass argument into the correct option
    """
    res = [o for o, a in option_toggles.items() if a == arg]
    res += [o for o, a in option_flags.items() if a == arg]
    return res[0] if res else None


def _parse_conf(conf_file=default_conf):
    """
    Parse a logadm configuration file.
    """
    ret = {}
    with salt.utils.files.fopen(conf_file, "r") as ifile:
        for line in ifile:
            line = salt.utils.stringutils.to_unicode(line).strip()
            if not line:
                continue
            if line.startswith("#"):
                continue
            splitline = line.split(" ", 1)
            ret[splitline[0]] = splitline[1]
    return ret


def _parse_options(entry, options, include_unset=True):
    """
    Parse a logadm options string
    """
    log_cfg = {}
    options = shlex.split(options)
    if not options:
        return None

    ## identifier is entry or log?
    if entry.startswith("/"):
        log_cfg["log_file"] = entry
    else:
        log_cfg["entryname"] = entry

    ## parse options
    # NOTE: we loop over the options because values may exist multiple times
    index = 0
    while index < len(options):
        # log file
        if index in [0, (len(options) - 1)] and options[index].startswith("/"):
            log_cfg["log_file"] = options[index]

        # check if toggle option
        elif options[index] in option_toggles:
            log_cfg[option_toggles[options[index]]] = True

        # check if flag option
        elif options[index] in option_flags and (index + 1) <= len(options):
            log_cfg[option_flags[options[index]]] = (
                int(options[index + 1])
                if options[index + 1].isdigit()
                else options[index + 1]
            )
            index += 1

        # unknown options
        else:
            if "additional_options" not in log_cfg:
                log_cfg["additional_options"] = []
            if " " in options[index]:
                log_cfg["dditional_options"] = f"'{options[index]}'"
            else:
                log_cfg["additional_options"].append(options[index])

        index += 1

    ## turn additional_options into string
    if "additional_options" in log_cfg:
        log_cfg["additional_options"] = " ".join(log_cfg["additional_options"])

    ## ensure we have a log_file
    # NOTE: logadm assumes logname is a file if no log_file is given
    if "log_file" not in log_cfg and "entryname" in log_cfg:
        log_cfg["log_file"] = log_cfg["entryname"]
        del log_cfg["entryname"]

    ## include unset
    if include_unset:
        # toggle optioons
        for name in option_toggles.values():
            if name not in log_cfg:
                log_cfg[name] = False

        # flag options
        for name in option_flags.values():
            if name not in log_cfg:
                log_cfg[name] = None

    return log_cfg


def show_conf(conf_file=default_conf, name=None):
    """
    Show configuration

    conf_file : string
        path to logadm.conf, defaults to /etc/logadm.conf
    name : string
        optional show only a single entry

    CLI Example:

    .. code-block:: bash

        salt '*' logadm.show_conf
        salt '*' logadm.show_conf name=/var/log/syslog
    """
    cfg = _parse_conf(conf_file)

    # filter
    if name and name in cfg:
        return {name: cfg[name]}
    elif name:
        return {name: f"not found in {conf_file}"}
    else:
        return cfg


def list_conf(conf_file=default_conf, log_file=None, include_unset=False):
    """
    Show parsed configuration

    .. versionadded:: 2018.3.0

    conf_file : string
        path to logadm.conf, defaults to /etc/logadm.conf
    log_file : string
        optional show only one log file
    include_unset : boolean
        include unset flags in output

    CLI Example:

    .. code-block:: bash

        salt '*' logadm.list_conf
        salt '*' logadm.list_conf log=/var/log/syslog
        salt '*' logadm.list_conf include_unset=False
    """
    cfg = _parse_conf(conf_file)
    cfg_parsed = {}

    ## parse all options
    for entry in cfg:
        log_cfg = _parse_options(entry, cfg[entry], include_unset)
        cfg_parsed[
            log_cfg["log_file"] if "log_file" in log_cfg else log_cfg["entryname"]
        ] = log_cfg

    ## filter
    if log_file and log_file in cfg_parsed:
        return {log_file: cfg_parsed[log_file]}
    elif log_file:
        return {log_file: f"not found in {conf_file}"}
    else:
        return cfg_parsed


@decorators.memoize
def show_args():
    """
    Show which arguments map to which flags and options.

    .. versionadded:: 2018.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' logadm.show_args
    """
    mapping = {"flags": {}, "options": {}}
    for flag, arg in option_toggles.items():
        mapping["flags"][flag] = arg
    for option, arg in option_flags.items():
        mapping["options"][option] = arg

    return mapping


def rotate(name, pattern=None, conf_file=default_conf, **kwargs):
    """
    Set up pattern for logging.

    name : string
        alias for entryname
    pattern : string
        alias for log_file
    conf_file : string
        optional path to alternative configuration file
    kwargs : boolean|string|int
        optional additional flags and parameters

    .. note::
        ``name`` and ``pattern`` were kept for backwards compatibility reasons.

        ``name`` is an alias for the ``entryname`` argument, ``pattern`` is an alias
        for ``log_file``. These aliases will only be used if the ``entryname`` and
        ``log_file`` arguments are not passed.

        For a full list of arguments see ```logadm.show_args```.

    CLI Example:

    .. code-block:: bash

        salt '*' logadm.rotate myapplog pattern='/var/log/myapp/*.log' count=7
        salt '*' logadm.rotate myapplog log_file='/var/log/myapp/*.log' count=4 owner=myappd mode='0700'

    """
    ## cleanup kwargs
    kwargs = salt.utils.args.clean_kwargs(**kwargs)

    ## inject name into kwargs
    if "entryname" not in kwargs and name and not name.startswith("/"):
        kwargs["entryname"] = name

    ## inject pattern into kwargs
    if "log_file" not in kwargs:
        if pattern and pattern.startswith("/"):
            kwargs["log_file"] = pattern
        # NOTE: for backwards compatibility check if name is a path
        elif name and name.startswith("/"):
            kwargs["log_file"] = name

    ## build command
    log.debug("logadm.rotate - kwargs: %s", kwargs)
    command = f"logadm -f {conf_file}"
    for arg, val in kwargs.items():
        if arg in option_toggles.values() and val:
            command = "{} {}".format(
                command,
                _arg2opt(arg),
            )
        elif arg in option_flags.values():
            command = f"{command} {_arg2opt(arg)} {shlex.quote(str(val))}"
        elif arg != "log_file":
            log.warning("Unknown argument %s, don't know how to map this!", arg)
    if "log_file" in kwargs:
        # NOTE: except from ```man logadm```
        #   If no log file name is provided on a logadm command line, the entry
        #   name is assumed to be the same as the log file name. For example,
        #   the following two lines achieve the same thing, keeping two copies
        #   of rotated log files:
        #
        #     % logadm -C2 -w mylog /my/really/long/log/file/name
        #     % logadm -C2 -w /my/really/long/log/file/name
        if "entryname" not in kwargs:
            command = "{} -w {}".format(command, shlex.quote(kwargs["log_file"]))
        else:
            command = "{} {}".format(command, shlex.quote(kwargs["log_file"]))

    log.debug("logadm.rotate - command: %s", command)
    result = __salt__["cmd.run_all"](command, python_shell=False)
    if result["retcode"] != 0:
        return dict(Error="Failed in adding log", Output=result["stderr"])

    return dict(Result="Success")


def remove(name, conf_file=default_conf):
    """
    Remove log pattern from logadm

    CLI Example:

    .. code-block:: bash

      salt '*' logadm.remove myapplog
    """
    command = f"logadm -f {conf_file} -r {name}"
    result = __salt__["cmd.run_all"](command, python_shell=False)
    if result["retcode"] != 0:
        return dict(
            Error="Failure in removing log. Possibly already removed?",
            Output=result["stderr"],
        )
    return dict(Result="Success")
