#!/usr/local/nsc/bin/python
# vim: tabstop=4 softtabstop=4 shiftwidth=4 textwidth=80 smarttab expandtab

"""
* Copyright (C) 2012  Sangoma Technologies Corp.
* All Rights Reserved.
*
* Author(s)
* Johnny Ma <jma@sangoma.com>
*
* This code is Sangoma Technologies Confidential Property.
* Use of and access to this code is covered by a previously executed
* non-disclosure agreement between Sangoma Technologies and the Recipient.
* This code is being supplied for evaluation purposes only and is not to be
* used for any other purpose.
"""

import sys
import oswc
import os
import time as mytime
import sngpy
import pyodbc
import string
import logging

#from sngpy import Service, DailyTask
from datetime import *
from optparse import OptionParser


#Cleanup Thread function
def dbclean_func(daystokeep, dbstring, tblname):

    #Check input arguments first
    if daystokeep <= 0:
        return

    #delete records here
    try:
        myconn = pyodbc.connect(dbstring)
    except:
        return

    mycursor = myconn.cursor()

    today = date.today()
    td = timedelta(daystokeep-1)
    deltime = datetime.combine(today-td, time(0,0,0))

    delquery = "LOCK TABLES " + tblname + " WRITE, " + tblname + " AS t1 READ"
    mycursor.execute(delquery)
    myconn.commit()

    delquery = "DELETE FROM " + tblname + " where event_time \
               < \'" + str(deltime) + "\'"
    mycursor.execute(delquery)
    myconn.commit()

    delquery = "UNLOCK TABLES"
    mycursor.execute(delquery)
    myconn.commit()

    myconn.close()
            

