"""Nrpe utils module."""
import glob
import os
import shutil
import subprocess

from charmhelpers import fetch
from charmhelpers.core import hookenv
from charmhelpers.core import host
from charmhelpers.core.services import helpers
from charmhelpers.core.services.base import (
    ManagerCallback,
    PortManagerCallback,
)
from charmhelpers.core.templating import render

import nrpe_helpers

import yaml


def restart_rsync(service_name):
    """Restart rsync."""
    host.service_restart("rsync")


def restart_nrpe(service_name):
    """Restart nrpe."""
    host.service_restart("nagios-nrpe-server")


def determine_packages():
    """Return a list of packages this charm needs installed."""
    pkgs = [
        "nagios-nrpe-server",
        "nagios-plugins-basic",
        "nagios-plugins-standard",
        "python3",
        "python3-netifaces",
    ]
    if hookenv.config("export_nagios_definitions"):
        pkgs.append("rsync")
    if hookenv.config("nagios_master") not in ["None", "", None]:
        pkgs.append("rsync")
    return pkgs


def install_packages(service_name):
    """Install packages."""
    fetch.apt_update()
    apt_options = [
        # avoid installing rpcbind LP#1873171
        "--no-install-recommends",
        # and retain the default option too
        "--option=Dpkg::Options::=--force-confold",
    ]
    fetch.apt_install(determine_packages(), options=apt_options, fatal=True)


def remove_host_export_fragments(service_name):
    """Remove nagios host config fragment."""
    for fname in glob.glob("/var/lib/nagios/export/host__*"):
        os.unlink(fname)


def install_charm_files(service_name):
    """Install files shipped with charm."""
    # The preinst script of nagios-nrpe-server deb package will add nagios user
    # and create this dir as home
    # ref: https://git.launchpad.net/ubuntu/+source/nagios-nrpe/tree/debian/nagios-nrpe-server.preinst#n28  # NOQA: E501
    nagios_home = "/var/lib/nagios"

    # it's possible dir owner be changed to root by other process, e.g.: LP1866382
    # here we ensure owner is nagios, but didn't apply it resursively intentionally.
    shutil.chown(nagios_home, user="nagios", group="nagios")

    # the `2` in mode will setgid for group, set dir permission to `drwxr-sr-x`.
    # the `s` (setgid) will ensure any file created in this dir inherits parent dir
    # group `nagios`, regardless of the effective user, such as root.
    os.chmod(nagios_home, 0o2755)  # 2 will set the s flag for group

    nag_dirs = [
        "/etc/nagios/nrpe.d/",
        "/usr/local/lib/nagios/plugins",
        "/var/lib/nagios/export/",
    ]
    for nag_dir in nag_dirs:
        if not os.path.exists(nag_dir):
            host.mkdir(nag_dir, perms=0o755)
    charm_file_dir = os.path.join(hookenv.charm_dir(), "files")
    charm_plugin_dir = os.path.join(charm_file_dir, "plugins")
    pkg_plugin_dir = "/usr/lib/nagios/plugins/"
    local_plugin_dir = "/usr/local/lib/nagios/plugins/"

    shutil.copy2(
        os.path.join(charm_file_dir, "nagios_plugin.py"),
        pkg_plugin_dir + "/nagios_plugin.py",
    )
    shutil.copy2(
        os.path.join(charm_file_dir, "nagios_plugin3.py"),
        pkg_plugin_dir + "/nagios_plugin3.py",
    )
    shutil.copy2(os.path.join(charm_file_dir, "default_rsync"), "/etc/default/rsync")
    shutil.copy2(os.path.join(charm_file_dir, "rsyncd.conf"), "/etc/rsyncd.conf")
    host.mkdir("/etc/rsync-juju.d", perms=0o755)
    host.rsync(charm_plugin_dir, "/usr/local/lib/nagios/", options=["--executability"])
    for nagios_plugin in ("nagios_plugin.py", "nagios_plugin3.py"):
        if not os.path.exists(local_plugin_dir + nagios_plugin):
            os.symlink(pkg_plugin_dir + nagios_plugin, local_plugin_dir + nagios_plugin)


def render_nrpe_check_config(checkctxt):
    """Write nrpe check definition."""
    # Only render if we actually have cmd parameters
    if checkctxt["cmd_params"]:
        render(
            "nrpe_command.tmpl",
            "/etc/nagios/nrpe.d/{}.cfg".format(checkctxt["cmd_name"]),
            checkctxt,
        )


def render_nrped_files(service_name):
    """Render each of the predefined checks."""
    for checkctxt in nrpe_helpers.SubordinateCheckDefinitions()["checks"]:
        # Clean up existing files
        for fname in checkctxt["matching_files"]:
            try:
                os.unlink(fname)
            except FileNotFoundError:
                # Don't clean up non-existent files
                pass
        render_nrpe_check_config(checkctxt)
    process_local_monitors()
    process_user_monitors()


