'''
Name: srd_audit_log.py

Overview: Provides basic functions for managing audit messages.

Description:
    Provides a set of methods to manage messages to support
    generic audit processes. Two types of messaging is supported.
    The first type is informational messaging which is basically
    free text stored in a single column table. The second type
    supports feature level messaging that is associated with a
    feature through an ID (long integer) field. The message can
    be defined as either an ERROR or a WARNING.

Notes:

Author: Doug Crane
        Sept, 2012

Modifications:

'''

import pdb
import os
import sys
import textwrap

import arcpy

from srd.srd_exception import *
import srd.srd_logging as srd_logging
import srd.srd_misc as srd_misc
            
__author__ = 'Doug Crane'
__version__ = '1.0'

__all__ = ['SRD_AuditLog', 'SRD_AuditRecord']
# ----------------------------------------------------------
class SRD_AuditRecord(object):
    '''
    An individual audit record
    '''
    
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, featID, msg, msgType='ERROR'):
        '''
        '''

        self.featID = featID
        self.msg = msg
        self.msgType = msgType

# ----------------------------------------------------------
class SRD_AuditLog(object):
    '''
    Handles logging of audit errors
    '''
    
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, auditTablePath = None, informationalTablePath = None):
        '''
        Parameters:
            auditTablePath - path to table to contain audit messages

            informationalTablePath - path to table containing metadata for
                                     auditTablePath.
        '''

        self._logChn = srd_logging.SRD_Log()
        
        arcpy.env.overwriteOutput = True
        arcpy.gp.logHistory = False

        # Each audit instance can have both an audit and
        # informational talbe associated with it. The audit
        # tables will have messages associated with individual
        # features while the informational table is simply used
        # to store general messages.
        self.auditTablePath = auditTablePath
        self.informationalTablePath = informationalTablePath
        
        # Each audit record contains 2 items, and audit message and the audit
        # level (ERROR/WARNING). These are stored as a simple list. Multiple
        # audit records can be associated with a single feature and are stored
        # as a list of audit records. All audit records for a single feature are
        # stored in the _auditRecords dictionary which used the feature ID as the
        # key to access all the audit records for a given feature.
        self._auditRecords = {}

        # Since informational messages are not tied to any features
        # then they are just defined as a simple list.
        self._infoMessages = []

        # Set size of message field
        self.msgLen = 1000
        
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def createAuditTable(self, tablePath):
        '''

        Creates the table that will contain audit messages. If
        the table already exists it will be overwritten.

        Fields:
            ID - links to feature ID that audit messages are associated with.
            TYPE - type of audit message ERROR or WARNING
            MESSAGE - audit message.

        Parameters:
            tablePath - path to table to create.
        '''

        # Save reference to table in case user has called method.
        self.auditTablePath = tablePath

        tableWS = os.path.dirname(tablePath)
        tableName = os.path.basename(tablePath)

        # Cannot create table in feature dataset so go up a level
        wsType = arcpy.Describe(tableWS).DataType
        if wsType.upper() == 'FeatureDataset'.upper():
            raise SRD_Exception('Cannot create Audit table within a feature dataset')
        
        arcpy.CreateTable_management(tableWS, tableName)

        # Add supporting fields.
        arcpy.AddField_management(tablePath, 'ID', 'LONG')
        arcpy.AddField_management(tablePath, 'TYPE', 'TEXT', '', '', 10)
        arcpy.AddField_management(tablePath, 'MESSAGE', 'TEXT', '', '', self.msgLen)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def deleteAllTables(self):
        '''
        Deletes both the audit and information tables is they exist.
        '''

        self.deleteAuditTable()
        self.deleteInformationTable()
        
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def deleteAuditTable(self):
        '''
        Deletes the currently set audit table.
        '''

        if self.auditTablePath:
            if arcpy.Exists(self.auditTablePath):
                arcpy.Delete_management(self.auditTablePath)
            
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def createInformationTable(self, tablePath):
        '''

        Creates the table that will contain informational messages. If
        the table already exists it will be overwritten.

        Fields:
            MESSAGE - message field

        Parameters:
            tablePath - path to table to create.
        '''

        # Save reference to table in case user has called method.
        self.informationalTablePath = tablePath

        tableWS = os.path.dirname(tablePath)
        tableName = os.path.basename(tablePath)
        
        # Cannot create table in feature dataset so go up a level
        wsType = arcpy.Describe(tableWS).DataType
        if wsType.upper() == 'FeatureDataset'.upper():
            raise SRD_Exception('Cannot create Information table within a feature dataset')
            
        arcpy.CreateTable_management(tableWS, tableName)

        # Add supporting fields.
        arcpy.AddField_management(tablePath, 'MESSAGE', 'TEXT', '', '', self.msgLen)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def deleteInformationTable(self):
        '''
        Deletes the currently set information table.
        '''

        if self.informationalTablePath:
            if arcpy.Exists(self.informationalTablePath):
                arcpy.Delete_management(self.informationalTablePath)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def writeAuditRecords(self, purgeRecords = True):
        '''
        Write current set of audit records.

        Parameters:
            purgeRecords - True/False - remove all existing records once written.
                           By default this is True

        There might be multiple records associated with the
        set of messages stored. Messages for each record will
        be written. The messages will be purged once they have been
        written by default.
        '''

        # Only proceed if there are messages to write.
        if len(self._auditRecords) > 0:

            # Make sure the table we are writing to exists.
            if not arcpy.Exists(self.auditTablePath):
                self.createAuditTable(self.auditTablePath)

            fieldList = ('ID', 'TYPE', 'MESSAGE')
            cursor = arcpy.da.InsertCursor(self.auditTablePath, fieldList)

            # Process each audit record's message.
            for featureID,auditRecs in self._auditRecords.items():
                for auditRec in auditRecs:
                    msg = auditRec[0]
                    if len(msg) > self.msgLen:
                        self._logChn.logMsg('Log Message exceeds max length of %s, will truncate: %s' % (self.msgLen, msg))
                        msg = msg[:self.msgLen]
                    auditLevel = auditRec[1]
                    cursor.insertRow((featureID, auditLevel, msg))
                    
            del cursor

            # Purge all the current set of audit messages.
            if purgeRecords:
                self._auditRecords = {}

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getAuditRecords(self):
        '''
        Reads current set of audit records from the audit table
        and returns as dictionary with ID as key and all messages
        in table associated with the ID. This is intended to be used
        if you wish access to all audit records in order to create
        your own custom report.

        Example:
        
            auditTableArg = os.path.join(auditGDB, 'auditMsgs')
            infoTableArg = os.path.join(auditGDB, 'infoMsgs')
            
            auditObj = auditLog(auditTableArg, infoTableArg)

            auditDict = auditObj.getAuditMessages()
            for id, auditRecs in auditDict.items():
                logChn.logMsg('*** ID: %s' %id)
                for auditRec in auditRecs:
                    msg,auditStatus = auditRec
                    logChn.logMsg('%s - %s' % (auditStatus, msg))

        '''

        auditDict = {}
        
        # Only proceed if there is an existing table of audit records.
        if arcpy.Exists(self.auditTablePath):

            fieldList = ['ID', 'TYPE', 'MESSAGE']
            with arcpy.da.SearchCursor(self.auditTablePath, fieldList) as cursor:
                for row in cursor:
                    rec_id = row[0]
                    
                    auditLevel = row[1]
                    if not auditLevel:
                        auditLevel = ''
                    msg = row[2]
                    if not msg:
                        msg = ''

                    # Generate our audit record.
                    auditRecord = [msg, auditLevel]

                    # If we already have an entry in our dictionary
                    # retrieve the set of records and add the current one.
                    if rec_id in auditDict:
                        auditRecords = auditDict[rec_id]    
                    else:
                        auditRecords = []

                    # Add record and update dictionary of all records.
                    auditRecords.append(auditRecord)
                    auditDict[rec_id] = auditRecords


        return auditDict    
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def writeInformationMessages(self, purgeMessages = True):
        '''
        Write all current set of information messages.

        Parameters:
            purgeMessages - True/False - remove all existing message once written.
                            By default this is True
        '''

        # Only proceed if there are messages to write.
        if len(self._infoMessages) > 0:

            # Make sure the table we are writing to exists.
            if not arcpy.Exists(self.informationalTablePath):
                self.createInformationTable(self.informationalTablePath)

            fieldList = ['MESSAGE',]
            cursor = arcpy.da.InsertCursor(self.informationalTablePath, fieldList)

            for infoMessage in self._infoMessages:
                cursor.insertRow([infoMessage,])
                    
            del cursor

            # Purge all the current set of audit messages.
            if purgeMessages:
                self._infoMessages = []

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getInformationMessages(self):
        '''
        Read all the information records from the table and returns
        as a list of messages. Is intended to provide access to
        informational messages so you can write your own report.
        '''

        informationMessages = []
        
        # Only proceed if there is an existing table of audit records.
        if arcpy.Exists(self.informationalTablePath):
            fieldList = 'MESSAGE'
            with arcpy.da.SearchCursor(self.informationalTablePath, fieldList) as cursor:
                for row in cursor:
                    msg = row[0]
                    if not msg:
                        msg = ''
                    informationMessages.append(msg)

        return informationMessages
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def addAuditRecords(self, rec_id, auditMessages, auditLevel):
        '''
        Adds list of audit records to the master audit message list.

        Parameters:
            rec_id - feature ID record is to be associated with.

            auditMessages - list of audit messages to add

            auditLevel - category of record (ERROR/WARNING)
            
        '''

        for auditMessage in auditMessages:
            self.addAuditRecord(rec_id, auditMessage, auditLevel)
                
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def addAuditRecord(self, rec_id, message, auditLevel = 'ERROR'):
        '''
        Adds an audit message to the message list.

        Parameters:
            rec_id - feature ID message is to be associated with.

            message - audit message

            auditLevel - category of message (ERROR/WARNING)
            
        '''

        # Each message stored will contain the message as
        # well as the audit category.
        auditRecord = [message, auditLevel]

        if self._auditRecords.has_key(rec_id):
            messageList = self._auditRecords[rec_id]    
        else:
            messageList = []
            
        messageList.append(auditRecord)
        self._auditRecords[rec_id] = messageList

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def addInformationMessages(self, messages):
        '''
        Adds a list of information message to the message list.

        Parameters:
            messages - list of information message to add
            
        '''

        for msg in messages:
            self._infoMessages.append(msg)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def addInformationMessage(self, message):
        '''
        Adds an information message to the message list.

        Parameters:
            message - information message to add
            
        '''

        self._infoMessages.append(message)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def writeAuditReport(self, reportPath, errorsOnly = False, attDumpDict = {}, idFieldName = 'ID'):
        '''
        Generates an audit report using both the informational and
        audit records.

        Parameters:

            reportPath - path to report file to generate.

            errorsOnly - set to True if you only want errors reported.
                         Default value is false.

        Example:
        
            auditTableArg = os.path.join(auditGDB, 'auditMsgs')
            infoTableArg = os.path.join(auditGDB, 'infoMsgs')
            
            auditObj = auditLog(auditTableArg, infoTableArg)

            reportPathArg = r'D:\testData\auditLog\auditTest1.log'

            auditObj.writeAuditReport(reportPathArg, True)
            
        '''

        reportChn = open(reportPath, 'w')

        # Informational messages will be displayed first.
        msgList = self.getInformationMessages()

        for msg in msgList:
            reportChn.writelines([msg, '\n'])

        if errorsOnly:
            reportChn.writelines('\n*** ERROR Report ***\n\n')
        else:
            reportChn.writelines('\n*** ERROR and WARNING Report ***\n\n')
                             
        auditDict = self.getAuditRecords()

        selCnt = len(auditDict)
        arcpy.SetProgressor("step", "Writing audit records...", 0, selCnt, 1)

        # If user only wants error messages then remove all other messages.
        # If no error messages then remove entry from dictionary.
        # At same time accumulate statistics.
        errCnt = 0
        warnCnt = 0
        for rec_id, auditRecs in auditDict.items():
            # Determine if any the audit records are errors.
            errorMsgFnd = False
            for auditRec in auditRecs:
                msg,auditStatus = auditRec
                if auditStatus.upper() == 'ERROR':
                    errorMsgFnd = True
                    errCnt += 1
                else:
                    warnCnt += 1
            # if no error messages found then remove entry from dictionary
            # where user only wishes to see error messages.
            if errorsOnly:
                if not errorMsgFnd:
                    auditDict.pop(rec_id, '')
            arcpy.SetProgressorPosition()

        arcpy.ResetProgressor()
        
        if errorsOnly:
            reportChn.writelines('\n Total Errors: %s\n' % errCnt)
        else:
            reportChn.writelines('\n   Total Errors: %s\n' % errCnt)
            reportChn.writelines(' Total Warnings: %s\n' % warnCnt)

        reportChn.writelines('\n----------------------------------------\n\n')

        # Sort the report by the key values.
        keyList = auditDict.keys()
        keyList.sort()

        for rec_id in keyList:
            auditRecs = auditDict[rec_id]
            if len(auditRecs) > 0:
                reportChn.writelines('\n*** %s: %s\n' % (idFieldName, rec_id))
                for auditRec in auditRecs:
                    msg,auditStatus = auditRec
                    indentText = '     '
                    if errorsOnly:
                        if auditStatus.upper() == 'ERROR':
                            reportChn.writelines('\n')
                            rptMsg = textwrap.wrap(msg,width=70,initial_indent=indentText,subsequent_indent=indentText)
                            for msgLine in rptMsg:
                                reportChn.writelines('%s\n' % msgLine)
                    else:
                        reportChn.writelines('\n  %s:\n' % auditStatus)
                        rptMsg = textwrap.wrap(msg,width=70,initial_indent=indentText,subsequent_indent=indentText)
                        for msgLine in rptMsg:
                            reportChn.writelines('%s\n' % msgLine)

            # Print any attributes.
            if rec_id in attDumpDict:
                attList = attDumpDict[rec_id]
                reportChn.writelines('\n')
                for attRec in attList:
                    reportChn.writelines(attRec)
                    reportChn.writelines('\n')

        reportChn.close()
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getErrorWarningCnt(self):
        '''
        Returns a tuple containing the Error and Warning count in the audit table.

        '''

        errCnt = 0
        warnCnt = 0
        
        # Only proceed if table exists.
        if arcpy.Exists(self.auditTablePath):
            fieldList = ('ID', 'TYPE', 'MESSAGE')
            for row in arcpy.da.SearchCursor(self.auditTablePath, fieldList):
                msgType = row[1]
                if msgType:
                    if msgType == 'ERROR':
                        errCnt += 1
                    else:
                        warnCnt += 1

        return (errCnt, warnCnt)