#Definition of RTCPMonitor Class
class RTCPMonitor(sngpy.DBService):

    _service_name = "rtcpmon"

    _dbconf = dict()
    _dbconf_params = ['connection-string', 'table-prefix']

    _osconf = dict()
    _osconf_params = ['connection-string']

    _dbclean = dict()
    _dbclean_params = ['daily-time', 'days-keep-records']

    def __init__(self, logger):
        self._dbconn = None
        self._tblname = 'reports'
        self._logger = logger

        formatter = logging.Formatter(self._logformat)

        console_handler = logging.StreamHandler()
        console_handler.setFormatter(formatter)
        self._logger.addHandler(console_handler)

        self.parser = OptionParser()
        self.parser.add_option("", "--init-database", action="store_true",
                            dest="init_database",
                            help="Initialize Database")

        super(RTCPMonitor, self).__init__()

    def _parse_db(self, params):
        self._logger.debug("Reading database parameters")
        for p in params:
            self._logger.debug("database: %s=%s" % (p.attrib['name'], p.attrib['value']))
            if p.attrib['name'] in self._dbconf_params:
                self._dbconf[p.attrib['name']] = p.attrib['value']
            else:
                raise ValueError, "Unknown database XML parameter %s" % p.attrib['name']
        if 'connection-string' not in self._dbconf:
                raise ValueError, "Missing database connection-string XML parameter %s" % p.attrib['name']
        if 'table-prefix' not in self._dbconf:
                self._dbconf['table-prefix'] = ''

    def _parse_switch(self, params):
        self._logger.debug("Reading switch parameters")
        for p in params:
            self._logger.debug("switch: %s=%s" % (p.attrib['name'], p.attrib['value']))
            if p.attrib['name'] in self._osconf_params:
                self._osconf[p.attrib['name']] = p.attrib['value']
            else:
                raise ValueError, "Unknown switch XML parameter %s" % p.attrib['name']
        if 'connection-string' not in self._osconf:
                raise ValueError, "Missing switch connection-string XML parameter %s" % p.attrib['name']

    def _parse_dbclean(self, params):
        self._logger.debug("Reading dbcleanup parameters")
        for p in params:
            self._logger.debug("dbcleanup: %s=%s" % (p.attrib['name'], p.attrib['value']))
            if p.attrib['name'] in self._dbclean_params:
                self._dbclean[p.attrib['name']] = p.attrib['value']
            else:
                raise ValueError, "Unknown dbcleanup XML parameter %s" % p.attrib['name']
        if 'daily-time' not in self._dbclean:
                raise ValueError, "Missing dbcleanup daily-time XML parameter %s" % p.attrib['name']
        if 'days-keep-records' not in self._dbclean:
                raise ValueError, "Missing dbcleanup days-keep-records XML parameter %s" % p.attrib['name']
        
    def configure(self):
        ret = True
        try:

            tree = super(RTCPMonitor,self).configure()

            dbconf = tree.find('database')
            if dbconf is None:
                raise ValueError, "Missing <database> configuration"
            else:
                params = self._get_params(dbconf)
                self._parse_db(params)

            osconf = tree.find('switch')
            if osconf is None:
                raise ValueError, "Missing <switch> configuration"
            else:
                params = self._get_params(osconf)
                self._parse_switch(params)

            dbclean = tree.find('dbcleanup')
            if dbclean is None:
                raise ValueError, "Missing <dbcleanup> configuration"
            else:
                params = self._get_params(dbclean)
                self._parse_dbclean(params)

        except:
            self._logger.critical("Failed to configure service")
            exc_type, exc_value, exc_traceback = sys.exc_info()
            self._print_exception(exc_type, exc_value, exc_traceback)
            ret = False

        return ret

    def _db_init(self):
        try:
            dbcursor = self._dbconn.cursor()

            #Second, create table if needed
            self._tblname = self._dbconf['table-prefix'] + "reports"
            dbquery = "CREATE  TABLE IF NOT EXISTS " + self._tblname + """ ( 
                `call_leg_unique_id` VARCHAR(50) NOT NULL COMMENT 'NSC Session Call Leg Unique ID' ,
                `inflow_rtp_flag` TINYINT(3) UNSIGNED NOT NULL COMMENT 'This record for an inflow RTP or not' ,
                `event_time` DATETIME NOT NULL COMMENT 'This event is generated at local time of' ,
                `session_start_time` DATETIME NOT NULL COMMENT 'NSC session started at local time of' ,
                `session_stop_time` DATETIME NULL DEFAULT NULL COMMENT 'NSC session stopped at local time of' ,
                `local_member_ip` VARCHAR(16) NOT NULL COMMENT 'NSC Vocallo Chip IP Address' ,
                `remote_member_ip` VARCHAR(16) NOT NULL COMMENT 'RTP Session Remote Member IP Address' ,
                `sent_pkt_cnt` INT(11) UNSIGNED NOT NULL COMMENT 'RTP Packets sent in this direction' ,
                `sent_byte_cnt` INT(11) UNSIGNED NOT NULL COMMENT 'Bytes sent in this direction' ,
                `sender_report_cnt` INT(11) UNSIGNED NOT NULL COMMENT 'Number of SR ever sent' ,
                `cumulative_lost_cnt` INT(11) UNSIGNED NOT NULL COMMENT 'Total Number of lost RTP packets in 
                 this direction' ,
                `max_fraction_lost` TINYINT(3) UNSIGNED NOT NULL COMMENT 'Maximum Fraction Lost ever happened 
                 in this direction' ,
                `max_inter_arrival_jitter` INT(11) UNSIGNED NOT NULL COMMENT 'Maximum InterArrival Jitter 
                 ever happened in this direction' ,
                `average_inter_arrival_jitter` INT(11) UNSIGNED NOT NULL COMMENT 'Average InterArrival 
                 Jitter of all that ever happened in this direction' ,
                `max_round_trip_time` INT(11) UNSIGNED NOT NULL COMMENT 'Maximum RTT in milliseconds' ,
                `average_round_trip_time` INT(11) UNSIGNED NOT NULL COMMENT 'Average RTT in milliseconds' ,
                `codec` VARCHAR(20) NOT NULL COMMENT 'codec used' ,
                `sampling_rate` INT(11) UNSIGNED NOT NULL COMMENT 'Sampling Rate for this codec' )
                ENGINE = InnoDB
                DEFAULT CHARACTER SET = utf8"""
            dbcursor.execute(dbquery)
        except:
            self._logger.error("Failed to initialize database")
            raise

    def _db_connect(self, retry=False):
        self._dbconn = None
        db_conn_str = self._dbconf['connection-string']
        while self.daemon_alive:
            try:
                self._dbconn = pyodbc.connect(db_conn_str)
                break
            except (KeyboardInterrupt, SystemExit):
                raise
            except Exception, e:
                self._logger.error("Failed to connect to the database %s: %s" % (db_conn_str, str(e)))
                if not retry:
                    raise
                mytime.sleep(5)
                
        if self._dbconn is not None:
            self._db_init()

    def init_database(self):
        # connection to the database takes care of initialization
        try:
            self._db_connect()
            return True
        except:
            return False

    def run(self):

        super(RTCPMonitor, self).run()

        # Connect to the database
        self._db_connect(retry=True)

        dbcursor = self._dbconn.cursor()

        # Start dailytask to cleanup
        tlist = self._dbclean['daily-time'].split(':')
        if len(tlist) != 3:
            raise Exception, "dbcleanup daily-time in wrong format, HH:MM:SS"

        cleantime = time(eval(tlist[0]), eval(tlist[1]), eval(tlist[2]))
        dtask = sngpy.DailyTask(timedotask = cleantime, 
              function = dbclean_func, 
              args = (eval(self._dbclean['days-keep-records']), 
                              self._dbconf['connection-string'], self._tblname)
              )
        dtask.start()
        
        #Fourth, we need to connect to FreeSWITCH event socket
        osconn_string = self._osconf['connection-string']

        while self.daemon_alive:
        
            osconn = oswc.create_connection(osconn_string, logger=self._logger)
            
            if osconn is None:
                self._logger.error("Failed to create connection to %s" % (osconn_string))
                mytime.sleep(10);
                continue
    
            oslistener = RTCPStatsListener(self._logger)
            oslistener.set_filter(['RTCP_STATISTICS'])
            oslistener.config_service(self)
            osconn.add_event_listener(oslistener)
    
            disclistener = OSDisconnectListener(self._logger)
            disclistener.set_filter(['OS_DISCONNECTED'])
            osconn.add_event_listener(disclistener)
    
            try:
                if not osconn.connect():
                    self._logger.debug("Failed to connect to %s" % (osconn_string))
                    mytime.sleep(1)
                    osconn.remove_event_listener(oslistener)
                    osconn.remove_event_listener(disclistener)
                    oslistener = None
                    disclistener = None
                    osconn = None
                    continue
            except KeyboardInterrupt:
                self._logger.info("Received keyboard interrupt, aborting retry")
                break
            except SystemExit:
                self._logger.info("Program exit request, aborting retry")
                break
            except:
                self._logger.debug("Caught exception while connecting to %s" % (osconn_string))
                mytime.sleep(1)
                osconn.remove_event_listener(oslistener)
                osconn.remove_event_listener(disclistener)
                oslistener = None
                disclistener = None
                osconn = None
                continue
    
            #Fifth, forever loop to receive events
            _breakall = False
            
            while self.daemon_alive:
                try:
                    osconn.receive_event(timeout=1000)
                except (oswc.OSDisconnectException):
                    break
                except (KeyboardInterrupt, SystemExit):
                    self._logger.info("Stopping %s. User aborted." % 
    				  (self._service_name))
                    _breakall = True
                    break
                except:
                    exc_type, exc_value, exc_traceback = sys.exc_info()
                    self._print_exception(exc_type, exc_value, exc_traceback)
                    mytime.sleep(1)

            osconn.remove_event_listener(oslistener)
            osconn.remove_event_listener(disclistener)
            oslistener = None
            disclistener = None
            osconn = None
            if _breakall == True:
                break
            
        dtask.cancel()
        dtask.join()
        self._logger.info("%s is terminating" % (self._service_name))


