import os
import string
import time
import fcntl
import errno
import subprocess
import re

import libxml2
import libxslt

import jon.cgi as cgi
#import jon.modpy as modpy
import jon.session as session

TIME_OUT_SEC = 120
TRUE = 1
FALSE = 0
LOOP_TEST = 10

PATH_XML_DOCS = '/var/local/lib/asimux/'
PATH_XSL_DOCS = '/srv/www/html/xsl/'
PATH_ASI_PROC = '/proc/asimux/'
PATH_UDD_FILE = '/var/local/lib/asimux/udd/'
#PATH_XSL_DOCS = '/srv/www/vhosts/asimux/xsl/'
#PATH_ASI_PROC = '/etc/asimux/'
HOST_NAME_FILE = '/etc/HOSTNAME'

STR_ADMIN_FILE = 'edje3000-web'

MESSAGE_FORMAT_PAGE = """
<html>
<head>
<title></title>
<meta http-equiv="Refresh" content="%d;URL=%s">
</head>
<body bgcolor="#CCCC99" text="black" link="black" vlink="black" alink="black">
<table width="100%%" cellspacing="0" cellpadding="0">
<tr>
    <td>&nbsp;</td>
    <td><br><br><br><br>
    <b><center><font face="Arial" color="#C6DDD3"> </font><BR>
    <font face="Arial" color="White"><a href="%s">%s</a></font></center></b><br>
<!-- Successful Logout message
    <b><center><font face="Arial" color="White">You have successfully logged out ! <BR></font></center></b><br>
-->
<!--   No new session message
    <b><center><font face="Arial" color="White">A new session is unavailable ! <BR></font></center></b><br>
-->
<br><br><br><br>
    </td>
    <td>&nbsp;</td>
</tr>
</table>
</body>
</html>
"""

REFRESH_MESSAGE = 'refresh_alert'

class Error(Exception):
    """The base class for all exceptions thrown by this module."""
    pass

class NotImplementedError(Error):
    """The exception thrown when (abstract) functions are not implemented."""
    pass

class InvalidConfigurationXmlError(Error):
    """The exception thrown when expected fields cannot be found in the XML configuration."""
    pass

class XpathCreateError(Error):
    """The exception thrown when xpath creation fails."""
    pass

class ActiveSession(session.FileSession):
#class ActiveSession(session.MemorySession):
    def __init__(self, req, secret, basedir=None, **kwargs):
        session.FileSession.__init__(self, req, secret, **kwargs)
        if not self.has_key('active'):
            self['active'] = str(int(time.time()))
        if not self.has_key('state'):
            self['state'] = 'readonly'
        #req.error('id=%s;active=%s' % (self['id'], self['active']))
    #def __init__(self, req, secret, basedir=None, **kwargs):
    #    session.FileSession.__init__(self, req, secret, basedir, **kwargs)
    #    req.error('id=%s' % (self['id']))
    #    self._admin = 0

    def active(self):
        self['active'] = str(int(time.time()))
        #self['active'] = str(time.time())

    def timeout(self, timeout=4*60):
        last_active = int(self['active'])
        if time.time() - last_active > timeout:
            return 1
        else:
            return 0
        
    def setDefaultBase(self, base):
        self["defaultBase"] = base
        
    def getDefaultBase(self):
        try:
            return self["defaultBase"]
        except KeyError:
            return "16"
        
    def setPMTTracking(self, pmt_tracking):
        self["pmt_tracking"] = pmt_tracking;
        
    def getPMTTracking(self):
        try:
            return self["pmt_tracking"]
        except KeyError:
            return "off"


class SingleAdminSession(session.FileSession):
    """a class that only allows a single admin login """    
    def login(self, timeout=4*60):
        try:
            # write session id as admin