# ======================================================================
if __name__ == '__main__':

    auditGDB = r'D:\testData\auditLog\auditTest1.gdb'
    auditReportPath = r'D:\testData\auditLog\auditTest1.log'
    
    auditTableArg = os.path.join(auditGDB, 'auditMsgs')
    infoTableArg = os.path.join(auditGDB, 'infoMsgs')
    
    auditObj = SRD_AuditLog(auditTableArg, infoTableArg)

    auditObj.deleteAllTables()
    
    for rec_id in range(1,10):
        msgList = []
        for msg in ['This is test 1', 'This is test2']:
            msgList.append(msg)
        auditObj.addAuditRecords(rec_id, msgList, 'ERROR')
        auditObj.addAuditRecords(rec_id, msgList, 'WARNING')

    auditObj.writeAuditRecords()

    msgList = []
    for rec_id in range(1,10):
        myMsg = '%s message' % rec_id
        msgList.append(myMsg)

    auditObj.addInformationMessages(msgList)

    auditObj.writeInformationMessages()

    auditDict = auditObj.getAuditRecords()
    for rec_id, auditRecs in auditDict.items():
        print('*** ID: %s' % rec_id)
        for auditRec in auditRecs:
            msg,auditStatus = auditRec
            print('%s - %s' % (auditStatus, msg))


    msgList = auditObj.getInformationMessages()
    for msg in msgList:
        print(msg)

    auditObj.writeAuditReport(auditReportPath, True)
            