class RTCPStatsListener(oswc.EventListener):

    _logger = None
    _service = None

    def __init__(self, logger):
        self._logger = logger
        super(oswc.EventListener, self).__init__()

    def on_event(self, e):
        mysvc = self._service
        if (mysvc is None or
            mysvc._dbconn is None or
            mysvc._dbconn.cursor() is None or
            mysvc._tblname is None):
            return;

        eventlocaltime = e.date
        remoteip = e.get_header("remote-ip")
        sentpacketscnt = eval(e.get_header("sent-packets-cnt"))
        localip = e.get_header("local-ip")
        srsentcount = eval(e.get_header("sr-sent-cnt"))
        maxrtt = eval(e.get_header("max-rtt"))
        avginterarrivaljitter = eval(e.get_header("avg-jitter"))
        uniid = e.get_header("unique-id")
        totallostrtppackets = eval(e.get_header("total-lost-rtp-packets"))
        sstarttime = e.get_header("session-start-time")
        sstoptime = e.get_header("session-stop-time")
        inflowflag = eval(e.get_header("inflow-rtp-flag"))
        avgrtt = eval(e.get_header("avg-rtt"))
        sentbytescnt = eval(e.get_header("sent-bytes-cnt"))
        maxfractionlost = eval(e.get_header("max-fraction-lost"))
        maxinterarrivaljitter = eval(e.get_header("max-jitter"))
        codec = e.get_header("sip-codec")
        samplerate = eval(e.get_header("sip-codec-rate"))
        
        dbquery = "INSERT INTO %s VALUES(\'%s\', %d, \'%s\', \'%s\', \'%s\', \
            \'%s\', \'%s\', %d, %d, %d, %d, %d, %d, %d, %d, %d, \'%s\', %d)" % \
            (mysvc._tblname, uniid, inflowflag, eventlocaltime, sstarttime, 
             sstoptime, localip, remoteip, sentpacketscnt, sentbytescnt, 
             srsentcount, totallostrtppackets, maxfractionlost, 
             maxinterarrivaljitter, avginterarrivaljitter, maxrtt, avgrtt,
             codec, samplerate)

        try:
            self.my_sql_exec(dbquery)
        except:
            raise

    def my_sql_exec(self, query):
        mysvc = self._service
        if mysvc is None:
            return

        try:
            mysvc._dbconn.cursor().execute(query)
            mysvc._dbconn.commit()
        except (pyodbc.Error):
            mysvc._dbconn = mysvc._retry_odbc_conn(mysvc._dbconf["connection-string"])
            if mysvc._dbconn is None:
                raise SystemExit # TODO: should raise meaningful exception?
            else:
                self.my_sql_exec(query)
        except:
            raise

    def config_service(self, service):
        self._service = service