#            if not os.path.exists("%s/%s" % (self.basedir):#'/tmp/edje3000-web'):
#                os.mkdir("%s/%s" % (self.basedir), 0700)#'/tmp/edje3000-web', 0700)
            f = open("%s/%s" % (self.basedir, STR_ADMIN_FILE), 'w+')
            try:
                f.seek(0,0)
                admin_ts_str = f.readline()
                #string.strip(admin_ts_str)
                if len(admin_ts_str) > 1:
                    admin_ts = time.mktime(time.strptime(admin_ts_str))
                else:
                    admin_ts = 0.0
                #admin_ts = time.mktime(time.strptime(f.readline()))
                admin_id = f.readline()
                if len(admin_id) > 1 and \
                   time.time() - admin_ts < timeout:
                    if admin_id == self['id']:                   
                        return 1
                    else:
                        return 0
                else:
                    f.seek(0,0)
                    f.write(time.asctime() + '\n')
                    f.write(self['id'] + '\n')
                    f.truncate()
            finally:
                f.close()
            return 1
        except:
            raise
        
    def logout(self):
        f = open("%s/%s" % (self.basedir, STR_ADMIN_FILE), 'wb')
        try:
            f.seek(0,0)
            f.write('\n')
            f.write('\n')
            f.truncate()
        finally:
            f.close()

    def timeout(self, timeout=4*60):
        f = open("%s/%s" % (self.basedir, STR_ADMIN_FILE), 'r+b')
        try:
            f.seek(0,0)
            admin_ts_str = f.readline()
            #string.strip(admin_ts_str)
            if len(admin_ts_str) > 1:
                admin_ts = time.mktime(time.strptime(admin_ts_str))
            else:
                admin_ts = 0.0                
        finally:
            f.close()
        try:
            return ((time.time() - admin_ts) > timeout)
        except:
            return 1;

    def active(self):
        """ write current time using the same id"""
        f = open("%s/%s" % (self.basedir, STR_ADMIN_FILE), 'r+b')
        try:
            f.seek(0,0)
            admin_ts_str = f.readline()
            admin_id = f.readline()
            if len(admin_id) > 1:
                f.seek(0,0)
                f.write(time.asctime() + '\n')
                f.write(admin_id)
                f.truncate()
        finally:
            f.close()           
        
        
class Handler(cgi.DebugHandler):

    """
    """

    _adminPage = None
    _roPage = None
    _pathXmlDocs = None
    _pathXslDocs = None
    _xinclude = 1
    _configFile = 'asimux.xml'
    #_password = 'USER'
    _message = ''
    _delay = 0
    _delay_message = ''
    _delay_href = ''
    _hostname = ''

    def htmlComment(self, req, msg):
        req.write('\n<!-- %s -->' % msg)

    def xpathCreate(self, path):
        # TODO: how will this handle xpath predicates, i.e. parent[@prop1=1]/child
        # get parentPath, child from path
        t = string.split(path, '/')
        parentPath = string.join(t[:-1], '/')
        child = t[-1]
        parentNode = self._muxconfig.xpathEval2(parentPath)
        if len(parentNode) == 0:
            #if parentPath[-1] == ']':
            #    # stop recursion
            #    raise XpathCreateError()
            parentNode = [self.xpathCreate(parentPath)]
        if child[-1] == ']':
            t = child[:-1].split('[')
            childEl = t[0]
            childPred = t[1]
            if childPred[0] == '@':
                childPredPropVal = childPred[1:].split('=')
                childNode = parentNode[0].newChild(None, childEl, None)
                childNode.newProp(childPredPropVal[0], childPredPropVal[1])
                return childNode
            else:
                raise XpathCreateError()
        elif child[0] == '@':
            return parentNode[0].newProp(child[1:], '')
        else:
            return parentNode[0].newChild(None, child, None)
        
    def getPortPNQualifierFromXpath(self, req, xpath):
        idxPortStart = xpath.index('[')
        idxPortEnd = xpath.index(']')
        idxPNStart = xpath[idxPortEnd+1:].index('[')
        idxPNEnd = xpath[idxPortEnd+1:].index(']')
        port = xpath[idxPortStart+1:idxPortEnd]
        pn = xpath[idxPortEnd+idxPNStart+2:idxPortEnd+idxPNEnd+1]
        return port, pn

    def xpathSet(self, req, xpath, val, **kwargs):
        #req.error(">>>> %s, %s" % (xpath, val))