def process_user_monitors():
    """Collect the user defined local monitors from config."""
    if hookenv.config("monitors"):
        monitors = yaml.safe_load(hookenv.config("monitors"))
    else:
        return
    try:
        local_user_checks = monitors["monitors"]["local"].keys()
    except KeyError as e:
        hookenv.log("no local monitors found in monitors config: {}".format(e))
        return
    for checktype in local_user_checks:
        for check in monitors["monitors"]["local"][checktype].keys():
            check_def = nrpe_helpers.NRPECheckCtxt(
                checktype, monitors["monitors"]["local"][checktype][check], "user"
            )
            render_nrpe_check_config(check_def)


def process_local_monitors():
    """Get all the monitor dicts and write out and local checks."""
    monitor_dicts = nrpe_helpers.MonitorsRelation().get_monitor_dicts()
    for monitor_src in monitor_dicts.keys():
        monitor_dict = monitor_dicts[monitor_src]
        if not (monitor_dict and "local" in monitor_dict["monitors"]):
            continue
        monitors = monitor_dict["monitors"]["local"]
        for checktype in monitors:
            for check in monitors[checktype]:
                render_nrpe_check_config(
                    nrpe_helpers.NRPECheckCtxt(
                        checktype,
                        monitors[checktype][check],
                        monitor_src,
                    )
                )


def update_nrpe_external_master_relation(service_name):
    """Update nrpe external master relation.

    Send updated nagios_hostname to charms attached
    to nrpe_external_master relation.
    """
    principal_relation = nrpe_helpers.PrincipalRelation()
    for rid in hookenv.relation_ids("nrpe-external-master"):
        hookenv.relation_set(
            relation_id=rid, relation_settings=principal_relation.provide_data()
        )


def update_monitor_relation(service_name):
    """Send updated monitor yaml to charms attached to monitor relation."""
    monitor_relation = nrpe_helpers.MonitorsRelation()
    for rid in hookenv.relation_ids("monitors"):
        hookenv.relation_set(
            relation_id=rid, relation_settings=monitor_relation.provide_data()
        )


def has_consumer():
    """Check for the monitor relation or external monitor config."""
    return hookenv.config("nagios_master") not in ["None", "", None] or bool(
        hookenv.relation_ids("monitors")
    )


class TolerantPortManagerCallback(PortManagerCallback):
    """Manage unit ports.

    Specialization of the PortManagerCallback. It will open or close
    ports as its superclass, but will not raise an error on conflicts
    for opening ports

    For context, see:
    https://bugs.launchpad.net/juju/+bug/1750079 and
    https://github.com/juju/charm-helpers/pull/152
    """

    def __call__(self, manager, service_name, event_name):
        """Open unit ports."""
        service = manager.get_service(service_name)
        new_ports = service.get("ports", [])
        port_file = os.path.join(hookenv.charm_dir(), ".{}.ports".format(service_name))
        if os.path.exists(port_file):
            with open(port_file) as fp:
                old_ports = fp.read().split(",")
            for old_port in old_ports:
                if bool(old_port) and not self.ports_contains(old_port, new_ports):
                    hookenv.close_port(old_port)
        with open(port_file, "w") as fp:
            fp.write(",".join(str(port) for port in new_ports))
        for port in new_ports:
            # A port is either a number or 'ICMP'
            protocol = "TCP"
            if str(port).upper() == "ICMP":
                protocol = "ICMP"
            if event_name == "start":
                try:
                    hookenv.open_port(port, protocol)
                except subprocess.CalledProcessError as err:
                    if err.returncode == 1:
                        hookenv.log(
                            "open_port returns: {}, ignoring".format(err),
                            level=hookenv.INFO,
                        )
                    else:
                        raise
            elif event_name == "stop":
                hookenv.close_port(port, protocol)


maybe_open_ports = TolerantPortManagerCallback()


class ExportManagerCallback(ManagerCallback):
    """Defer lookup of nagios_hostname.

    This class exists in order to defer lookup of nagios_hostname()
    until the template is ready to be rendered.  This should reduce the
    incidence of incorrectly-rendered hostnames in /var/lib/nagios/exports.
    See charmhelpers.core.services.base.ManagerCallback and
    charmhelpers.core.services.helpers.TemplateCallback for more background.
    """

    def __call__(self, manager, service_name, event_name):
        """Render export_host.cfg."""
        nag_hostname = nrpe_helpers.PrincipalRelation().nagios_hostname()
        target = "/var/lib/nagios/export/host__{}.cfg".format(nag_hostname)
        renderer = helpers.render_template(
            source="export_host.cfg.tmpl",
            target=target,
            perms=0o644,
        )
        renderer(manager, service_name, event_name)


create_host_export_fragment = ExportManagerCallback()