class OSDisconnectListener(oswc.EventListener):

    _logger = None

    def __init__(self, logger):
        self._logger = logger
        super(oswc.EventListener, self).__init__()

    def on_event(self, e):
        raise oswc.OSDisconnectException("Open Switch is disconnected!")

## let's just keep this logger around a little bit longer, shall we? ##

logger = logging.getLogger(RTCPMonitor._service_name)
logger.setLevel(logging.DEBUG)

## main() ##

rtcpmon = None

try:
    rtcpmon = RTCPMonitor(logger)

    if rtcpmon.options.stop is not None:
        rtcpmon.stop()
        sys.exit(0)

    if rtcpmon.options.restart is not None:
        rtcpmon.restart()
        sys.exit(0)

    if rtcpmon.options.conf_path is None:
        # convenient way to retrieve -c option when service is running
        rtcpmon.options.conf_path = sngpy.Service.find_conf_path()

    if rtcpmon.options.conf_path is None:
        rtcpmon.parser.print_help()
        rtcpmon.parser.error("-c is required to find the configuration path")
        sys.exit(1)

    if rtcpmon.configure() is False:
        rtcpmon.parser.error("Failed to configure daemon using file %s" % rtcpmon.options.conf_path)
        sys.exit(1)

    if rtcpmon.options.init_database:
        if rtcpmon.init_database():
            sys.exit(0)
        else:
            sys.exit(1)

    # Following options (either start or run) should not be executed if the pid file exists
    if os.path.exists(rtcpmon.pidfile):
        logger.error("Service seems to be running already, pid file %s already exists" % rtcpmon.pidfile)
        sys.exit(1)

    # Decide whether to run in the background (Daemon mode) or foreground
    if rtcpmon.options.start is not None:
        rtcpmon.start()
    else:
        rtcpmon.run()

    sys.exit(0)
except KeyboardInterrupt:
    logger.info("Received keyboard interrupt, exiting.")
    sys.exit(0)
except SystemExit, e:
    """
    We just catch this so it won't end up in the catch-all below
    but we still must raise the exception if we want python to
    exit with the provided sys.exit() return code
    """
    if not sys.stdin.isatty():
        logger.info("SystemExit status %d" % e.code)
    raise
except:
    exc_type, exc_value, exc_traceback = sys.exc_info()
    sngpy.print_exception(exc_type, exc_value, exc_traceback)
    if not sys.stdin.isatty():
        logger.error("Unexpected exception %s/%s, aborting." % (exc_type, exc_value))
    sys.exit(1)