#        for k,v in kwargs.items():
#            self.htmlComment(req, 'arg %s val %s' % (k, v))
        ts = time.time()
        try:
            #if kwargs.has_key('updateIfNotEqual') and kwargs['updateIfNotEqual'].replace('_', ' ') == val:
                #self.htmlComment(req, 'no update required %s, %s (%g)' % (xpath, val, time.time() - ts))
            #elif xpath[-6:] == '@index':
            if xpath[-6:] == '@index':
                #self.htmlComment(req, 'SPECIAL CASE scramble/@index: %s, %s (%g)' % (xpath, val, time.time() - ts))
                # if val == 'No', remove old scramble
                if val == 'No':
                    nodes = self._muxconfig.xpathEval2(xpath[:-7])
                    if len(nodes) == 0:
                        nodes = [self.xpathCreate(xpath)]
                    nodes[0].setContent(val)
                    self.xpathRemove(xpath)
                # else {create and] set scramble/@index and scramble to match
                else:
                    # get @index node and set value
                    nodes = self._muxconfig.xpathEval2(xpath)
                    if len(nodes) == 0:
                        nodes = [self.xpathCreate(xpath)]
                    nodes[0].setContent(val)
                    # extra step, set scramble content to match index
                    nodes = self._muxconfig.xpathEval2(xpath[:-7])  # strip of "/@index" to get scramble node
                    caEnable = self._muxconfig.xpathEval2("/configuration/ca[@index='%s']/enable" % (val))
                    if len(caEnable) and len(nodes):
                        nodes[0].setContent(caEnable[0].content)
                #self.htmlComment(req, 'SPECIAL CASE scramble/@index: complete')
            elif xpath[-16:] == '@access_criteria':
                #self.htmlComment(req, 'SPECIAL CASE scramble/@access_criteria: %s, %s (%g)' % (xpath, val, time.time() - ts))
                # when is it appropriate to add this property?
                scrambleNodes = self._muxconfig.xpathEval2(xpath[:-17])
                if (val != '' 
                        and len(scrambleNodes) > 0):
                        #and scrambleNodes[0].content != 'No'):
                    nodes = self._muxconfig.xpathEval2(xpath);
                    if len(nodes) == 0:
                        nodes = [self.xpathCreate(xpath)]
                    nodes[0].setContent(val)
            elif xpath[-23:] == '@access_criteria_length':
                #self.htmlComment(req, 'SPECIAL CASE scramble/@access_criteria_length: %s, %s (%g)' % (xpath, val, time.time() - ts))
                scrambleNodes = self._muxconfig.xpathEval2(xpath[:-24])
                if (val != '' 
                        and len(scrambleNodes) > 0):
                        #and scrambleNodes[0].content != 'No'):
                    nodes = self._muxconfig.xpathEval2(xpath);
                    if len(nodes) == 0:
                        nodes = [self.xpathCreate(xpath)]
                    nodes[0].setContent(val)
            else:
                nodes = self._muxconfig.xpathEval2(xpath);
                if len(nodes) >= 1:
                    nodes[0].setContent(val)
                    if kwargs.has_key('txt_enc_enable'):
                        nodes[0].setProp('text_encoding_enable', kwargs['txt_enc_enable'])
                    if kwargs.has_key('base'):
                        nodes[0].setProp('base', kwargs['base'])
                elif kwargs.has_key('create') and kwargs['create'] == '1':
                    nodes = [self.xpathCreate(xpath)]
                    if len(nodes) >= 1:
                        nodes[0].setContent(val)
                        if kwargs.has_key('txt_enc_enable'):
                            nodes[0].newProp('text_encoding_enable', kwargs['txt_enc_enable'])
                #else:
                    #req.error('xpath does not exist: %s (%g)\n' % (xpath, time.time() - ts))
                    #self.htmlComment(req, 'xpath does not exist: %s (%g)' % (xpath, time.time() - ts))
        except XpathCreateError:
            req.error("xpathCreate error: '%s' (%g)\n" % (xpath, time.time() - ts))
        except libxml2.xpathError:
            req.error('xpathError: %s (%g)\n' % (xpath, time.time() - ts))
        except Exception, e:
            req.error('Exception: %s (%g)\n' % (str(e), time.time() - ts))

    def handleMultiParams(req, key):
        raise NotImplementedError()
        #return xpath, val, nvDict
        
    def processParam(self, req, key):
        #debug(req, '%s\n' % (key))
        nvDict = {}
        if key[-1] == '*':
            xpath, val, nvDict = self.handleMultiParams(req, key)
        else:
            temp = key[len('xpath:'):].split('?')
            xpath = temp[0]
            if len(temp) == 2:
                args = temp[1].split('^')
                for nvstr in args:
                    nv = nvstr.split('=')
                    nvDict[nv[0]] = nv[1]
                #for k, v in nvDict.items():
                #    self.htmlComment(req, 'arg %s has value %s' % (k, v))
            val = req.params[key]
            #debug(req, '%s, %s\n' % (xpath, val))
        return xpath, val, nvDict

    def _getState(self, req):
        if ('state' in self._session):
            state = self._session['state']
        else:
            state = None              
        return state
        #pw = req.params.get('admin_pw')
        #if (pw == self._password) or \
        #   (state == 'admin'):
        #    #self._setState('admin')
        #    return 'admin'
        #else:
        #    return None
        
    def _setState(self, state):
        self._session['state'] = state
        
    def start_session(self, req):
        #self._session = SingleAdminSession(req, "jtSecret", cookie='edje3000')
        self._session = ActiveSession(req, "jtSecret", cookie='edje3000')
        #self._session = session.FileSession(req, "jtSecret", cookie='edje3000')
    
    def end_session(self, req):
        #raise NotImplementedError, "end_session() must be overridden"
        try:
            self._session.save()
        except IOError:
            pass

    def getHostname(self):
        if self._hostname == None or self._hostname == "":
            f = open("%s" % (HOST_NAME_FILE), 'r')
            try:
                f.seek(0,0)
                self._hostname = "%s" % (f.readline())
            finally:
                f.close()
        return self._hostname
    
    def get_udd_info(self):
        results = []
        if (not os.path.exists(PATH_UDD_FILE)):
            return [["cnf"]]
        for filename in os.listdir(PATH_UDD_FILE):
            output = subprocess.Popen(["hexdump", PATH_UDD_FILE + filename], stdout=subprocess.PIPE).communicate()[0].split('\n')
            result = ""
            for line in output:
                pieces = line.split(' ')
                for piece in pieces:
                    if len(piece) == 4:
                        result += piece
            udd_id = result[0:2]
            result = result[4:int(result[2:4], 16)*2+4]
            results.append([udd_id, result])
        return results
    
    def set_udd_info(self, index, udd_id, data):
        subprocess.call(["dscrgen", "-i" + index, udd_id, data])
        
    def del_udd_info(self, index):
        subprocess.call(["rm", PATH_UDD_FILE + "nit-" + index + ".udd"])
        activeIndex = 0
        for filename in os.listdir(PATH_UDD_FILE):
            output = subprocess.Popen(["hexdump", PATH_UDD_FILE + filename], stdout=subprocess.PIPE).communicate()[0].split('\n')
            currentIndex = int(re.sub("\D", "", filename))
            result = ""
            if activeIndex != currentIndex:
                for line in output:
                    pieces = line.split(' ')
                    for piece in pieces:
                        if len(piece) == 4:
                            result += piece
                self.set_udd_info(str(activeIndex), result[0:2], result[4:int(result[2:4], 16) * 2 + 4])
                subprocess.call(["rm", PATH_UDD_FILE + "nit-" + str(currentIndex) + ".udd"])
            activeIndex += 1
    
    def getJaguarVersion(self):
        pieces = self._muxconfig.xpathEval2('configuration/system/sw_version')[0].content.split(".");
        if len(pieces) > 0:
            return pieces[1];
        return 2;
    
    def view(self, req):
        #raise NotImplementedError, "view() must be overridden"
        if (self._adminPage == None or 
            self._roPage == None):
            req.set_header('Content-Type', 'text/plain')
            req.write('No rendering implemented')
            return
        if self._delay:
            req.set_header('Content-Type', 'text/html')
            req.write(MESSAGE_FORMAT_PAGE % (self._delay, self._delay_href, self._delay_href, self._delay_message))
            return
        if self._xinclude:
            if self._muxconfig.xincludeProcess() == -1:
#                self.errorPage()
                self.traceback(req)
        #req.error(str(self.__class__) + '.view(): getting styledoc\n')
        pathXsl = self._pathXslDocs
        if pathXsl == None:
            #req.error(pathXsl + ',' + self._pathXslDocs)
            pathXsl = PATH_XSL_DOCS
        if self._getState(req) == 'admin':
            styledoc = libxml2.parseFile(pathXsl + self._adminPage)
        else:
            styledoc = libxml2.parseFile(pathXsl + self._roPage)
        #req.error(str(self.__class__) + '.view(): parse styledoc\n')
        style = libxslt.parseStylesheetDoc(styledoc)
        style.parseStylesheetInclude(None)
        if self._message == None:
            xslDict = {'message':"''"}
        else:
            xslDict = {'message':"'" + self._message + "'"}

        # shovel any parameters into the stylesheet dict    
        if req.params.has_key("function"):
            xslDict.update({"function":"'"+req.params["function"]+"'"})
        if req.params.has_key("destipaddr"):
            xslDict.update({"destipaddr":"'"+req.params["destipaddr"]+"'"})
        if req.params.has_key("destport"):
            xslDict.update({"destport":"'"+req.params["destport"]+"'"})
        if req.params.has_key("defaultsrcport"):
            xslDict.update({"srcport":"'"+req.params["defaultsrcport"]+"'"})
        if req.params.has_key("defaultipttl"):
            xslDict.update({"defaultipttl":"'"+req.params["defaultipttl"]+"'"})
        if req.params.has_key("inputEditIndex"):
            xslDict.update({"inputEditIndex":"'"+req.params["inputEditIndex"]+"'"})
        xslDict.update({"hostname":"'"+self.getHostname()+"'"})
        xslDict.update({"udd_info":"'" + "".join([piece+',' for pieces in self.get_udd_info() for piece in pieces]) + "'"})
        xslDict.update({"defaultBase":"'"+self._session.getDefaultBase()+"'"})
        xslDict.update({"pmt_tracking":"'"+self._session.getPMTTracking()+"'"})
        xslDict.update({"version":self.getJaguarVersion()})
        #if reg.params.has_key("function"):
        #    xslDict.update("function":"'"+req.params["function"]+"'")     
        # testing section
        #xslDict.update({'function':"'edit'"})
        #xslDict.update({"index":2})
        
        result = style.applyStylesheet(self._muxconfig, xslDict)
        req.set_header('Content-Type', 'text/html')
        #for (key, val) in xslDict.items():
        #    req.write('%s=%s\n' % (key, val))
        #__debug(req, "Result: %s\n" % result)
