# Copyright (C) Patrick Brady, Brian Moe, Branson Stephens (2015)
# Copyright (C) Leo Singer (2016)
#
# This file is part of lvalert
#
# lvalert 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 3 of the License, or
# (at your option) any later version.
#
# It 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 lvalert.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import print_function
import sys
import os
import datetime
import logging
import libxml2
import getpass
import uuid
import six.moves.configparser
from subprocess import Popen, PIPE
from optparse import *

# pubsub import must come first because it overloads part of the
# StanzaProcessor class
from ligo.lvalert import pubsub
from ligo.lvalert.utils import safe_netrc

from pyxmpp.all import JID,Iq,Presence,Message,StreamError,TLSSettings
from pyxmpp.jabber.all import Client
from pyxmpp.jabber.simple import send_message
from pyxmpp.interface import implements
from pyxmpp.interfaces import *

"""
A tool to listen for events on a pubsub node 
"""

#################################################################
# help message
usage = """\
%prog [options]
-----------------------------------------------------------------

  A tool to listen to the pubsub service of openfire at
  lvalert.cgca.uwm.edu. LSC-Virgo members can activate their accounts 
  on this server by completing the form at
  
    https://www.lsc-group.phys.uwm.edu/cgi-bin/jabber-acct.cgi 

  and typing your password.

  Before using this program to listen to a node, you must subscribe to
  the node using lvalert_admin. Then you will receive any events that are
  published to that node by doing:

  %prog --username albert.einstein

  If you do not provide the username, then the program will look for it in
  the file ~/.netrc in your home directory. This location may be overriden
  with the NETRC environment variable or the --netrc option. This file
  should contain a line like the following:

      machine lvalert.cgca.uwm.edu login albert.einstein password ligorocks

  If a matching entry in your .netrc file is found, the program will use
  that password. Otherwise, it will prompt you interactively for your
  password.

  When an event is published to the node, a message will be printed to
  the window where the listener is running. To see the event, run

  %prog --username albert.einstein --show

  The owner (person who creates the node by default) can delete and
  publish information to the node. The owner can also add other
  publishers to the node. Configuration and management of nodes and
  subscriptions are handled with lvalert_admin. 
  
  Others can subscribe to any existing node. Run 

  lvalert_admin --help

  to find out how to manage your subscriptions. 

  It is also possible to specify actions to be taken upon receipt of a
  message from a given node using a config-file to specify the program
  to run when the message is received via different nodes. The payload
  of the message is piped to the command via stdin.  A sample
  config-file called example.ini might look like

  [lvalert_test]
  executible=./mycounter

  then running

  lvalert_listen --username albert.einstein secret --config-file example.ini

  would result in ./mycounter being executed and the output printed to
  screen whenever a message was received via the lvalert_test node. 

  Alternatively, instead of the name of an executable, you could
  indicate either "stdout" or "-" (withouth quotes) and the alert
  payload will be written to standard output.
"""

#################################################################
parser = OptionParser(usage=usage)

#username and password
parser.add_option("-a","--username",action="store",type="string",\
  help="the username of the publisher or listener" )
parser.add_option("-s","--server",action="store",type="string",\
  default="lvalert.cgca.uwm.edu", help="the pubsub server" )
parser.add_option("-r","--resource",action="store",type="string",\
  default=uuid.uuid4().hex, help="resource to use in JID [default: random]" )
parser.add_option("-c","--config-file",action="store",type="string",\
  default=None, help="config file with list of actions" )
parser.add_option("--netrc", "-N",
  help="Load username and password from this file",
  default=os.environ.get('NETRC', '~/.netrc'))

parser.add_option("-S","--show",action="store_true",\
  default=False, help="print the payload to stdout" )

parser.add_option("-n","--node",action="store",type="string",\
  default=None, help="name of the node on the pubsub server" )

# debugging options
parser.add_option("-v","--verbose",action="store_true",\
  default=False, help="be verbose as you process the request" )
parser.add_option("-g","--debug",action="store_true",\
  default=False, help="should  print out lots of information" )

# options about message handling
parser.add_option("", "--dont-wait",action="store_true",\
  default=False, help="if supplied, jobs will be launched \
  as soon as they arrive, rather than blocking")

# version
parser.add_option("-w", "--version", action="store_true",
  default=False, help="display version information")

# ============================================================================
# -- get command line arguments
opts, args = parser.parse_args()

if opts.version:
    import pkg_resources
    version = pkg_resources.require("ligo-lvalert")[0].version
    print("LVAlert v. %s" % version)
    exit(0)

try:
    default_username, _, default_password = safe_netrc(os.path.expanduser(opts.netrc)).authenticators(opts.server)
except IOError:
    default_username, default_password = None, None

myusername = opts.username or default_username
if not myusername:
    parser.error('--username is required')
if myusername == default_username:
    mypassword = default_password
else:
    mypassword = getpass.getpass('password for ' + myusername + ': ')

if opts.dont_wait: ### required packages for "dont-wait" option
    import multiprocessing
    import tempfile

