# -*- coding: utf-8 -*-
#
# pynag - Python Nagios plug-in and configuration environment
# Copyright (C) 2010 Pall Sigurdsson
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""
This module is experimental.

The idea is to create a mechanism that allows you to hook your own events into
an ObjectDefinition instance.

This enables you for example to log to file every time an object is rewritten.
"""


from __future__ import absolute_import
from __future__ import print_function
import time
from platform import node
import subprocess
from os import environ
from getpass import getuser
from pynag.Utils import bytes2str


class BaseEventHandler:

    def __init__(self, debug=False):
        self._debug = debug

    def debug(self, object_definition, message):
        """Used for any particual debug notifications"""
        raise NotImplementedError()

    def write(self, object_definition, message):
        """Called whenever a modification has been written to file"""
        raise NotImplementedError()

    def pre_save(self, object_definition, message):
        """ Called at the beginning of save() """

    def save(self, object_definition, message):
        """Called when objectdefinition.save() has finished"""
        raise NotImplementedError()


class PrintToScreenHandler(BaseEventHandler):

    """Handler that prints everything to stdout"""

    def debug(self, object_definition, message):
        """Used for any particual debug notifications"""
        if self._debug:
            print("%s: %s" % (time.asctime(), message))

    def write(self, object_definition, message):
        """Called whenever a modification has been written to file"""
        print("%s: file='%s' %s" % (time.asctime(), object_definition['meta']['filename'], message))

    def save(self, object_definition, message):
        """Called when objectdefinition.save() has finished"""
        print("%s: %s" % (time.asctime(), message))


class FileLogger(BaseEventHandler):

    """Handler that logs everything to file"""

    def __init__(self, logfile='/var/log/pynag.log', debug=False):
        BaseEventHandler.__init__(self)
        self.file = logfile
        self._debug = debug

    def _append_to_file(self, message):
        f = open(self.file, 'a')
        if not message.endswith('\n'):
            message += '\n'
        f.write(message)
        f.close()

    def debug(self, object_definition, message):
        """Used for any particular debug notifications"""
        if self._debug:
            message = "%s: %s" % (time.asctime(), message)
            self._append_to_file(message)

    def write(self, object_definition, message):
        """Called whenever a modification has been written to file"""
        message = "%s: file='%s' %s" % (time.asctime(), object_definition['meta']['filename'], message)
        self._append_to_file(message)

    def save(self, object_definition, message):
        """Called when objectdefinition.save() has finished"""
        message = "%s: %s" % (time.asctime(), message)
        self._append_to_file(message)


class GitEventHandler(BaseEventHandler):

    def __init__(self, gitdir, source, modified_by, auto_init=False, ignore_errors=False):
        """
        Commits to git repo rooted in nagios configuration directory

        It automatically raises an exception if the configuration directory
        is not a git repository.

        source = prepended to git commit messages
        modified_by = is the username in username@<hostname> for commit messages
        ignore_errors = if True, do not raise exceptions on git errors
        auto_init = If True, run git init if no git repository is found.
        """
        BaseEventHandler.__init__(self)

        # Git base is the nagios config directory
        self.gitdir = gitdir

        # Who made the change
        self.modified_by = modified_by

        # Which program did the change
        self.source = source

        # Every string in self.messages indicated a line in the eventual commit message
        self.messages = []

        self.ignore_errors = ignore_errors
        if auto_init:
            try:
                self._run_command('git status --short')
            except EventHandlerError as e:
                if e.errorcode == 128:
                    self._git_init()
        #self._run_command('git status --short')

        self._update_author()

    def debug(self, object_definition, message):
        pass

    def _update_author(self):
        """ Updates environment variables GIT_AUTHOR_NAME and EMAIL

        Returns: None
        """
        environ['GIT_AUTHOR_NAME'] = self.modified_by
        environ['GIT_AUTHOR_EMAIL'] = "%s@%s" % (self.source, node())

    def _run_command(self, command):
        """ Run a specified command from the command line. Return stdout """
        cwd = self.gitdir
        proc = subprocess.Popen(command, cwd=cwd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,)
        stdout, stderr = proc.communicate('through stdin to stdout')
        stdout = bytes2str(stdout)
        stderr = bytes2str(stderr)
        returncode = proc.returncode
        if returncode > 0 and self.ignore_errors is False:
            errorstring = "Command '%s' returned exit status %s.\n stdout: %s \n stderr: %s\n Current user: %s"
            errorstring = errorstring % (command, returncode, stdout, stderr, getuser())
            raise EventHandlerError(errorstring, errorcode=returncode, errorstring=stderr)
        return stdout

    def is_commited(self):
        """ Returns True if all files in git repo are fully commited """
        return self.get_uncommited_files() == 0

    def get_uncommited_files(self):
        """ Returns a list of files that are have unstaged changes """
        output = self._run_command("git status --porcelain")
        result = []
        for line in output.split('\n'):
            line = line.split()
            if len(line) < 2:
                continue
            result.append({'status': line[0], 'filename': " ".join(line[1:])})
        return result

    def _git_init(self, directory=None):
        """ Initilizes a new git repo in directory. If directory is none, use self.gitdir """
        self._update_author()
        self._run_command("git init")
        self._run_command("git add .")
        self._run_command("git commit -a -m 'Initial Commit'")

    def _git_add(self, filename):
        """ Wrapper around git add command """
        self._update_author()
        command = "git add '%s'" % filename
        return self._run_command(command)

    def _git_commit(self, filename, message, filelist=None):
        """ Wrapper around git commit command """
        if filelist is None:
            filelist = []
        self._update_author()
        # Lets strip out any single quotes from the message:
        message = message.replace("'", '"')
        if len(filelist) > 0:
            filename = "' '".join(filelist)
        command = "git commit '%s' -m '%s'" % (filename, message)
        return self._run_command(command=command)

    def pre_save(self, object_definition, message):
        """ Commits object_definition.get_filename() if it has any changes """
        filename = object_definition.get_filename()
        if self._is_dirty(filename):
            self._git_add(filename)
            self._git_commit(filename,
                             message="External changes commited in %s '%s'" %
                            (object_definition.object_type, object_definition.get_shortname()))

    def save(self, object_definition, message):
        filename = object_definition.get_filename()
        if len(self.messages) > 0:
            message = [message, '\n'] + self.messages
            message = '\n'.join(message)
        self._git_add(filename)
        if self._is_dirty(filename):
            self._git_commit(filename, message)
        self.messages = []

    def _is_dirty(self, filename):
        """ Returns True if filename needs to be committed to git """
        command = "git status --porcelain '%s'" % filename
        output = self._run_command(command)
        # Return True if there is any output
        return len(output) > 0

    def write(self, object_definition, message):
        # When write is called ( something was written to file )
        # We will log it in a buffer, and commit when save() is called.
        self.messages.append(" * %s" % message)


class NagiosReloadHandler(BaseEventHandler):

    """ This handler reloads nagios every time that a change is made. This is only meant for small environments
    """

    def __init__(self, nagios_init, *args, **kwargs):
        import pynag.Control
        BaseEventHandler.__init__(self, *args, **kwargs)
        self.daemon = pynag.Control.daemon(nagios_init=nagios_init)
        self.in_transaction = False

    def _reload(self):
        """ Reload nagios

        """
        self.daemon.reload()

    def debug(self, object_definition, message):
        """Used for any particual debug notifications"""
        pass

    def write(self, object_definition, message):
        """Called whenever a modification has been written to file"""
        if not self.in_transaction:
            self._reload()

    def pre_save(self, object_definition, message):
        """ Called at the beginning of save() """
        self.in_transaction = True

    def save(self, object_definition, message):
        """Called when objectdefinition.save() has finished"""
        self.in_transaction = False
        self._reload()


class EventHandlerError(Exception):

    def __init__(self, message, errorcode=None, errorstring=None):
        self.message = message
        self.errorcode = errorcode
        self.errorstring = errorstring

    def __str__(self):
        return self.errorstring