#        if not result.isEmpty():
#            req.write(style.saveResultToString(result))
        req.write(style.saveResultToString(result))
        #print style.saveResultToString(result)
        result.freeDoc()
        result = None
        # frees styledoc (I hope)
        style.freeStylesheet()
        style = None
        #styledoc.freeDoc()
        styledoc = None

    def apply(self, req):
        #raise NotImplementedError, "save() must be overridden"
        req.set_header('Content-Type', 'text/plain')

    def __debug(self, req):
        #raise NotImplementedError, "save() must be overridden"
        req.set_header('Content-Type', 'text/plain')
        for (key, val) in self._session.items():
            req.write('%s=%s\n' % (key, val))

    def process(self, req):
        #raise NotImplementedError, "process() must be overridden"
        self.start_session(req)
        libxml2.keepBlanksDefault(0)
        pathXml = self._pathXmlDocs
        if pathXml == None:
            pathXml = PATH_XML_DOCS
        try:
            self._muxconfig = libxml2.parseFile(pathXml + self._configFile)
        except libxml2.parserError:
            #self.errorPage()
            self.traceback(req)
        try:
            self._password = self._muxconfig.xpathEval2('configuration/user_data/admin_pw')[0].content
            self._autologout = int(self._muxconfig.xpathEval2('configuration/user_data/auto_logout_time')[0].content)
            self._message = ''
            #self._message = self._session['id']
            cnt = 0;
            mux_redundancy = "Disabled"
            while cnt < LOOP_TEST:
                try:
                    status = libxml2.parseFile(PATH_ASI_PROC + 'status.xml')
                except libxml2.parserError:
                    continue
                try:
                    mux_redundancy = status.xpathEval2('/status/redundancy/mux/mode')[0].content
                except IndexError:
                    continue
                status.freeDoc()
                #req.error("Looped: %d" % cnt)
                break
        
                #self.errorPage()
                #self.traceback(req)
            if not self._session.relocated:
                action = req.params.get('action')
                try:
                    if action == 'getStatus':
                        self._adminPage = 'status.xsl'
                        self._roPage = 'status.xsl'
                    if action == 'add_udd' and self._session.timeout() == 0:
                        self.set_udd_info(req.params.get('udd_index'), req.params.get('udd_id'), req.params.get('udd_data'))
                    elif action == 'remove_udd' and self._session.timeout() == 0:
                        self.del_udd_info(req.params.get('udd_index'))
                    elif action == 'update_base':
                        self._session.setDefaultBase(req.params.get('defaultBase'))
                    elif action == 'login':
                        pw = req.params.get('admin_pw')
                        if len(pw) == 0:
                            self._message = 'Must fill in password'
                        elif pw != self._password:
                            self._message = 'Incorrect password'
                        #elif self._session.login(self._autologout*60) != 1:
                        #    self._message = 'Admin already logged in'
                        elif mux_redundancy == 'Secondary' or \
                             mux_redundancy == 'Backup':
                            self._message = 'Cannot modify Backup'
                        else:
                            self._setState('admin')
                            self._session.active()
                        #self._message = self['id']
                        self.view(req)
                        #    self.view(req)
                        #else:
                        #    #req.error('Login failed')
                        #    req.write(MESSAGE_FORMAT_PAGE % ('system.py', 'Login failed'))
                    elif action == 'apply':
                        if self._autologout != 0 and \
                                self._session.timeout(self._autologout*60):
                            self._message = 'Admin timed out'
                            self._setState('logout')
                            self.view(req)
                        elif mux_redundancy == 'Secondary' or \
                             mux_redundancy == 'Backup':
                            self._message = 'Cannot modify Backup'
                            self._setState('logout')
                            self.view(req)
                        else:
                            self._session.active()
                            try:
                                os.mkdir('/tmp/webapplying')
                                self.apply(req)
                            except OSError, (errno, strerror):
                                req.error('[1] OSError for mkdir("/tmp/webapplying"): %s' % (strerror))
                                time.sleep(1)
                                try:
                                    os.mkdir('/tmp/webapplying')
                                    self.apply(req)
                                except OSError:
                                    req.error('[2] OSError for mkdir("/tmp/webapplying"): %s' % (strerror))
                                    self._message = 'Cannot modify configuration'
                                    self.view(req)
                            try:
                                os.rmdir('/tmp/webapplying')
                            except OSError:
                                req.error('[1] OSError for rmdir("/tmp/webapplying"): %s' % (strerror))
                    elif action == 'logout':
                        #self._session.logout()
                        #self._session['state'] = 'logout'
                        self._setState('')
                        self.view(req)
                    else:
                        # update session
                        #self.__debug(req)
                        #self._message = "%s %s" % (self._message, self._getState(req))
                        if self._getState(req) == 'admin':
                            if self._autologout != 0 and \
                                    self._session.timeout(self._autologout*60):
                                self._message = 'Admin timed out'
                                self._setState('logout')
                                #self.view(req)
                            elif mux_redundancy == 'Secondary' or \
                                 mux_redundancy == 'Backup':
                                self._message = 'Cannot modify Backup'
                                self._setState('logout')
                                #self.view(req)
                            else:
                                self._session.active()
                        self.view(req)
                except AttributeError, x:
                    self._message = "%s %s %s" % (self._message, 'error on page', x[0])
                    self.view(req)
        finally:
            self._muxconfig.freeDoc()
            self._muxconfig = None
            libxml2.cleanupParser()
            self.end_session(req)
        
        