#=============================================================================
### R. ESSICK 12/2/2014
def forked_wait(cmd, file_obj):
    """
    used with the "--dont-wait" option to avoid zombie processes via a double fork
    main process will wait for this function to finish (quick), send the "wait" signal
    which removes this function from the process table, and then moves on.
    the forked process that this function creates become orphaned, and will automatically
    be removed from the process table upon completion.
    """
    Popen(cmd, stdin=file_obj)

#=======================

class LVAlertHandler(object):
    """Provides the actions taken when an event arrives.
    """

    implements(IMessageHandlersProvider)
    
    def __init__(self, client, actions, wait=True):
        """Just remember who created this."""
        self.client = client
        self.actions = actions
        self.wait = wait
    
    def get_message_handlers(self):
        """Return list of (message_type, message_handler) tuples.

        The handlers returned will be called when matching message is received
        in a client session."""
        return [
            (None, self.message),
            ]

    def message(self,stanza):
        """Message handler for the component.

        Prints a message with the time an alert is received. If the
        --show option is set, then it will also print the contents of
        the alert.

        if self.wait: blocks until this processes terminates and prints the returned string
        else: does not block, and multiple events will be launched and processed as soon as they are read

        :returns: `True` to indicate, that the stanza should not be processed
        any further."""
        e=self.get_entry(stanza)
        n=self.get_node(stanza)
        if e:
            if n in self.actions:
                action = self.actions[n]
                if action == 'stdout' or action == '-':
                    sys.stdout.write(str(e))
                #===================================================================
                ### changes made by R. Essick (ressick@mit.edu) and R. Vaulin (vaulin@ligo.org)
                ###
                ### this avoids using p.communicate(), which blocks and prevents the 
                ### alert handler from launching another processes until this one finishes
                ### we use "spooled" tempfiles because they exist only in memory (if small enought)
                ### max_size=1000 (bytes?) should be larger than most expected lvalert messages?
                elif self.wait:
                    p = Popen([self.actions[n]], stdin=PIPE, stdout=PIPE)
                    print(p.communicate(e)[0])
                else:
#                    import tempfile
                    file_obj = tempfile.SpooledTemporaryFile(mode="w+r", max_size=1000)
                    file_obj.write(e)
                    file_obj.readlines() ### bug fix for "alert_type"=="new" events
                                         ### without this, the position in file_obj gets messed up
                                         ### no idea why, but it might be related to long messages becoming multiple stanzas
                    file_obj.seek(0, 0)
#                    p = Popen([self.actions[n]], stdin=file_obj)
                    p = multiprocessing.Process(target=forked_wait, args=([self.actions[n]], file_obj))
                    p.start()
                    p.join()
                    file_obj.close()
                #===================================================================
                sys.stdout.flush()
            else:
                print("Payload received at %s" % (datetime.datetime.now().ctime()))
                if opts.show:
                    print(u'%s' % (e,), end=' ')
        return True

    def get_node(self,stanza):
        c = stanza.xmlnode.children
        c = c.children
        if c:
            return c.prop("node")

    def get_entry(self,stanza):
        c = stanza.xmlnode.children
        while c:
            try:
                if c.name=="event":
                    return c.getContent()
            except libxml2.treeError:
                pass
            c = c.next
        return None

class MyClient(Client):
    def __init__(self, jid, password,actions, wait=True):
        # we require a TLS connection
        t=TLSSettings(require=True,verify_peer=False)

        # setup client with provided connection information
        # and identity data
        Client.__init__(self, jid, password, \
            auth_methods=["sasl:GSSAPI","sasl:PLAIN"], tls_settings=t,keepalive=30)

        # add the separate components
        self.interface_providers = [
            LVAlertHandler(self,actions, wait=wait),
            ]

    def stream_state_changed(self,state,arg):
        """This one is called when the state of stream connecting the
        component to a server changes. This will usually be used to
        let the user know what is going on."""
        if opts.verbose:
            print("*** State changed: %s %r ***" % (state,arg))
        else:
            pass

# add a logger so that we can see what's going
if opts.debug:
    logger=logging.getLogger()
    logger.addHandler(logging.StreamHandler())
    logger.setLevel(logging.DEBUG)
else:
    logging.basicConfig(level=logging.ERROR)

# debug the memory
libxml2.debugMemory(1)

# set up handlers for each node
actions={}
if opts.config_file:
    cp=six.moves.configparser.ConfigParser()
    cp.read(opts.config_file)
    for node in cp.sections():
        try:
            actions[node] = cp.get(node,'executible')
        except six.moves.configparser.NoOptionError:
            actions[node] = cp.get(node,'executable')

# set up the stream
# append UUID to resource to guarantee that it is non-empty and unique
myjid=JID(myusername+"@"+opts.server+"/"+opts.resource)
s=MyClient(myjid,mypassword,actions, wait=not opts.dont_wait)

if opts.verbose:
    print("connecting...")
s.connect()

if opts.verbose:
    print("listening for message...")
try:
    s.loop(1)
except KeyboardInterrupt:
    print(u"disconnecting...")
    s.disconnect()

# vi: sts=4 et sw=4
